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:
committed by
Ale Nijamkin
parent
eda61eeeeb
commit
52b30beb2d
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user