Merge "Support animatable icons to quick affordances." into tm-qpr-dev

This commit is contained in:
Ale Nijamkin
2022-11-30 23:11:00 +00:00
committed by Android (Google) Code Review
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)
}
}
}