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 +}