Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 8e06308b authored by Yein Jo's avatar Yein Jo
Browse files

Show edge glowing on keyboard connect

Scenarios:
- on keyboard connect, play the animation with a fixed duration
- while playing the animation keyborad disconnects, finish animation
  with ease out
- when config changes, force cancel animation and nullify animation
  config.

Bug: 324600132
Test: KeyboardDockingIndicationViewModelTest
Test: Replace KeyboardRepositoryImpl with CommandLineKeyboardRepository
and run adb shell cmd statusbar keyboard keyboard-connected true|false
Flag: com.android.systemui.keyboard_docking_indicator
Change-Id: Ia7130bf10aba98dcb33d983501332dd1848796ee

Change-Id: I6fd359c989368159d9b5056c00190de916346ae6
parent 0c7f6327
Loading
Loading
Loading
Loading
+44 −6
Original line number Diff line number Diff line
@@ -25,8 +25,9 @@ import com.android.systemui.surfaceeffects.utils.MathUtils.lerp

/** Glow box effect where the box moves from start to end positions defined in the [config]. */
class GlowBoxEffect(
    private val config: GlowBoxConfig,
    private val paintDrawCallback: PaintDrawCallback
    private var config: GlowBoxConfig,
    private val paintDrawCallback: PaintDrawCallback,
    private val stateChangedCallback: AnimationStateChangedCallback? = null
) {
    private val glowBoxShader =
        GlowBoxShader().apply {
@@ -39,6 +40,17 @@ class GlowBoxEffect(
    @VisibleForTesting var state: AnimationState = AnimationState.NOT_PLAYING
    private val paint = Paint().apply { shader = glowBoxShader }

    fun updateConfig(newConfig: GlowBoxConfig) {
        this.config = newConfig

        with(glowBoxShader) {
            setSize(config.width, config.height)
            setCenter(config.startCenterX, config.startCenterY)
            setBlur(config.blurAmount)
            setColor(config.color)
        }
    }

    fun play() {
        if (state != AnimationState.NOT_PLAYING) {
            return
@@ -47,20 +59,32 @@ class GlowBoxEffect(
        playEaseIn()
    }

    fun finish() {
        if (state == AnimationState.NOT_PLAYING || state == AnimationState.EASE_OUT) {
    /** Finishes the animation with ease out. */
    fun finish(force: Boolean = false) {
        // If it's playing ease out, cancel immediately.
        if (force && state == AnimationState.EASE_OUT) {
            animator?.cancel()
            return
        }

        // If it's playing either ease in or main, fast-forward to ease out.
        if (state == AnimationState.EASE_IN || state == AnimationState.MAIN) {
            animator?.pause()
            playEaseOut()
        }

        // At this point, animation state should be ease out. Cancel it if force is true.
        if (force) {
            animator?.cancel()
        }
    }

    private fun playEaseIn() {
        if (state == AnimationState.EASE_IN) {
            return
        }
        state = AnimationState.EASE_IN
        stateChangedCallback?.onStart()

        animator =
            ValueAnimator.ofFloat(0f, 1f).apply {
@@ -124,6 +148,7 @@ class GlowBoxEffect(
                doOnEnd {
                    animator = null
                    state = AnimationState.NOT_PLAYING
                    stateChangedCallback?.onEnd()
                }

                start()
@@ -144,4 +169,17 @@ class GlowBoxEffect(
        EASE_OUT,
        NOT_PLAYING,
    }

    interface AnimationStateChangedCallback {
        /**
         * Triggered when the animation starts, specifically when the states goes from
         * [AnimationState.NOT_PLAYING] to [AnimationState.EASE_IN].
         */
        fun onStart()
        /**
         * Triggered when the animation ends, specifically when the states goes from
         * [AnimationState.EASE_OUT] to [AnimationState.NOT_PLAYING].
         */
        fun onEnd()
    }
}
+8 −2
Original line number Diff line number Diff line
@@ -19,10 +19,12 @@ package com.android.systemui.keyboard

import android.hardware.input.InputSettings
import com.android.systemui.CoreStartable
import com.android.systemui.Flags
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.flags.Flags as LegacyFlag
import com.android.systemui.keyboard.backlight.ui.KeyboardBacklightDialogCoordinator
import com.android.systemui.keyboard.docking.binder.KeyboardDockingIndicationViewBinder
import com.android.systemui.keyboard.stickykeys.ui.StickyKeysIndicatorCoordinator
import dagger.Lazy
import javax.inject.Inject
@@ -34,14 +36,18 @@ class PhysicalKeyboardCoreStartable
constructor(
    private val keyboardBacklightDialogCoordinator: Lazy<KeyboardBacklightDialogCoordinator>,
    private val stickyKeysIndicatorCoordinator: Lazy<StickyKeysIndicatorCoordinator>,
    private val keyboardDockingIndicationViewBinder: Lazy<KeyboardDockingIndicationViewBinder>,
    private val featureFlags: FeatureFlags,
) : CoreStartable {
    override fun start() {
        if (featureFlags.isEnabled(Flags.KEYBOARD_BACKLIGHT_INDICATOR)) {
        if (featureFlags.isEnabled(LegacyFlag.KEYBOARD_BACKLIGHT_INDICATOR)) {
            keyboardBacklightDialogCoordinator.get().startListening()
        }
        if (InputSettings.isAccessibilityStickyKeysFeatureEnabled()) {
            stickyKeysIndicatorCoordinator.get().startListening()
        }
        if (Flags.keyboardDockingIndicator()) {
            keyboardDockingIndicationViewBinder.get().startListening()
        }
    }
}
+100 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.keyboard.docking.binder

import android.content.Context
import android.graphics.Paint
import android.graphics.PixelFormat
import android.view.WindowManager
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyboard.docking.ui.KeyboardDockingIndicationView
import com.android.systemui.keyboard.docking.ui.viewmodel.KeyboardDockingIndicationViewModel
import com.android.systemui.surfaceeffects.PaintDrawCallback
import com.android.systemui.surfaceeffects.glowboxeffect.GlowBoxEffect
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch

@SysUISingleton
class KeyboardDockingIndicationViewBinder
@Inject
constructor(
    context: Context,
    @Application private val applicationScope: CoroutineScope,
    private val viewModel: KeyboardDockingIndicationViewModel,
    private val windowManager: WindowManager
) {

    private val windowLayoutParams =
        WindowManager.LayoutParams().apply {
            width = WindowManager.LayoutParams.MATCH_PARENT
            height = WindowManager.LayoutParams.MATCH_PARENT
            format = PixelFormat.TRANSLUCENT
            type = WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG
            fitInsetsTypes = 0 // Ignore insets from all system bars
            title = "Edge glow effect"
            flags =
                (WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
                    WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE)
            setTrustedOverlay()
        }

    private var glowEffect: GlowBoxEffect? = null
    private val glowEffectView = KeyboardDockingIndicationView(context, null)

    private val drawCallback =
        object : PaintDrawCallback {
            override fun onDraw(paint: Paint) {
                glowEffectView.draw(paint)
            }
        }

    private val stateChangedCallback =
        object : GlowBoxEffect.AnimationStateChangedCallback {
            override fun onStart() {
                windowManager.addView(glowEffectView, windowLayoutParams)
            }

            override fun onEnd() {
                windowManager.removeView(glowEffectView)
            }
        }

    fun startListening() {
        applicationScope.launch {
            viewModel.edgeGlow.collect { config ->
                if (glowEffect == null) {
                    glowEffect = GlowBoxEffect(config, drawCallback, stateChangedCallback)
                } else {
                    glowEffect?.finish(force = true)
                    glowEffect!!.updateConfig(config)
                }
            }
        }

        applicationScope.launch {
            viewModel.keyboardConnected.collect { connected ->
                if (connected) {
                    glowEffect?.play()
                } else {
                    glowEffect?.finish()
                }
            }
        }
    }
}
+29 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.keyboard.docking.domain.interactor

import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyboard.data.repository.KeyboardRepository
import javax.inject.Inject

/** Listens for keyboard docking event. */
@SysUISingleton
class KeyboardDockingIndicationInteractor
@Inject
constructor(keyboardRepository: KeyboardRepository) {
    val onKeyboardConnected = keyboardRepository.isAnyKeyboardConnected
}
+42 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.keyboard.docking.ui

import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.util.AttributeSet
import android.view.View

/** View that's used for rendering keyboard docking indicator. */
class KeyboardDockingIndicationView(context: Context?, attrs: AttributeSet?) :
    View(context, attrs) {

    private var paint: Paint? = null

    override fun onDraw(canvas: Canvas) {
        if (!canvas.isHardwareAccelerated) {
            return
        }
        paint?.let { canvas.drawPaint(it) }
    }

    fun draw(paint: Paint) {
        this.paint = paint
        invalidate()
    }
}
Loading