From 52b30beb2d7e70c9eee5ab9d80fb6b6097efa4ce Mon Sep 17 00:00:00 2001 From: Alejandro Nijamkin Date: Tue, 29 Nov 2022 15:46:58 -0800 Subject: [PATCH] Support animatable icons to quick affordances. This CL adds support for animatable icons for quick affordances. It starts by supporting them in the flashlight affordance. Fix: 259295242 Test: manually tested the flashlight affordance. Made sure that toggling it on and off properly animates the icon and that turning the display off and on again does not animate the icon again. Change-Id: I4e6d261fba0431e72c3df5904510ae89469e0177 --- .../FlashlightQuickAffordanceConfig.kt | 116 +++++++++--------- .../ui/binder/KeyguardBottomAreaViewBinder.kt | 30 ++++- .../FlashlightQuickAffordanceConfigTest.kt | 108 +++++++++------- 3 files changed, 150 insertions(+), 104 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfig.kt index 49527d32d2292..62fe80a82908a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfig.kt @@ -21,50 +21,52 @@ import android.content.Context import com.android.systemui.R import com.android.systemui.animation.Expandable import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.shared.quickaffordance.ActivationState import com.android.systemui.statusbar.policy.FlashlightController +import javax.inject.Inject import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow -import javax.inject.Inject @SysUISingleton -class FlashlightQuickAffordanceConfig @Inject constructor( - @Application private val context: Context, - private val flashlightController: FlashlightController, +class FlashlightQuickAffordanceConfig +@Inject +constructor( + @Application private val context: Context, + private val flashlightController: FlashlightController, ) : KeyguardQuickAffordanceConfig { private sealed class FlashlightState { abstract fun toLockScreenState(): KeyguardQuickAffordanceConfig.LockScreenState - object On: FlashlightState() { + object On : FlashlightState() { override fun toLockScreenState(): KeyguardQuickAffordanceConfig.LockScreenState = KeyguardQuickAffordanceConfig.LockScreenState.Visible( Icon.Resource( - R.drawable.ic_flashlight_on, + R.drawable.qs_flashlight_icon_on, ContentDescription.Resource(R.string.quick_settings_flashlight_label) ), ActivationState.Active ) } - object OffAvailable: FlashlightState() { + object OffAvailable : FlashlightState() { override fun toLockScreenState(): KeyguardQuickAffordanceConfig.LockScreenState = KeyguardQuickAffordanceConfig.LockScreenState.Visible( Icon.Resource( - R.drawable.ic_flashlight_off, + R.drawable.qs_flashlight_icon_off, ContentDescription.Resource(R.string.quick_settings_flashlight_label) ), ActivationState.Inactive ) } - object Unavailable: FlashlightState() { + object Unavailable : FlashlightState() { override fun toLockScreenState(): KeyguardQuickAffordanceConfig.LockScreenState = KeyguardQuickAffordanceConfig.LockScreenState.Hidden } @@ -77,57 +79,57 @@ class FlashlightQuickAffordanceConfig @Inject constructor( get() = context.getString(R.string.quick_settings_flashlight_label) override val pickerIconResourceId: Int - get() = if (flashlightController.isEnabled) { - R.drawable.ic_flashlight_on - } else { - R.drawable.ic_flashlight_off - } + get() = R.drawable.ic_flashlight_off override val lockScreenState: Flow = - conflatedCallbackFlow { - val flashlightCallback = object : FlashlightController.FlashlightListener { - override fun onFlashlightChanged(enabled: Boolean) { - trySendWithFailureLogging( - if (enabled) { - FlashlightState.On.toLockScreenState() - } else { - FlashlightState.OffAvailable.toLockScreenState() - }, - TAG - ) - } + conflatedCallbackFlow { + val flashlightCallback = + object : FlashlightController.FlashlightListener { + override fun onFlashlightChanged(enabled: Boolean) { + trySendWithFailureLogging( + if (enabled) { + FlashlightState.On.toLockScreenState() + } else { + FlashlightState.OffAvailable.toLockScreenState() + }, + TAG + ) + } - override fun onFlashlightError() { - trySendWithFailureLogging(FlashlightState.OffAvailable.toLockScreenState(), TAG) - } + override fun onFlashlightError() { + trySendWithFailureLogging( + FlashlightState.OffAvailable.toLockScreenState(), + TAG + ) + } - override fun onFlashlightAvailabilityChanged(available: Boolean) { - trySendWithFailureLogging( - if (!available) { - FlashlightState.Unavailable.toLockScreenState() - } else { - if (flashlightController.isEnabled) { - FlashlightState.On.toLockScreenState() - } else { - FlashlightState.OffAvailable.toLockScreenState() - } - }, - TAG - ) - } + override fun onFlashlightAvailabilityChanged(available: Boolean) { + trySendWithFailureLogging( + if (!available) { + FlashlightState.Unavailable.toLockScreenState() + } else { + if (flashlightController.isEnabled) { + FlashlightState.On.toLockScreenState() + } else { + FlashlightState.OffAvailable.toLockScreenState() + } + }, + TAG + ) + } + } + + flashlightController.addCallback(flashlightCallback) + + awaitClose { flashlightController.removeCallback(flashlightCallback) } } - flashlightController.addCallback(flashlightCallback) - - awaitClose { - flashlightController.removeCallback(flashlightCallback) - } - } - - override fun onTriggered(expandable: Expandable?): - KeyguardQuickAffordanceConfig.OnTriggeredResult { - flashlightController - .setFlashlight(flashlightController.isAvailable && !flashlightController.isEnabled) + override fun onTriggered( + expandable: Expandable? + ): KeyguardQuickAffordanceConfig.OnTriggeredResult { + flashlightController.setFlashlight( + flashlightController.isAvailable && !flashlightController.isEnabled + ) return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled } @@ -141,4 +143,4 @@ class FlashlightQuickAffordanceConfig @Inject constructor( companion object { private const val TAG = "FlashlightQuickAffordanceConfig" } -} \ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt index 3276b6dd9748e..cbe512ff83ba6 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt @@ -16,6 +16,7 @@ package com.android.systemui.keyguard.ui.binder +import android.graphics.drawable.Animatable2 import android.util.Size import android.util.TypedValue import android.view.View @@ -27,12 +28,11 @@ import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle -import com.android.keyguard.KeyguardUpdateMonitor -import com.android.keyguard.LockIconViewController import com.android.settingslib.Utils import com.android.systemui.R import com.android.systemui.animation.Expandable import com.android.systemui.animation.Interpolators +import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.ui.binder.IconViewBinder import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewModel @@ -73,7 +73,8 @@ object KeyguardBottomAreaViewBinder { fun onConfigurationChanged() /** - * Returns whether the keyguard bottom area should be constrained to the top of the lock icon + * Returns whether the keyguard bottom area should be constrained to the top of the lock + * icon */ fun shouldConstrainToTopOfLockIcon(): Boolean } @@ -217,7 +218,7 @@ object KeyguardBottomAreaViewBinder { } override fun shouldConstrainToTopOfLockIcon(): Boolean = - viewModel.shouldConstrainToTopOfLockIcon() + viewModel.shouldConstrainToTopOfLockIcon() } } @@ -248,6 +249,27 @@ object KeyguardBottomAreaViewBinder { IconViewBinder.bind(viewModel.icon, view) + (view.drawable as? Animatable2)?.let { animatable -> + (viewModel.icon as? Icon.Resource)?.res?.let { iconResourceId -> + // Always start the animation (we do call stop() below, if we need to skip it). + animatable.start() + + if (view.tag != iconResourceId) { + // Here when we haven't run the animation on a previous update. + // + // Save the resource ID for next time, so we know not to re-animate the same + // animation again. + view.tag = iconResourceId + } else { + // Here when we've already done this animation on a previous update and want to + // skip directly to the final frame of the animation to avoid running it. + // + // By calling stop after start, we go to the final frame of the animation. + animatable.stop() + } + } + } + view.isActivated = viewModel.isActivated view.drawable.setTint( Utils.getColorAttrDefaultColor( diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfigTest.kt index cda701819d608..9fa7db127e1f6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfigTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfigTest.kt @@ -25,6 +25,7 @@ import com.android.systemui.keyguard.shared.quickaffordance.ActivationState import com.android.systemui.statusbar.policy.FlashlightController import com.android.systemui.utils.leaks.FakeFlashlightController import com.android.systemui.utils.leaks.LeakCheckedTest +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch import kotlinx.coroutines.test.UnconfinedTestDispatcher @@ -38,156 +39,177 @@ import org.junit.runners.JUnit4 import org.mockito.Mock import org.mockito.MockitoAnnotations +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(JUnit4::class) class FlashlightQuickAffordanceConfigTest : LeakCheckedTest() { @Mock private lateinit var context: Context private lateinit var flashlightController: FakeFlashlightController - private lateinit var underTest : FlashlightQuickAffordanceConfig + private lateinit var underTest: FlashlightQuickAffordanceConfig @Before fun setUp() { injectLeakCheckedDependency(FlashlightController::class.java) MockitoAnnotations.initMocks(this) - flashlightController = SysuiLeakCheck().getLeakChecker(FlashlightController::class.java) as FakeFlashlightController + flashlightController = + SysuiLeakCheck().getLeakChecker(FlashlightController::class.java) + as FakeFlashlightController underTest = FlashlightQuickAffordanceConfig(context, flashlightController) } @Test fun `flashlight is off -- triggered -- icon is on and active`() = runTest { - //given + // given flashlightController.isEnabled = false flashlightController.isAvailable = true val values = mutableListOf() - val job = launch(UnconfinedTestDispatcher()) { underTest.lockScreenState.toList(values)} + val job = launch(UnconfinedTestDispatcher()) { underTest.lockScreenState.toList(values) } - //when + // when underTest.onTriggered(null) val lastValue = values.last() - //then + // then assertTrue(lastValue is KeyguardQuickAffordanceConfig.LockScreenState.Visible) - assertEquals(R.drawable.ic_flashlight_on, - ((lastValue as KeyguardQuickAffordanceConfig.LockScreenState.Visible).icon as? Icon.Resource)?.res) + assertEquals( + R.drawable.qs_flashlight_icon_on, + ((lastValue as KeyguardQuickAffordanceConfig.LockScreenState.Visible).icon + as? Icon.Resource) + ?.res + ) job.cancel() } @Test fun `flashlight is on -- triggered -- icon is off and inactive`() = runTest { - //given + // given flashlightController.isEnabled = true flashlightController.isAvailable = true val values = mutableListOf() - val job = launch(UnconfinedTestDispatcher()) { underTest.lockScreenState.toList(values)} + val job = launch(UnconfinedTestDispatcher()) { underTest.lockScreenState.toList(values) } - //when + // when underTest.onTriggered(null) val lastValue = values.last() - //then + // then assertTrue(lastValue is KeyguardQuickAffordanceConfig.LockScreenState.Visible) - assertEquals(R.drawable.ic_flashlight_off, - ((lastValue as KeyguardQuickAffordanceConfig.LockScreenState.Visible).icon as? Icon.Resource)?.res) + assertEquals( + R.drawable.qs_flashlight_icon_off, + ((lastValue as KeyguardQuickAffordanceConfig.LockScreenState.Visible).icon + as? Icon.Resource) + ?.res + ) job.cancel() } @Test fun `flashlight is on -- receives error -- icon is off and inactive`() = runTest { - //given + // given flashlightController.isEnabled = true flashlightController.isAvailable = false val values = mutableListOf() - val job = launch(UnconfinedTestDispatcher()) { underTest.lockScreenState.toList(values)} + val job = launch(UnconfinedTestDispatcher()) { underTest.lockScreenState.toList(values) } - //when + // when flashlightController.onFlashlightError() val lastValue = values.last() - //then + // then assertTrue(lastValue is KeyguardQuickAffordanceConfig.LockScreenState.Visible) - assertEquals(R.drawable.ic_flashlight_off, - ((lastValue as KeyguardQuickAffordanceConfig.LockScreenState.Visible).icon as? Icon.Resource)?.res) + assertEquals( + R.drawable.qs_flashlight_icon_off, + ((lastValue as KeyguardQuickAffordanceConfig.LockScreenState.Visible).icon + as? Icon.Resource) + ?.res + ) job.cancel() } @Test fun `flashlight availability now off -- hidden`() = runTest { - //given + // given flashlightController.isEnabled = true flashlightController.isAvailable = false val values = mutableListOf() - val job = launch(UnconfinedTestDispatcher()) { underTest.lockScreenState.toList(values)} + val job = launch(UnconfinedTestDispatcher()) { underTest.lockScreenState.toList(values) } - //when + // when flashlightController.onFlashlightAvailabilityChanged(false) val lastValue = values.last() - //then + // then assertTrue(lastValue is KeyguardQuickAffordanceConfig.LockScreenState.Hidden) job.cancel() } @Test fun `flashlight availability now on -- flashlight on -- inactive and icon off`() = runTest { - //given + // given flashlightController.isEnabled = true flashlightController.isAvailable = false val values = mutableListOf() - val job = launch(UnconfinedTestDispatcher()) { underTest.lockScreenState.toList(values)} + val job = launch(UnconfinedTestDispatcher()) { underTest.lockScreenState.toList(values) } - //when + // when flashlightController.onFlashlightAvailabilityChanged(true) val lastValue = values.last() - //then + // then assertTrue(lastValue is KeyguardQuickAffordanceConfig.LockScreenState.Visible) - assertTrue((lastValue as KeyguardQuickAffordanceConfig.LockScreenState.Visible).activationState is ActivationState.Active) - assertEquals(R.drawable.ic_flashlight_on, (lastValue.icon as? Icon.Resource)?.res) + assertTrue( + (lastValue as KeyguardQuickAffordanceConfig.LockScreenState.Visible).activationState + is ActivationState.Active + ) + assertEquals(R.drawable.qs_flashlight_icon_on, (lastValue.icon as? Icon.Resource)?.res) job.cancel() } @Test fun `flashlight availability now on -- flashlight off -- inactive and icon off`() = runTest { - //given + // given flashlightController.isEnabled = false flashlightController.isAvailable = false val values = mutableListOf() - val job = launch(UnconfinedTestDispatcher()) { underTest.lockScreenState.toList(values)} + val job = launch(UnconfinedTestDispatcher()) { underTest.lockScreenState.toList(values) } - //when + // when flashlightController.onFlashlightAvailabilityChanged(true) val lastValue = values.last() - //then + // then assertTrue(lastValue is KeyguardQuickAffordanceConfig.LockScreenState.Visible) - assertTrue((lastValue as KeyguardQuickAffordanceConfig.LockScreenState.Visible).activationState is ActivationState.Inactive) - assertEquals(R.drawable.ic_flashlight_off, (lastValue.icon as? Icon.Resource)?.res) + assertTrue( + (lastValue as KeyguardQuickAffordanceConfig.LockScreenState.Visible).activationState + is ActivationState.Inactive + ) + assertEquals(R.drawable.qs_flashlight_icon_off, (lastValue.icon as? Icon.Resource)?.res) job.cancel() } @Test fun `flashlight available -- picker state default`() = runTest { - //given + // given flashlightController.isAvailable = true - //when + // when val result = underTest.getPickerScreenState() - //then + // then assertTrue(result is KeyguardQuickAffordanceConfig.PickerScreenState.Default) } @Test fun `flashlight not available -- picker state unavailable`() = runTest { - //given + // given flashlightController.isAvailable = false - //when + // when val result = underTest.getPickerScreenState() - //then + // then assertTrue(result is KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice) } -} \ No newline at end of file +}