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
This commit is contained in:
Alejandro Nijamkin
2022-11-29 15:46:58 -08:00
committed by Ale Nijamkin
parent eda61eeeeb
commit 52b30beb2d
3 changed files with 150 additions and 104 deletions

View File

@@ -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<KeyguardQuickAffordanceConfig.LockScreenState> =
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"
}
}
}

View File

@@ -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(

View File

@@ -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<KeyguardQuickAffordanceConfig.LockScreenState>()
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<KeyguardQuickAffordanceConfig.LockScreenState>()
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<KeyguardQuickAffordanceConfig.LockScreenState>()
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<KeyguardQuickAffordanceConfig.LockScreenState>()
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<KeyguardQuickAffordanceConfig.LockScreenState>()
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<KeyguardQuickAffordanceConfig.LockScreenState>()
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)
}
}
}