Merge "Delete the obsolete `StatusBar` composable." into main
diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig
index d05d23c..55278f6 100644
--- a/core/java/android/companion/virtual/flags/flags.aconfig
+++ b/core/java/android/companion/virtual/flags/flags.aconfig
@@ -32,6 +32,13 @@
}
flag {
+ namespace: "virtual_devices"
+ name: "media_projection_keyguard_restrictions"
+ description: "Auto-stop MP when the device locks"
+ bug: "348335290"
+}
+
+flag {
namespace: "virtual_devices"
name: "virtual_display_insets"
description: "APIs for specifying virtual display insets (via cutout)"
diff --git a/core/java/android/hardware/input/VirtualRotaryEncoderScrollEvent.java b/core/java/android/hardware/input/VirtualRotaryEncoderScrollEvent.java
index 3be911abe7..8c98abd 100644
--- a/core/java/android/hardware/input/VirtualRotaryEncoderScrollEvent.java
+++ b/core/java/android/hardware/input/VirtualRotaryEncoderScrollEvent.java
@@ -107,7 +107,8 @@
}
/**
- * Sets the scroll amount, normalized from -1.0 to 1.0, inclusive.
+ * Sets the scroll amount, normalized from -1.0 to 1.0, inclusive. By default, the scroll
+ * amount is 0, which results in no scroll.
* <p>
* Positive values indicate scrolling forward (e.g. down in a vertical list); negative
* values, backward.
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index 984bf65..3aa42c6 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -5668,6 +5668,33 @@
}
}
+ private static final String CACHE_KEY_QUIET_MODE_ENABLED_PROPERTY =
+ PropertyInvalidatedCache.createPropertyName(
+ PropertyInvalidatedCache.MODULE_SYSTEM, "quiet_mode_enabled");
+
+ private final PropertyInvalidatedCache<Integer, Boolean> mQuietModeEnabledCache =
+ new PropertyInvalidatedCache<Integer, Boolean>(
+ 32, CACHE_KEY_QUIET_MODE_ENABLED_PROPERTY) {
+ @Override
+ public Boolean recompute(Integer query) {
+ try {
+ return mService.isQuietModeEnabled(query);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+ @Override
+ public boolean bypass(Integer query) {
+ return query < 0;
+ }
+ };
+
+
+ /** @hide */
+ public static final void invalidateQuietModeEnabledCache() {
+ PropertyInvalidatedCache.invalidateCache(CACHE_KEY_QUIET_MODE_ENABLED_PROPERTY);
+ }
+
/**
* Returns whether the given profile is in quiet mode or not.
*
@@ -5675,6 +5702,13 @@
* @return true if the profile is in quiet mode, false otherwise.
*/
public boolean isQuietModeEnabled(UserHandle userHandle) {
+ if (android.multiuser.Flags.cacheQuietModeState()){
+ final int userId = userHandle.getIdentifier();
+ if (userId < 0) {
+ return false;
+ }
+ return mQuietModeEnabledCache.query(userId);
+ }
try {
return mService.isQuietModeEnabled(userHandle.getIdentifier());
} catch (RemoteException re) {
diff --git a/packages/SettingsLib/res/layout/zen_mode_duration_dialog.xml b/packages/SettingsLib/res/layout/zen_mode_duration_dialog.xml
index 6552296..bc5ec69 100644
--- a/packages/SettingsLib/res/layout/zen_mode_duration_dialog.xml
+++ b/packages/SettingsLib/res/layout/zen_mode_duration_dialog.xml
@@ -28,7 +28,7 @@
android:layout_height="match_parent"
android:orientation="vertical">
- <com.android.settingslib.notification.ZenRadioLayout
+ <com.android.settingslib.notification.modes.ZenRadioLayout
android:id="@+id/zen_duration_conditions"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -46,7 +46,7 @@
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical"/>
- </com.android.settingslib.notification.ZenRadioLayout>
+ </com.android.settingslib.notification.modes.ZenRadioLayout>
</LinearLayout>
</ScrollView>
\ No newline at end of file
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt
index 273a63d..72c3c17 100644
--- a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt
@@ -56,7 +56,7 @@
val backgroundHandler: Handler?,
) : ZenModeRepository {
- private val notificationBroadcasts =
+ private val notificationBroadcasts by lazy {
callbackFlow {
val receiver =
object : BroadcastReceiver() {
@@ -95,8 +95,9 @@
)
}
}
+ }
- override val consolidatedNotificationPolicy: StateFlow<NotificationManager.Policy?> =
+ override val consolidatedNotificationPolicy: StateFlow<NotificationManager.Policy?> by lazy {
if (Flags.volumePanelBroadcastFix() && android.app.Flags.modesApi())
flowFromBroadcast(NotificationManager.ACTION_CONSOLIDATED_NOTIFICATION_POLICY_CHANGED) {
notificationManager.consolidatedNotificationPolicy
@@ -105,11 +106,13 @@
flowFromBroadcast(NotificationManager.ACTION_NOTIFICATION_POLICY_CHANGED) {
notificationManager.consolidatedNotificationPolicy
}
+ }
- override val globalZenMode: StateFlow<Int?> =
+ override val globalZenMode: StateFlow<Int?> by lazy {
flowFromBroadcast(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED) {
notificationManager.zenMode
}
+ }
private fun <T> flowFromBroadcast(intentAction: String, mapper: () -> T) =
notificationBroadcasts
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt
index 096c25d..06333b61 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt
@@ -48,7 +48,6 @@
@RunWith(RobolectricTestRunner::class)
@SmallTest
class ZenModeRepositoryTest {
-
@Mock private lateinit var context: Context
@Mock private lateinit var notificationManager: NotificationManager
@@ -73,7 +72,7 @@
)
}
- @DisableFlags(android.app.Flags.FLAG_MODES_API, Flags.FLAG_VOLUME_PANEL_BROADCAST_FIX)
+ @DisableFlags(Flags.FLAG_VOLUME_PANEL_BROADCAST_FIX)
@Test
fun consolidatedPolicyChanges_repositoryEmits_flagsOff() {
testScope.runTest {
@@ -88,9 +87,7 @@
triggerIntent(NotificationManager.ACTION_NOTIFICATION_POLICY_CHANGED)
runCurrent()
- assertThat(values)
- .containsExactlyElementsIn(listOf(null, testPolicy1, testPolicy2))
- .inOrder()
+ assertThat(values).containsExactly(null, testPolicy1, testPolicy2).inOrder()
}
}
@@ -109,9 +106,7 @@
triggerIntent(NotificationManager.ACTION_CONSOLIDATED_NOTIFICATION_POLICY_CHANGED)
runCurrent()
- assertThat(values)
- .containsExactlyElementsIn(listOf(null, testPolicy1, testPolicy2))
- .inOrder()
+ assertThat(values).containsExactly(null, testPolicy1, testPolicy2).inOrder()
}
}
@@ -128,14 +123,13 @@
runCurrent()
assertThat(values)
- .containsExactlyElementsIn(
- listOf(null, Global.ZEN_MODE_OFF, Global.ZEN_MODE_ALARMS))
+ .containsExactly(null, Global.ZEN_MODE_OFF, Global.ZEN_MODE_ALARMS)
.inOrder()
}
}
private fun triggerIntent(action: String) {
- verify(context).registerReceiver(receiverCaptor.capture(), any())
+ verify(context).registerReceiver(receiverCaptor.capture(), any(), any(), any())
receiverCaptor.value.onReceive(context, Intent(action))
}
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index e2ecda3..63ce7eb 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -1143,3 +1143,13 @@
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ namespace: "systemui"
+ name: "qs_register_setting_observer_on_bg_thread"
+ description: "Registers Quick Settings content providers on background thread"
+ bug: "351766769"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt
index b8f9ca8..f655ac1 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt
@@ -83,6 +83,7 @@
import com.android.compose.PlatformButton
import com.android.compose.animation.Easings
import com.android.compose.animation.scene.ElementKey
+import com.android.compose.animation.scene.MutableSceneTransitionLayoutState
import com.android.compose.animation.scene.SceneKey
import com.android.compose.animation.scene.SceneScope
import com.android.compose.animation.scene.SceneTransitionLayout
@@ -516,13 +517,22 @@
val currentSceneKey =
if (isSplitAroundTheFold) SceneKeys.SplitSceneKey else SceneKeys.ContiguousSceneKey
- SceneTransitionLayout(
- currentScene = currentSceneKey,
- onChangeScene = {},
- transitions = SceneTransitions,
- modifier = modifier,
- enableInterruptions = false,
- ) {
+ val state = remember {
+ MutableSceneTransitionLayoutState(
+ currentSceneKey,
+ SceneTransitions,
+ enableInterruptions = false,
+ )
+ }
+
+ // Update state whenever currentSceneKey has changed.
+ LaunchedEffect(state, currentSceneKey) {
+ if (currentSceneKey != state.transitionState.currentScene) {
+ state.setTargetScene(currentSceneKey, coroutineScope = this)
+ }
+ }
+
+ SceneTransitionLayout(state, modifier = modifier) {
scene(SceneKeys.ContiguousSceneKey) {
FoldableScene(
aboveFold = aboveFold,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt
index 0673153..0cd4b68 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt
@@ -26,6 +26,7 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
@@ -33,6 +34,7 @@
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.android.compose.animation.scene.MutableSceneTransitionLayoutState
import com.android.compose.animation.scene.SceneScope
import com.android.compose.animation.scene.SceneTransitionLayout
import com.android.compose.modifiers.thenIf
@@ -78,13 +80,22 @@
WeatherClockScenes.splitShadeLargeClockScene
}
- SceneTransitionLayout(
- modifier = modifier,
- currentScene = currentScene,
- onChangeScene = {},
- transitions = ClockTransition.defaultClockTransitions,
- enableInterruptions = false,
- ) {
+ val state = remember {
+ MutableSceneTransitionLayoutState(
+ currentScene,
+ ClockTransition.defaultClockTransitions,
+ enableInterruptions = false,
+ )
+ }
+
+ // Update state whenever currentSceneKey has changed.
+ LaunchedEffect(state, currentScene) {
+ if (currentScene != state.transitionState.currentScene) {
+ state.setTargetScene(currentScene, coroutineScope = this)
+ }
+ }
+
+ SceneTransitionLayout(state, modifier) {
scene(splitShadeLargeClockScene) {
LargeClockWithSmartSpace(
shouldOffSetClockToOneHalf = !hasCustomPositionUpdatedAnimation
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt
index dfb8c49..734241e 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt
@@ -56,13 +56,7 @@
progress.collect { backEvent -> transition.dragProgress = backEvent.progress }
// Back gesture successful.
- transition.animateTo(
- if (state.canChangeScene(targetSceneForBack)) {
- targetSceneForBack
- } else {
- fromScene
- }
- )
+ transition.animateTo(targetSceneForBack)
} catch (e: CancellationException) {
// Back gesture cancelled.
transition.animateTo(fromScene)
@@ -105,12 +99,15 @@
return it
}
- currentScene = scene
+ if (scene != currentScene && state.transitionState == this && state.canChangeScene(scene)) {
+ currentScene = scene
+ }
+
val targetProgress =
- when (scene) {
+ when (currentScene) {
fromScene -> 0f
toScene -> 1f
- else -> error("scene $scene should be either $fromScene or $toScene")
+ else -> error("scene $currentScene should be either $fromScene or $toScene")
}
val animatable = Animatable(dragProgress).also { progressAnimatable = it }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
index 7c8fce8..45758c5 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
@@ -48,7 +48,6 @@
* @param transitionInterceptionThreshold used during a scene transition. For the scene to be
* intercepted, the progress value must be above the threshold, and below (1 - threshold).
* @param scenes the configuration of the different scenes of this layout.
- * @see updateSceneTransitionLayoutState
*/
@Composable
fun SceneTransitionLayout(
@@ -70,56 +69,6 @@
)
}
-/**
- * [SceneTransitionLayout] is a container that automatically animates its content whenever
- * [currentScene] changes, using the transitions defined in [transitions].
- *
- * Note: You should use [androidx.compose.animation.AnimatedContent] instead of
- * [SceneTransitionLayout] if it fits your need. Use [SceneTransitionLayout] over AnimatedContent if
- * you need support for swipe gestures, shared elements or transitions defined declaratively outside
- * UI code.
- *
- * @param currentScene the current scene
- * @param onChangeScene a mutator that should set [currentScene] to the given scene when called.
- * This is called when the user commits a transition to a new scene because of a [UserAction], for
- * instance by triggering back navigation or by swiping to a new scene.
- * @param transitions the definition of the transitions used to animate a change of scene.
- * @param swipeSourceDetector the source detector used to detect which source a swipe is started
- * from, if any.
- * @param transitionInterceptionThreshold used during a scene transition. For the scene to be
- * intercepted, the progress value must be above the threshold, and below (1 - threshold).
- * @param scenes the configuration of the different scenes of this layout.
- */
-@Composable
-fun SceneTransitionLayout(
- currentScene: SceneKey,
- onChangeScene: (SceneKey) -> Unit,
- transitions: SceneTransitions,
- modifier: Modifier = Modifier,
- swipeSourceDetector: SwipeSourceDetector = DefaultEdgeDetector,
- swipeDetector: SwipeDetector = DefaultSwipeDetector,
- @FloatRange(from = 0.0, to = 0.5) transitionInterceptionThreshold: Float = 0f,
- enableInterruptions: Boolean = DEFAULT_INTERRUPTIONS_ENABLED,
- scenes: SceneTransitionLayoutScope.() -> Unit,
-) {
- val state =
- updateSceneTransitionLayoutState(
- currentScene,
- onChangeScene,
- transitions,
- enableInterruptions = enableInterruptions,
- )
-
- SceneTransitionLayout(
- state,
- modifier,
- swipeSourceDetector,
- swipeDetector,
- transitionInterceptionThreshold,
- scenes,
- )
-}
-
interface SceneTransitionLayoutScope {
/**
* Add a scene to this layout, identified by [key].
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
index 5b4fbf0..56c8752 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
@@ -22,13 +22,9 @@
import androidx.compose.animation.core.AnimationVector1D
import androidx.compose.animation.core.spring
import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.util.fastAll
import androidx.compose.ui.util.fastFilter
@@ -38,14 +34,12 @@
import kotlin.math.absoluteValue
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
-import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
/**
* The state of a [SceneTransitionLayout].
*
* @see MutableSceneTransitionLayoutState
- * @see updateSceneTransitionLayoutState
*/
@Stable
sealed interface SceneTransitionLayoutState {
@@ -152,55 +146,6 @@
)
}
-/**
- * Sets up a [SceneTransitionLayoutState] and keeps it synced with [currentScene], [onChangeScene]
- * and [transitions]. New transitions will automatically be started whenever [currentScene] is
- * changed.
- *
- * @param currentScene the current scene
- * @param onChangeScene a mutator that should set [currentScene] to the given scene when called.
- * This is called when the user commits a transition to a new scene because of a [UserAction], for
- * instance by triggering back navigation or by swiping to a new scene.
- * @param transitions the definition of the transitions used to animate a change of scene.
- * @param canChangeScene whether we can transition to the given scene. This is called when the user
- * commits a transition to a new scene because of a [UserAction]. If [canChangeScene] returns
- * `true`, then [onChangeScene] will be called right afterwards with the same [SceneKey]. If it
- * returns `false`, the user action will be cancelled and we will animate back to the current
- * scene.
- * @param stateLinks the [StateLink] connecting this [SceneTransitionLayoutState] to other
- * [SceneTransitionLayoutState]s.
- */
-@Composable
-fun updateSceneTransitionLayoutState(
- currentScene: SceneKey,
- onChangeScene: (SceneKey) -> Unit,
- transitions: SceneTransitions = SceneTransitions.Empty,
- canChangeScene: (SceneKey) -> Boolean = { true },
- stateLinks: List<StateLink> = emptyList(),
- enableInterruptions: Boolean = DEFAULT_INTERRUPTIONS_ENABLED,
-): SceneTransitionLayoutState {
- return remember {
- HoistedSceneTransitionLayoutState(
- currentScene,
- transitions,
- onChangeScene,
- canChangeScene,
- stateLinks,
- enableInterruptions,
- )
- }
- .apply {
- update(
- currentScene,
- onChangeScene,
- canChangeScene,
- transitions,
- stateLinks,
- enableInterruptions,
- )
- }
-}
-
@Stable
sealed interface TransitionState {
/**
@@ -729,58 +674,6 @@
}
}
-/**
- * A [SceneTransitionLayout] whose current scene/source of truth is hoisted (its current value comes
- * from outside).
- */
-internal class HoistedSceneTransitionLayoutState(
- initialScene: SceneKey,
- override var transitions: SceneTransitions,
- private var changeScene: (SceneKey) -> Unit,
- private var canChangeScene: (SceneKey) -> Boolean,
- stateLinks: List<StateLink> = emptyList(),
- enableInterruptions: Boolean = DEFAULT_INTERRUPTIONS_ENABLED,
-) : BaseSceneTransitionLayoutState(initialScene, stateLinks, enableInterruptions) {
- private val targetSceneChannel = Channel<SceneKey>(Channel.CONFLATED)
-
- override fun canChangeScene(scene: SceneKey): Boolean = canChangeScene.invoke(scene)
-
- override fun CoroutineScope.onChangeScene(scene: SceneKey) = changeScene.invoke(scene)
-
- @Composable
- fun update(
- currentScene: SceneKey,
- onChangeScene: (SceneKey) -> Unit,
- canChangeScene: (SceneKey) -> Boolean,
- transitions: SceneTransitions,
- stateLinks: List<StateLink>,
- enableInterruptions: Boolean,
- ) {
- SideEffect {
- this.changeScene = onChangeScene
- this.canChangeScene = canChangeScene
- this.transitions = transitions
- this.stateLinks = stateLinks
- this.enableInterruptions = enableInterruptions
-
- targetSceneChannel.trySend(currentScene)
- }
-
- LaunchedEffect(targetSceneChannel) {
- for (newKey in targetSceneChannel) {
- // Inspired by AnimateAsState.kt: let's poll the last value to avoid being one frame
- // late.
- val newKey = targetSceneChannel.tryReceive().getOrNull() ?: newKey
- animateToScene(
- layoutState = this@HoistedSceneTransitionLayoutState,
- target = newKey,
- transitionKey = null,
- )
- }
- }
- }
-}
-
/** A [MutableSceneTransitionLayoutState] that holds the value for the current scene. */
internal class MutableSceneTransitionLayoutStateImpl(
initialScene: SceneKey,
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
index 1ae9992..7988e0e 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
@@ -203,26 +203,28 @@
val elementSize = 50.dp
val elementOffset = 20.dp
- lateinit var changeScene: (SceneKey) -> Unit
-
- rule.testTransition(
- from = SceneA,
- to = SceneB,
- transitionLayout = { currentScene, onChangeScene ->
- changeScene = onChangeScene
-
- SceneTransitionLayout(
- currentScene,
- onChangeScene,
+ val state =
+ rule.runOnUiThread {
+ MutableSceneTransitionLayoutState(
+ SceneA,
transitions {
from(SceneA, to = SceneB) { spec = tween }
from(SceneB, to = SceneC) { spec = tween }
},
- // Disable interruptions so that the current transition is directly removed when
- // starting a new one.
+ // Disable interruptions so that the current transition is directly removed
+ // when starting a new one.
enableInterruptions = false,
- ) {
+ )
+ }
+
+ lateinit var coroutineScope: CoroutineScope
+ rule.testTransition(
+ state = state,
+ to = SceneB,
+ transitionLayout = { state ->
+ coroutineScope = rememberCoroutineScope()
+ SceneTransitionLayout(state) {
scene(SceneA) {
Box(Modifier.size(layoutSize)) {
// Transformed element
@@ -243,7 +245,7 @@
onElement(TestElements.Bar).assertExists()
// Start transition from SceneB to SceneC
- changeScene(SceneC)
+ rule.runOnUiThread { state.setTargetScene(SceneC, coroutineScope) }
}
at(3 * frameDuration) { onElement(TestElements.Bar).assertIsNotDisplayed() }
@@ -340,18 +342,16 @@
@Test
fun elementIsReusedBetweenScenes() {
- var currentScene by mutableStateOf(SceneA)
+ val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) }
var sceneCState by mutableStateOf(0)
val key = TestElements.Foo
var nullableLayoutImpl: SceneTransitionLayoutImpl? = null
+ lateinit var coroutineScope: CoroutineScope
rule.setContent {
+ coroutineScope = rememberCoroutineScope()
SceneTransitionLayoutForTesting(
- state =
- updateSceneTransitionLayoutState(
- currentScene = currentScene,
- onChangeScene = { currentScene = it }
- ),
+ state = state,
onLayoutImpl = { nullableLayoutImpl = it },
) {
scene(SceneA) { /* Nothing */ }
@@ -375,7 +375,7 @@
assertThat(layoutImpl.elements).isEmpty()
// Scene B: element is in the map.
- currentScene = SceneB
+ rule.runOnUiThread { state.setTargetScene(SceneB, coroutineScope) }
rule.waitForIdle()
assertThat(layoutImpl.elements.keys).containsExactly(key)
@@ -383,7 +383,7 @@
assertThat(element.sceneStates.keys).containsExactly(SceneB)
// Scene C, state 0: the same element is reused.
- currentScene = SceneC
+ rule.runOnUiThread { state.setTargetScene(SceneC, coroutineScope) }
sceneCState = 0
rule.waitForIdle()
@@ -472,12 +472,13 @@
@Test
fun elementModifierSupportsUpdates() {
+ val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) }
var key by mutableStateOf(TestElements.Foo)
var nullableLayoutImpl: SceneTransitionLayoutImpl? = null
rule.setContent {
SceneTransitionLayoutForTesting(
- state = updateSceneTransitionLayoutState(currentScene = SceneA, onChangeScene = {}),
+ state = state,
onLayoutImpl = { nullableLayoutImpl = it },
) {
scene(SceneA) { Box(Modifier.element(key)) }
@@ -521,11 +522,12 @@
rule.waitUntil(timeoutMillis = 10_000) { animationFinished }
}
+ val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) }
rule.setContent {
scrollScope = rememberCoroutineScope()
SceneTransitionLayoutForTesting(
- state = updateSceneTransitionLayoutState(currentScene = SceneA, onChangeScene = {}),
+ state = state,
onLayoutImpl = { nullableLayoutImpl = it },
) {
scene(SceneA) {
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ObservableTransitionStateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ObservableTransitionStateTest.kt
index 5543135..f717301 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ObservableTransitionStateTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ObservableTransitionStateTest.kt
@@ -40,7 +40,13 @@
@Test
fun testObservableTransitionState() = runTest {
- lateinit var state: SceneTransitionLayoutState
+ val state =
+ rule.runOnUiThread {
+ MutableSceneTransitionLayoutState(
+ SceneA,
+ EmptyTestTransitions,
+ )
+ }
// Collect the current observable state into [observableState].
// TODO(b/290184746): Use collectValues {} once it is extracted into a library that can be
@@ -63,16 +69,9 @@
}
rule.testTransition(
- from = SceneA,
+ state = state,
to = SceneB,
- transitionLayout = { currentScene, onChangeScene ->
- state =
- updateSceneTransitionLayoutState(
- currentScene,
- onChangeScene,
- EmptyTestTransitions
- )
-
+ transitionLayout = {
SceneTransitionLayout(state = state) {
scene(SceneA) {}
scene(SceneB) {}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt
new file mode 100644
index 0000000..6522eb3
--- /dev/null
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.activity.BackEventCompat
+import androidx.activity.ComponentActivity
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compose.animation.scene.TestScenes.SceneA
+import com.android.compose.animation.scene.TestScenes.SceneB
+import com.android.compose.animation.scene.TestScenes.SceneC
+import com.android.compose.animation.scene.subjects.assertThat
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class PredictiveBackHandlerTest {
+ // We use createAndroidComposeRule() here and not createComposeRule() because we need an
+ // activity for testBack().
+ @get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
+
+ @Test
+ fun testBack() {
+ val layoutState = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) }
+ rule.setContent {
+ SceneTransitionLayout(layoutState) {
+ scene(SceneA, mapOf(Back to SceneB)) { Box(Modifier.fillMaxSize()) }
+ scene(SceneB) { Box(Modifier.fillMaxSize()) }
+ }
+ }
+
+ assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
+
+ rule.runOnUiThread { rule.activity.onBackPressedDispatcher.onBackPressed() }
+ rule.waitForIdle()
+ assertThat(layoutState.transitionState).hasCurrentScene(SceneB)
+ }
+
+ @Test
+ fun testPredictiveBack() {
+ val layoutState = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) }
+ rule.setContent {
+ SceneTransitionLayout(layoutState) {
+ scene(SceneA, mapOf(Back to SceneB)) { Box(Modifier.fillMaxSize()) }
+ scene(SceneB) { Box(Modifier.fillMaxSize()) }
+ }
+ }
+
+ assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
+
+ // Start back.
+ val dispatcher = rule.activity.onBackPressedDispatcher
+ rule.runOnUiThread {
+ dispatcher.dispatchOnBackStarted(backEvent())
+ dispatcher.dispatchOnBackProgressed(backEvent(progress = 0.4f))
+ }
+
+ val transition = assertThat(layoutState.transitionState).isTransition()
+ assertThat(transition).hasFromScene(SceneA)
+ assertThat(transition).hasToScene(SceneB)
+ assertThat(transition).hasProgress(0.4f)
+
+ // Cancel it.
+ rule.runOnUiThread { dispatcher.dispatchOnBackCancelled() }
+ rule.waitForIdle()
+ assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
+ assertThat(layoutState.transitionState).isIdle()
+
+ // Start again and commit it.
+ rule.runOnUiThread {
+ dispatcher.dispatchOnBackStarted(backEvent())
+ dispatcher.dispatchOnBackProgressed(backEvent(progress = 0.4f))
+ dispatcher.onBackPressed()
+ }
+ rule.waitForIdle()
+ assertThat(layoutState.transitionState).hasCurrentScene(SceneB)
+ assertThat(layoutState.transitionState).isIdle()
+ }
+
+ @Test
+ fun interruptedPredictiveBackDoesNotCallCanChangeScene() {
+ var canChangeSceneCalled = false
+ val layoutState =
+ rule.runOnUiThread {
+ MutableSceneTransitionLayoutState(
+ SceneA,
+ canChangeScene = {
+ canChangeSceneCalled = true
+ true
+ },
+ )
+ }
+
+ lateinit var coroutineScope: CoroutineScope
+ rule.setContent {
+ coroutineScope = rememberCoroutineScope()
+ SceneTransitionLayout(layoutState) {
+ scene(SceneA, mapOf(Back to SceneB)) { Box(Modifier.fillMaxSize()) }
+ scene(SceneB) { Box(Modifier.fillMaxSize()) }
+ scene(SceneC) { Box(Modifier.fillMaxSize()) }
+ }
+ }
+
+ assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
+
+ // Start back.
+ val dispatcher = rule.activity.onBackPressedDispatcher
+ rule.runOnUiThread { dispatcher.dispatchOnBackStarted(backEvent()) }
+
+ val predictiveTransition = assertThat(layoutState.transitionState).isTransition()
+ assertThat(predictiveTransition).hasFromScene(SceneA)
+ assertThat(predictiveTransition).hasToScene(SceneB)
+
+ // Start a new transition to C.
+ rule.runOnUiThread { layoutState.setTargetScene(SceneC, coroutineScope) }
+ val newTransition = assertThat(layoutState.transitionState).isTransition()
+ assertThat(newTransition).hasFromScene(SceneA)
+ assertThat(newTransition).hasToScene(SceneC)
+
+ // Commit the back gesture. It shouldn't call canChangeScene given that the back transition
+ // was interrupted.
+ rule.runOnUiThread { dispatcher.onBackPressed() }
+ rule.waitForIdle()
+ assertThat(layoutState.transitionState).hasCurrentScene(SceneC)
+ assertThat(layoutState.transitionState).isIdle()
+ assertThat(predictiveTransition).hasCurrentScene(SceneA)
+ assertThat(canChangeSceneCalled).isFalse()
+ }
+
+ private fun backEvent(progress: Float = 0f): BackEventCompat {
+ return BackEventCompat(
+ touchX = 0f,
+ touchY = 0f,
+ progress = progress,
+ swipeEdge = BackEventCompat.EDGE_LEFT,
+ )
+ }
+}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
index 1c8efb8..1ec1079 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
@@ -16,8 +16,6 @@
package com.android.compose.animation.scene
-import androidx.activity.BackEventCompat
-import androidx.activity.ComponentActivity
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
@@ -31,6 +29,8 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -41,7 +41,7 @@
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertPositionInRootIsEqualTo
import androidx.compose.ui.test.assertWidthIsEqualTo
-import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onChild
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
@@ -58,6 +58,7 @@
import com.android.compose.test.subjects.DpOffsetSubject
import com.android.compose.test.subjects.assertThat
import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
import org.junit.Assert.assertThrows
import org.junit.Rule
import org.junit.Test
@@ -69,23 +70,27 @@
private val LayoutSize = 300.dp
}
- private var currentScene by mutableStateOf(SceneA)
- private lateinit var layoutState: SceneTransitionLayoutState
+ private lateinit var coroutineScope: CoroutineScope
+ private lateinit var layoutState: MutableSceneTransitionLayoutState
+ private var currentScene: SceneKey
+ get() = layoutState.transitionState.currentScene
+ set(value) {
+ rule.runOnUiThread { layoutState.setTargetScene(value, coroutineScope) }
+ }
- // We use createAndroidComposeRule() here and not createComposeRule() because we need an
- // activity for testBack().
- @get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
+ @get:Rule val rule = createComposeRule()
/** The content under test. */
@Composable
private fun TestContent(enableInterruptions: Boolean = true) {
- layoutState =
- updateSceneTransitionLayoutState(
- currentScene,
- { currentScene = it },
+ coroutineScope = rememberCoroutineScope()
+ layoutState = remember {
+ MutableSceneTransitionLayoutState(
+ SceneA,
EmptyTestTransitions,
enableInterruptions = enableInterruptions,
)
+ }
SceneTransitionLayout(
state = layoutState,
@@ -164,52 +169,6 @@
}
@Test
- fun testBack() {
- rule.setContent { TestContent() }
-
- assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
-
- rule.runOnUiThread { rule.activity.onBackPressedDispatcher.onBackPressed() }
- rule.waitForIdle()
- assertThat(layoutState.transitionState).hasCurrentScene(SceneB)
- }
-
- @Test
- fun testPredictiveBack() {
- rule.setContent { TestContent() }
-
- assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
-
- // Start back.
- val dispatcher = rule.activity.onBackPressedDispatcher
- rule.runOnUiThread {
- dispatcher.dispatchOnBackStarted(backEvent())
- dispatcher.dispatchOnBackProgressed(backEvent(progress = 0.4f))
- }
-
- val transition = assertThat(layoutState.transitionState).isTransition()
- assertThat(transition).hasFromScene(SceneA)
- assertThat(transition).hasToScene(SceneB)
- assertThat(transition).hasProgress(0.4f)
-
- // Cancel it.
- rule.runOnUiThread { dispatcher.dispatchOnBackCancelled() }
- rule.waitForIdle()
- assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
- assertThat(layoutState.transitionState).isIdle()
-
- // Start again and commit it.
- rule.runOnUiThread {
- dispatcher.dispatchOnBackStarted(backEvent())
- dispatcher.dispatchOnBackProgressed(backEvent(progress = 0.4f))
- dispatcher.onBackPressed()
- }
- rule.waitForIdle()
- assertThat(layoutState.transitionState).hasCurrentScene(SceneB)
- assertThat(layoutState.transitionState).isIdle()
- }
-
- @Test
fun testTransitionState() {
rule.setContent { TestContent() }
assertThat(layoutState.transitionState).isIdle()
@@ -218,23 +177,15 @@
// We will advance the clock manually.
rule.mainClock.autoAdvance = false
- // Change the current scene. Until composition is triggered, this won't change the layout
- // state.
+ // Change the current scene.
currentScene = SceneB
- assertThat(layoutState.transitionState).isIdle()
- assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
-
- // On the next frame, we will recompose because currentScene changed, which will start the
- // transition (i.e. it will change the transitionState to be a Transition) in a
- // LaunchedEffect.
- rule.mainClock.advanceTimeByFrame()
val transition = assertThat(layoutState.transitionState).isTransition()
assertThat(transition).hasFromScene(SceneA)
assertThat(transition).hasToScene(SceneB)
assertThat(transition).hasProgress(0f)
// Then, on the next frame, the animator we started gets its initial value and clock
- // starting time. We are now at progress = 0f.
+ // starting time. We are still at progress = 0f.
rule.mainClock.advanceTimeByFrame()
assertThat(transition).hasProgress(0f)
@@ -275,12 +226,9 @@
// Pause animations to test the state mid-transition.
rule.mainClock.autoAdvance = false
- // Go to scene B and let the animation start. See [testLayoutState()] and
- // [androidx.compose.ui.test.MainTestClock] to understand why we need to advance the clock
- // by 2 frames to be at the start of the animation.
+ // Go to scene B and let the animation start.
currentScene = SceneB
rule.mainClock.advanceTimeByFrame()
- rule.mainClock.advanceTimeByFrame()
// Advance to the middle of the animation.
rule.mainClock.advanceTimeBy(TestTransitionDuration / 2)
@@ -311,7 +259,6 @@
// Animate to scene C, let the animation start then go to the middle of the transition.
currentScene = SceneC
rule.mainClock.advanceTimeByFrame()
- rule.mainClock.advanceTimeByFrame()
rule.mainClock.advanceTimeBy(TestTransitionDuration / 2)
// In Scene C, foo is at the bottom start of the layout and has a size of 150.dp. The
@@ -409,24 +356,24 @@
fun multipleTransitionsWillComposeMultipleScenes() {
val duration = 10 * 16L
- var currentScene: SceneKey by mutableStateOf(SceneA)
- lateinit var state: SceneTransitionLayoutState
- rule.setContent {
- state =
- updateSceneTransitionLayoutState(
- currentScene = currentScene,
- onChangeScene = { currentScene = it },
- transitions =
- transitions {
- from(SceneA, to = SceneB) {
- spec = tween(duration.toInt(), easing = LinearEasing)
- }
- from(SceneB, to = SceneC) {
- spec = tween(duration.toInt(), easing = LinearEasing)
- }
+ val state =
+ rule.runOnUiThread {
+ MutableSceneTransitionLayoutState(
+ SceneA,
+ transitions {
+ from(SceneA, to = SceneB) {
+ spec = tween(duration.toInt(), easing = LinearEasing)
}
+ from(SceneB, to = SceneC) {
+ spec = tween(duration.toInt(), easing = LinearEasing)
+ }
+ }
)
+ }
+ lateinit var coroutineScope: CoroutineScope
+ rule.setContent {
+ coroutineScope = rememberCoroutineScope()
SceneTransitionLayout(state) {
scene(SceneA) { Box(Modifier.testTag("aRoot").fillMaxSize()) }
scene(SceneB) { Box(Modifier.testTag("bRoot").fillMaxSize()) }
@@ -444,12 +391,11 @@
rule.mainClock.autoAdvance = false
// Start A => B and go to the middle of the transition.
- currentScene = SceneB
+ rule.runOnUiThread { state.setTargetScene(SceneB, coroutineScope) }
- // We need to tick 2 frames after changing [currentScene] before the animation actually
+ // We need to tick 1 frames after changing [currentScene] before the animation actually
// starts.
rule.mainClock.advanceTimeByFrame()
- rule.mainClock.advanceTimeByFrame()
rule.mainClock.advanceTimeBy(duration / 2)
rule.waitForIdle()
@@ -462,8 +408,7 @@
rule.onNodeWithTag("cRoot").assertDoesNotExist()
// Start B => C.
- currentScene = SceneC
- rule.mainClock.advanceTimeByFrame()
+ rule.runOnUiThread { state.setTargetScene(SceneC, coroutineScope) }
rule.mainClock.advanceTimeByFrame()
rule.waitForIdle()
@@ -517,12 +462,7 @@
assertThrows(IllegalStateException::class.java) {
rule.setContent {
SceneTransitionLayout(
- state =
- updateSceneTransitionLayoutState(
- currentScene = currentScene,
- onChangeScene = { currentScene = it },
- transitions = EmptyTestTransitions
- ),
+ state = remember { MutableSceneTransitionLayoutState(SceneA) },
modifier = Modifier.size(LayoutSize),
) {
// from SceneA to SceneA
@@ -560,13 +500,4 @@
assertThat(keyInB).isEqualTo(SceneB)
assertThat(keyInC).isEqualTo(SceneC)
}
-
- private fun backEvent(progress: Float = 0f): BackEventCompat {
- return BackEventCompat(
- touchX = 0f,
- touchY = 0f,
- progress = progress,
- swipeEdge = BackEventCompat.EDGE_LEFT,
- )
- }
}
diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestSceneScope.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestSceneScope.kt
index de46f72..fbd557f 100644
--- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestSceneScope.kt
+++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestSceneScope.kt
@@ -27,12 +27,6 @@
content: @Composable SceneScope.() -> Unit,
) {
val currentScene = remember { SceneKey("current") }
- SceneTransitionLayout(
- currentScene,
- onChangeScene = { /* do nothing */},
- transitions = remember { transitions {} },
- modifier,
- ) {
- scene(currentScene, content = content)
- }
+ val state = remember { MutableSceneTransitionLayoutState(currentScene) }
+ SceneTransitionLayout(state, modifier) { scene(currentScene, content = content) }
}
diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt
index 6724851..a37d78e 100644
--- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt
+++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt
@@ -19,13 +19,14 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.SemanticsNode
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import kotlinx.coroutines.CoroutineScope
import platform.test.motion.MotionTestRule
import platform.test.motion.RecordedMotion
import platform.test.motion.compose.ComposeRecordingSpec
@@ -95,20 +96,24 @@
builder: TransitionTestBuilder.() -> Unit,
) {
testTransition(
- from = fromScene,
+ state =
+ runOnUiThread {
+ MutableSceneTransitionLayoutState(
+ fromScene,
+ transitions { from(fromScene, to = toScene, builder = transition) }
+ )
+ },
to = toScene,
- transitionLayout = { currentScene, onChangeScene ->
+ transitionLayout = { state ->
SceneTransitionLayout(
- currentScene,
- onChangeScene,
- transitions { from(fromScene, to = toScene, builder = transition) },
+ state,
layoutModifier,
) {
scene(fromScene, content = fromSceneContent)
scene(toScene, content = toSceneContent)
}
},
- builder,
+ builder = builder,
)
}
@@ -172,21 +177,19 @@
)
}
-/**
- * Test the transition between two scenes of [transitionLayout][SceneTransitionLayout] at different
- * points in time.
- */
+/** Test the transition from [state] to [to]. */
fun ComposeContentTestRule.testTransition(
- from: SceneKey,
+ state: MutableSceneTransitionLayoutState,
to: SceneKey,
- transitionLayout:
- @Composable
- (
- currentScene: SceneKey,
- onChangeScene: (SceneKey) -> Unit,
- ) -> Unit,
+ transitionLayout: @Composable (state: MutableSceneTransitionLayoutState) -> Unit,
builder: TransitionTestBuilder.() -> Unit,
) {
+ val currentScene = state.transitionState.currentScene
+ check(currentScene != to) {
+ "The 'to' scene (${to.debugName}) should be different from the state current scene " +
+ "(${currentScene.debugName})"
+ }
+
val test = transitionTest(builder)
val assertionScope =
object : TransitionTestAssertionScope {
@@ -198,8 +201,11 @@
}
}
- var currentScene by mutableStateOf(from)
- setContent { transitionLayout(currentScene, { currentScene = it }) }
+ lateinit var coroutineScope: CoroutineScope
+ setContent {
+ coroutineScope = rememberCoroutineScope()
+ transitionLayout(state)
+ }
// Wait for the UI to be idle then test the before state.
waitForIdle()
@@ -209,14 +215,8 @@
mainClock.autoAdvance = false
// Change the current scene.
- currentScene = to
-
- // Advance by a frame to trigger recomposition, which will start the transition (i.e. it will
- // change the transitionState to be a Transition) in a LaunchedEffect.
- mainClock.advanceTimeByFrame()
-
- // Advance by another frame so that the animator we started gets its initial value and clock
- // starting time. We are now at progress = 0f.
+ runOnUiThread { state.setTargetScene(to, coroutineScope) }
+ waitForIdle()
mainClock.advanceTimeByFrame()
waitForIdle()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/SettingObserverTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/SettingObserverTest.kt
new file mode 100644
index 0000000..188f2ac
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/SettingObserverTest.kt
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.qs
+
+import android.net.Uri
+import android.os.Handler
+import android.os.Looper
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.Flags
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.settings.SettingsProxy
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.any
+import org.mockito.kotlin.capture
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.reset
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SettingObserverTest : SysuiTestCase() {
+
+ private val DEFAULT_VALUE = 7
+
+ @Mock lateinit var settingsProxy: SettingsProxy
+ @Captor private lateinit var argumentCaptor: ArgumentCaptor<Runnable>
+
+ private lateinit var testSettingObserver: SettingObserver
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ whenever(settingsProxy.getInt(any(), any())).thenReturn(5)
+ whenever(settingsProxy.getUriFor(any())).thenReturn(Uri.parse("content://test_uri"))
+ testSettingObserver =
+ object :
+ SettingObserver(
+ settingsProxy,
+ Handler(Looper.getMainLooper()),
+ "test_setting",
+ DEFAULT_VALUE
+ ) {
+ override fun handleValueChanged(value: Int, observedChange: Boolean) {}
+ }
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_QS_REGISTER_SETTING_OBSERVER_ON_BG_THREAD)
+ fun setListening_true_settingsProxyRegistered() {
+ testSettingObserver.isListening = true
+ verify(settingsProxy)
+ .registerContentObserverAsync(
+ any<Uri>(),
+ eq(false),
+ eq(testSettingObserver),
+ capture(argumentCaptor)
+ )
+ assertThat(testSettingObserver.value).isEqualTo(5)
+
+ // Verify if the callback applies updated value after the fact
+ whenever(settingsProxy.getInt(any(), any())).thenReturn(12341234)
+ argumentCaptor.value.run()
+ assertThat(testSettingObserver.value).isEqualTo(12341234)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_QS_REGISTER_SETTING_OBSERVER_ON_BG_THREAD)
+ fun setListening_false_settingsProxyRegistered() {
+ testSettingObserver.isListening = true
+ reset(settingsProxy)
+ testSettingObserver.isListening = false
+
+ verify(settingsProxy).unregisterContentObserverAsync(eq(testSettingObserver))
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_QS_REGISTER_SETTING_OBSERVER_ON_BG_THREAD)
+ fun setListening_bgFlagDisabled_true_settingsProxyRegistered() {
+ testSettingObserver.isListening = true
+ verify(settingsProxy)
+ .registerContentObserverSync(any<Uri>(), eq(false), eq(testSettingObserver))
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_QS_REGISTER_SETTING_OBSERVER_ON_BG_THREAD)
+ fun setListening_bgFlagDisabled_false_settingsProxyRegistered() {
+ testSettingObserver.isListening = true
+ reset(settingsProxy)
+ testSettingObserver.isListening = false
+
+ verify(settingsProxy).unregisterContentObserverSync(eq(testSettingObserver))
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/airplate/domain/interactor/AirplaneModeTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/airplane/domain/interactor/AirplaneModeTileDataInteractorTest.kt
similarity index 94%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/airplate/domain/interactor/AirplaneModeTileDataInteractorTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/airplane/domain/interactor/AirplaneModeTileDataInteractorTest.kt
index 89b9b7f..67e2fba 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/airplate/domain/interactor/AirplaneModeTileDataInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/airplane/domain/interactor/AirplaneModeTileDataInteractorTest.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.qs.tiles.impl.airplate.domain.interactor
+package com.android.systemui.qs.tiles.impl.airplane.domain.interactor
import android.os.UserHandle
import android.platform.test.annotations.EnabledOnRavenwood
@@ -23,7 +23,6 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectValues
import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
-import com.android.systemui.qs.tiles.impl.airplane.domain.interactor.AirplaneModeTileDataInteractor
import com.android.systemui.qs.tiles.impl.airplane.domain.model.AirplaneModeTileModel
import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
import com.google.common.truth.Truth.assertThat
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/airplate/domain/interactor/AirplaneModeTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/airplane/domain/interactor/AirplaneModeTileUserActionInteractorTest.kt
similarity index 94%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/airplate/domain/interactor/AirplaneModeTileUserActionInteractorTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/airplane/domain/interactor/AirplaneModeTileUserActionInteractorTest.kt
index 8982d81..79fcc92 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/airplate/domain/interactor/AirplaneModeTileUserActionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/airplane/domain/interactor/AirplaneModeTileUserActionInteractorTest.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.qs.tiles.impl.airplate.domain.interactor
+package com.android.systemui.qs.tiles.impl.airplane.domain.interactor
import android.platform.test.annotations.EnabledOnRavenwood
import android.provider.Settings
@@ -26,7 +26,6 @@
import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandlerSubject.Companion.assertThat
import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx.click
import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx.longClick
-import com.android.systemui.qs.tiles.impl.airplane.domain.interactor.AirplaneModeTileUserActionInteractor
import com.android.systemui.qs.tiles.impl.airplane.domain.model.AirplaneModeTileModel
import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
@@ -54,7 +53,7 @@
connectivityRepository,
mobileConnectionsRepository,
),
- inputHandler
+ inputHandler,
)
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileDataInteractorTest.kt
index 2e5fde8..a5f98a7 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileDataInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileDataInteractorTest.kt
@@ -30,7 +30,6 @@
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.toCollection
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Test
@@ -56,8 +55,7 @@
@Test
fun alwaysAvailable() =
testScope.runTest {
- val availability = underTest.availability(TEST_USER).toCollection(mutableListOf())
-
+ val availability by collectValues(underTest.availability(TEST_USER))
assertThat(availability).hasSize(1)
assertThat(availability.last()).isEqualTo(isAvailable)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileUserActionInteractorTest.kt
index 6ea5e63..3133312 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileUserActionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileUserActionInteractorTest.kt
@@ -17,9 +17,13 @@
package com.android.systemui.qs.tiles.impl.reducebrightness.domain.interactor
import android.platform.test.annotations.EnabledOnRavenwood
+import android.platform.test.annotations.RequiresFlagsDisabled
+import android.platform.test.annotations.RequiresFlagsEnabled
import android.provider.Settings
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.internal.R
+import com.android.server.display.feature.flags.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.accessibility.reduceBrightColorsController
import com.android.systemui.kosmos.Kosmos
@@ -43,11 +47,22 @@
private val underTest =
ReduceBrightColorsTileUserActionInteractor(
+ context.resources,
+ inputHandler,
+ controller,
+ )
+
+ private val underTestEvenDimmerEnabled =
+ ReduceBrightColorsTileUserActionInteractor(
+ context.orCreateTestableResources
+ .apply { addOverride(R.bool.config_evenDimmerEnabled, true) }
+ .resources,
inputHandler,
controller,
)
@Test
+ @RequiresFlagsDisabled(Flags.FLAG_EVEN_DIMMER)
fun handleClickWhenEnabled() = runTest {
val wasEnabled = true
controller.isReduceBrightColorsActivated = wasEnabled
@@ -58,6 +73,7 @@
}
@Test
+ @RequiresFlagsDisabled(Flags.FLAG_EVEN_DIMMER)
fun handleClickWhenDisabled() = runTest {
val wasEnabled = false
controller.isReduceBrightColorsActivated = wasEnabled
@@ -68,6 +84,7 @@
}
@Test
+ @RequiresFlagsDisabled(Flags.FLAG_EVEN_DIMMER)
fun handleLongClickWhenDisabled() = runTest {
val enabled = false
@@ -79,6 +96,7 @@
}
@Test
+ @RequiresFlagsDisabled(Flags.FLAG_EVEN_DIMMER)
fun handleLongClickWhenEnabled() = runTest {
val enabled = true
@@ -88,4 +106,58 @@
assertThat(it.intent.action).isEqualTo(Settings.ACTION_REDUCE_BRIGHT_COLORS_SETTINGS)
}
}
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_EVEN_DIMMER)
+ fun handleClickWhenEnabledEvenDimmer() = runTest {
+ val wasEnabled = true
+ controller.isReduceBrightColorsActivated = wasEnabled
+
+ underTestEvenDimmerEnabled.handleInput(
+ QSTileInputTestKtx.click(ReduceBrightColorsTileModel(wasEnabled))
+ )
+
+ assertThat(controller.isReduceBrightColorsActivated).isEqualTo(wasEnabled)
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_EVEN_DIMMER)
+ fun handleClickWhenDisabledEvenDimmer() = runTest {
+ val wasEnabled = false
+ controller.isReduceBrightColorsActivated = wasEnabled
+
+ underTestEvenDimmerEnabled.handleInput(
+ QSTileInputTestKtx.click(ReduceBrightColorsTileModel(wasEnabled))
+ )
+
+ assertThat(controller.isReduceBrightColorsActivated).isEqualTo(wasEnabled)
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_EVEN_DIMMER)
+ fun handleLongClickWhenDisabledEvenDimmer() = runTest {
+ val enabled = false
+
+ underTestEvenDimmerEnabled.handleInput(
+ QSTileInputTestKtx.longClick(ReduceBrightColorsTileModel(enabled))
+ )
+
+ QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput {
+ assertThat(it.intent.action).isEqualTo(Settings.ACTION_DISPLAY_SETTINGS)
+ }
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_EVEN_DIMMER)
+ fun handleLongClickWhenEnabledEvenDimmer() = runTest {
+ val enabled = true
+
+ underTestEvenDimmerEnabled.handleInput(
+ QSTileInputTestKtx.longClick(ReduceBrightColorsTileModel(enabled))
+ )
+
+ QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput {
+ assertThat(it.intent.action).isEqualTo(Settings.ACTION_DISPLAY_SETTINGS)
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
index 33f9209..80cf4c5 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
@@ -21,6 +21,7 @@
import android.view.LayoutInflater
import android.view.View
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.constraintlayout.widget.ConstraintSet
@@ -29,9 +30,9 @@
import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
import androidx.constraintlayout.widget.ConstraintSet.START
import androidx.constraintlayout.widget.ConstraintSet.TOP
+import com.android.compose.animation.scene.MutableSceneTransitionLayoutState
import com.android.compose.animation.scene.SceneKey
import com.android.compose.animation.scene.SceneTransitionLayout
-import com.android.compose.animation.scene.transitions
import com.android.internal.jank.InteractionJankMonitor
import com.android.keyguard.KeyguardStatusView
import com.android.keyguard.KeyguardStatusViewController
@@ -115,7 +116,6 @@
private var rootViewHandle: DisposableHandle? = null
private var indicationAreaHandle: DisposableHandle? = null
- private val sceneKey = SceneKey("root-view-scene-key")
var keyguardStatusViewController: KeyguardStatusViewController? = null
get() {
@@ -233,12 +233,10 @@
setContent {
// STL is used solely to provide a SceneScope to enable us to invoke SceneScope
// composables.
- SceneTransitionLayout(
- currentScene = sceneKey,
- onChangeScene = {},
- transitions = transitions {},
- ) {
- scene(sceneKey) {
+ val currentScene = remember { SceneKey("root-view-scene-key") }
+ val state = remember { MutableSceneTransitionLayoutState(currentScene) }
+ SceneTransitionLayout(state) {
+ scene(currentScene) {
with(
LockscreenContent(
viewModel = viewModel,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ReduceBrightColorsController.java b/packages/SystemUI/src/com/android/systemui/qs/ReduceBrightColorsController.java
index 10c8e53..cf1dca3 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ReduceBrightColorsController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/ReduceBrightColorsController.java
@@ -16,6 +16,8 @@
package com.android.systemui.qs;
+import android.content.res.Resources;
+
import com.android.systemui.statusbar.policy.CallbackController;
public interface ReduceBrightColorsController extends
@@ -27,6 +29,14 @@
/** Sets the activation state of Reduce Bright Colors */
void setReduceBrightColorsActivated(boolean activated);
+ /** Sets whether Reduce Bright Colors is enabled */
+ void setReduceBrightColorsFeatureAvailable(boolean enabled);
+
+ /** Gets whether Reduce Bright Colors is enabled */
+ boolean isReduceBrightColorsFeatureAvailable();
+
+ /** Gets whether Reduce Bright Colors is being transitioned to Even Dimmer */
+ boolean isInUpgradeMode(Resources resources);
/**
* Listener invoked whenever the Reduce Bright Colors settings are changed.
*/
@@ -38,5 +48,12 @@
*/
default void onActivated(boolean activated) {
}
+ /**
+ * Listener invoked when the feature enabled state changes.
+ *
+ * @param enabled {@code true} if Reduce Bright Colors feature is enabled.
+ */
+ default void onFeatureEnabledChanged(boolean enabled) {
+ }
}
}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ReduceBrightColorsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/qs/ReduceBrightColorsControllerImpl.java
index 846d63f..d68b22b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ReduceBrightColorsControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/ReduceBrightColorsControllerImpl.java
@@ -19,6 +19,7 @@
package com.android.systemui.qs;
import android.content.Context;
+import android.content.res.Resources;
import android.database.ContentObserver;
import android.hardware.display.ColorDisplayManager;
import android.net.Uri;
@@ -28,6 +29,7 @@
import androidx.annotation.NonNull;
+import com.android.server.display.feature.flags.Flags;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.settings.UserTracker;
@@ -47,6 +49,7 @@
private final ContentObserver mContentObserver;
private final SecureSettings mSecureSettings;
private final ArrayList<ReduceBrightColorsController.Listener> mListeners = new ArrayList<>();
+ private boolean mAvailable = true;
@Inject
public ReduceBrightColorsControllerImpl(UserTracker userTracker,
@@ -75,6 +78,7 @@
mCurrentUserTrackerCallback = new UserTracker.Callback() {
@Override
public void onUserChanged(int newUser, Context userContext) {
+ mAvailable = true;
synchronized (mListeners) {
if (mListeners.size() > 0) {
mSecureSettings.unregisterContentObserverSync(mContentObserver);
@@ -121,10 +125,35 @@
mManager.setReduceBrightColorsActivated(activated);
}
+ @Override
+ public void setReduceBrightColorsFeatureAvailable(boolean enabled) {
+ mAvailable = enabled;
+ dispatchOnEnabledChanged(enabled);
+ mAvailable = true;
+ }
+
+ @Override
+ public boolean isReduceBrightColorsFeatureAvailable() {
+ return mAvailable;
+ }
+
+ @Override
+ public boolean isInUpgradeMode(Resources resources) {
+ return Flags.evenDimmer() && resources.getBoolean(
+ com.android.internal.R.bool.config_evenDimmerEnabled);
+ }
+
private void dispatchOnActivated(boolean activated) {
ArrayList<Listener> copy = new ArrayList<>(mListeners);
for (Listener l : copy) {
l.onActivated(activated);
}
}
+
+ private void dispatchOnEnabledChanged(boolean enabled) {
+ ArrayList<Listener> copy = new ArrayList<>(mListeners);
+ for (Listener l : copy) {
+ l.onFeatureEnabledChanged(enabled);
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/SettingObserver.java b/packages/SystemUI/src/com/android/systemui/qs/SettingObserver.java
index 6092348..2287f4d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/SettingObserver.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/SettingObserver.java
@@ -19,6 +19,7 @@
import android.database.ContentObserver;
import android.os.Handler;
+import com.android.systemui.Flags;
import com.android.systemui.statusbar.policy.Listenable;
import com.android.systemui.util.settings.SecureSettings;
import com.android.systemui.util.settings.SettingsProxy;
@@ -74,10 +75,20 @@
mListening = listening;
if (listening) {
mObservedValue = getValueFromProvider();
- mSettingsProxy.registerContentObserverSync(
- mSettingsProxy.getUriFor(mSettingName), false, this);
+ if (Flags.qsRegisterSettingObserverOnBgThread()) {
+ mSettingsProxy.registerContentObserverAsync(
+ mSettingsProxy.getUriFor(mSettingName), false, this,
+ () -> mObservedValue = getValueFromProvider());
+ } else {
+ mSettingsProxy.registerContentObserverSync(
+ mSettingsProxy.getUriFor(mSettingName), false, this);
+ }
} else {
- mSettingsProxy.unregisterContentObserverSync(this);
+ if (Flags.qsRegisterSettingObserverOnBgThread()) {
+ mSettingsProxy.unregisterContentObserverAsync(this);
+ } else {
+ mSettingsProxy.unregisterContentObserverSync(this);
+ }
mObservedValue = mDefaultValue;
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java b/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java
index a45d6f6..89f85ab 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java
@@ -32,6 +32,7 @@
import androidx.annotation.Nullable;
+import com.android.server.display.feature.flags.Flags;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.plugins.qs.QSTile;
@@ -116,6 +117,10 @@
final ArrayList<QSTile> tilesToAdd = new ArrayList<>();
possibleTiles.remove("cell");
possibleTiles.remove("wifi");
+ if (Flags.evenDimmer() && mContext.getResources().getBoolean(
+ com.android.internal.R.bool.config_evenDimmerEnabled)) {
+ possibleTiles.remove("reduce_brightness");
+ }
for (String spec : possibleTiles) {
// Only add current and stock tiles that can be created from QSFactoryImpl.
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/StockTilesRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/StockTilesRepository.kt
index ec9d151..86a29f9 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/StockTilesRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/StockTilesRepository.kt
@@ -17,6 +17,7 @@
package com.android.systemui.qs.panels.data.repository
import android.content.res.Resources
+import com.android.server.display.feature.flags.Flags
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.qs.pipeline.shared.TileSpec
@@ -32,10 +33,15 @@
/**
* List of stock platform tiles. All of the specs will be of type [TileSpec.PlatformTileSpec].
*/
+ val shouldRemoveRbcTile: Boolean =
+ Flags.evenDimmer() &&
+ resources.getBoolean(com.android.internal.R.bool.config_evenDimmerEnabled)
+
val stockTiles =
resources
.getString(R.string.quick_settings_tiles_stock)
.split(",")
+ .filterNot { shouldRemoveRbcTile && it.equals("reduce_brightness") }
.map(TileSpec::create)
.filterNot { it is TileSpec.Invalid }
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/ReduceBrightColorsAutoAddable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/ReduceBrightColorsAutoAddable.kt
index 9c1b857..4d823ab 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/ReduceBrightColorsAutoAddable.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/ReduceBrightColorsAutoAddable.kt
@@ -38,10 +38,11 @@
@Inject
constructor(
controller: ReduceBrightColorsController,
- @Named(RBC_AVAILABLE) private val available: Boolean,
+ @Named(RBC_AVAILABLE) private var available: Boolean,
) :
CallbackControllerAutoAddable<
- ReduceBrightColorsController.Listener, ReduceBrightColorsController
+ ReduceBrightColorsController.Listener,
+ ReduceBrightColorsController
>(controller) {
override val spec: TileSpec
@@ -50,10 +51,16 @@
override fun ProducerScope<AutoAddSignal>.getCallback(): ReduceBrightColorsController.Listener {
return object : ReduceBrightColorsController.Listener {
override fun onActivated(activated: Boolean) {
- if (activated) {
+ if (activated && available) {
sendAdd()
}
}
+
+ override fun onFeatureEnabledChanged(enabled: Boolean) {
+ if (!enabled) {
+ available = false
+ }
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ReduceBrightColorsTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/ReduceBrightColorsTile.java
index 3472352..af5b311 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ReduceBrightColorsTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ReduceBrightColorsTile.java
@@ -48,12 +48,13 @@
/** Quick settings tile: Reduce Bright Colors **/
public class ReduceBrightColorsTile extends QSTileImpl<QSTile.BooleanState>
- implements ReduceBrightColorsController.Listener{
+ implements ReduceBrightColorsController.Listener {
public static final String TILE_SPEC = "reduce_brightness";
- private final boolean mIsAvailable;
+ private boolean mIsAvailable;
private final ReduceBrightColorsController mReduceBrightColorsController;
private boolean mIsListening;
+ private final boolean mInUpgradeMode;
@Inject
public ReduceBrightColorsTile(
@@ -73,9 +74,11 @@
statusBarStateController, activityStarter, qsLogger);
mReduceBrightColorsController = reduceBrightColorsController;
mReduceBrightColorsController.observe(getLifecycle(), this);
- mIsAvailable = isAvailable;
+ mInUpgradeMode = reduceBrightColorsController.isInUpgradeMode(mContext.getResources());
+ mIsAvailable = isAvailable || mInUpgradeMode;
}
+
@Override
public boolean isAvailable() {
return mIsAvailable;
@@ -93,12 +96,28 @@
@Override
public Intent getLongClickIntent() {
- return new Intent(Settings.ACTION_REDUCE_BRIGHT_COLORS_SETTINGS);
+ return goToEvenDimmer() ? new Intent(Settings.ACTION_DISPLAY_SETTINGS) : new Intent(
+ Settings.ACTION_REDUCE_BRIGHT_COLORS_SETTINGS);
+ }
+
+ private boolean goToEvenDimmer() {
+ if (mInUpgradeMode) {
+ mHost.removeTile(getTileSpec());
+ mIsAvailable = false;
+ return true;
+ }
+ return false;
}
@Override
protected void handleClick(@Nullable Expandable expandable) {
- mReduceBrightColorsController.setReduceBrightColorsActivated(!mState.value);
+
+ if (goToEvenDimmer()) {
+ mActivityStarter.postStartActivityDismissingKeyguard(
+ new Intent(Settings.ACTION_DISPLAY_SETTINGS), 0);
+ } else {
+ mReduceBrightColorsController.setReduceBrightColorsActivated(!mState.value);
+ }
}
@Override
@@ -127,4 +146,9 @@
public void onActivated(boolean activated) {
refreshState();
}
+
+ @Override
+ public void onFeatureEnabledChanged(boolean enabled) {
+ refreshState();
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileDataInteractor.kt
index 98fd561..00b1e41 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileDataInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileDataInteractor.kt
@@ -23,13 +23,13 @@
import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor
import com.android.systemui.qs.tiles.impl.reducebrightness.domain.model.ReduceBrightColorsTileModel
+import com.android.systemui.util.kotlin.isAvailable
import com.android.systemui.util.kotlin.isEnabled
import javax.inject.Inject
import javax.inject.Named
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
@@ -52,5 +52,7 @@
.map { ReduceBrightColorsTileModel(it) }
.flowOn(bgCoroutineContext)
}
- override fun availability(user: UserHandle): Flow<Boolean> = flowOf(isAvailable)
+
+ override fun availability(user: UserHandle): Flow<Boolean> =
+ reduceBrightColorsController.isAvailable()
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileUserActionInteractor.kt
index 14dbe0e..ed5e4fe 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileUserActionInteractor.kt
@@ -17,7 +17,9 @@
package com.android.systemui.qs.tiles.impl.reducebrightness.domain.interactor
import android.content.Intent
+import android.content.res.Resources
import android.provider.Settings
+import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.qs.ReduceBrightColorsController
import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler
import com.android.systemui.qs.tiles.base.interactor.QSTileInput
@@ -30,19 +32,40 @@
class ReduceBrightColorsTileUserActionInteractor
@Inject
constructor(
+ @Main private val resources: Resources,
private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler,
private val reduceBrightColorsController: ReduceBrightColorsController,
) : QSTileUserActionInteractor<ReduceBrightColorsTileModel> {
+ val isInUpgradeMode: Boolean = reduceBrightColorsController.isInUpgradeMode(resources)
+
override suspend fun handleInput(input: QSTileInput<ReduceBrightColorsTileModel>): Unit =
with(input) {
when (action) {
is QSTileUserAction.Click -> {
+ if (isInUpgradeMode) {
+ reduceBrightColorsController.setReduceBrightColorsFeatureAvailable(false)
+ qsTileIntentUserActionHandler.handle(
+ action.expandable,
+ Intent(Settings.ACTION_DISPLAY_SETTINGS)
+ )
+ // TODO(b/349458355): show dialog
+ return@with
+ }
reduceBrightColorsController.setReduceBrightColorsActivated(
!input.data.isEnabled
)
}
is QSTileUserAction.LongClick -> {
+ if (isInUpgradeMode) {
+ reduceBrightColorsController.setReduceBrightColorsFeatureAvailable(false)
+ qsTileIntentUserActionHandler.handle(
+ action.expandable,
+ Intent(Settings.ACTION_DISPLAY_SETTINGS)
+ )
+ // TODO(b/349458355): show dialog
+ return@with
+ }
qsTileIntentUserActionHandler.handle(
action.expandable,
Intent(Settings.ACTION_REDUCE_BRIGHT_COLORS_SETTINGS)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java
index 91b5d0b..a538856 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java
@@ -414,6 +414,14 @@
}
}
+ @Override
+ public void onFeatureEnabledChanged(boolean enabled) {
+ if (!enabled) {
+ mHost.removeTile(BRIGHTNESS);
+ mHandler.post(() -> mReduceBrightColorsController.removeCallback(this));
+ }
+ }
+
private void addReduceBrightColorsTile() {
if (mAutoTracker.isAdded(BRIGHTNESS)) return;
mHost.addTile(BRIGHTNESS);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
index a2d7281..a2e44df 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
@@ -31,7 +31,6 @@
import com.android.internal.logging.UiEventLogger;
import com.android.internal.policy.SystemBarUtils;
import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
@@ -172,10 +171,8 @@
updateResources();
}
});
- if (!NotificationsHeadsUpRefactor.isEnabled()) {
- javaAdapter.alwaysCollectFlow(shadeInteractor.isAnyExpanded(),
+ javaAdapter.alwaysCollectFlow(shadeInteractor.isAnyExpanded(),
this::onShadeOrQsExpanded);
- }
}
public void setAnimationStateHandler(AnimationStateHandler handler) {
@@ -270,10 +267,9 @@
}
private void onShadeOrQsExpanded(Boolean isExpanded) {
- NotificationsHeadsUpRefactor.assertInLegacyMode();
if (isExpanded != mIsExpanded) {
mIsExpanded = isExpanded;
- if (isExpanded) {
+ if (!NotificationsHeadsUpRefactor.isEnabled() && isExpanded) {
mHeadsUpAnimatingAway.setValue(false);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/ReduceBrightColorsControllerExt.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/ReduceBrightColorsControllerExt.kt
index ee00e8b..e6e2a07 100644
--- a/packages/SystemUI/src/com/android/systemui/util/kotlin/ReduceBrightColorsControllerExt.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/ReduceBrightColorsControllerExt.kt
@@ -35,3 +35,17 @@
}
.onStart { emit(isReduceBrightColorsActivated) }
}
+
+fun ReduceBrightColorsController.isAvailable(): Flow<Boolean> {
+ return conflatedCallbackFlow {
+ val callback =
+ object : ReduceBrightColorsController.Listener {
+ override fun onFeatureEnabledChanged(enabled: Boolean) {
+ trySend(enabled)
+ }
+ }
+ addCallback(callback)
+ awaitClose { removeCallback(callback) }
+ }
+ .onStart { emit(isReduceBrightColorsFeatureAvailable) }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.kt b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.kt
index fe54044..b5934ec 100644
--- a/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.kt
@@ -19,6 +19,7 @@
import android.database.ContentObserver
import android.net.Uri
import android.provider.Settings.SettingNotFoundException
+import androidx.annotation.AnyThread
import androidx.annotation.WorkerThread
import com.android.app.tracing.TraceUtils.trace
import kotlinx.coroutines.CoroutineDispatcher
@@ -57,7 +58,7 @@
* @param name to look up in the table
* @return the corresponding content URI, or null if not present
*/
- fun getUriFor(name: String): Uri
+ @AnyThread fun getUriFor(name: String): Uri
/**
* Registers listener for a given content observer <b>while blocking the current thread</b>.
@@ -89,12 +90,31 @@
*
* API corresponding to [registerContentObserver] for Java usage.
*/
+ @AnyThread
fun registerContentObserverAsync(name: String, settingsObserver: ContentObserver) =
CoroutineScope(backgroundDispatcher).launch {
registerContentObserverSync(getUriFor(name), settingsObserver)
}
/**
+ * Convenience wrapper around [ContentResolver.registerContentObserver].'
+ *
+ * API corresponding to [registerContentObserver] for Java usage. After registration is
+ * complete, the callback block is called on the <b>background thread</b> to allow for update of
+ * value.
+ */
+ @AnyThread
+ fun registerContentObserverAsync(
+ name: String,
+ settingsObserver: ContentObserver,
+ @WorkerThread registered: Runnable
+ ) =
+ CoroutineScope(backgroundDispatcher).launch {
+ registerContentObserverSync(getUriFor(name), settingsObserver)
+ registered.run()
+ }
+
+ /**
* Registers listener for a given content observer <b>while blocking the current thread</b>.
*
* This should not be called from the main thread, use [registerContentObserver] or
@@ -120,6 +140,7 @@
*
* API corresponding to [registerContentObserver] for Java usage.
*/
+ @AnyThread
fun registerContentObserverAsync(uri: Uri, settingsObserver: ContentObserver) =
CoroutineScope(backgroundDispatcher).launch {
registerContentObserverSync(uri, settingsObserver)
@@ -128,8 +149,27 @@
/**
* Convenience wrapper around [ContentResolver.registerContentObserver].'
*
+ * API corresponding to [registerContentObserver] for Java usage. After registration is
+ * complete, the callback block is called on the <b>background thread</b> to allow for update of
+ * value.
+ */
+ @AnyThread
+ fun registerContentObserverAsync(
+ uri: Uri,
+ settingsObserver: ContentObserver,
+ @WorkerThread registered: Runnable
+ ) =
+ CoroutineScope(backgroundDispatcher).launch {
+ registerContentObserverSync(uri, settingsObserver)
+ registered.run()
+ }
+
+ /**
+ * Convenience wrapper around [ContentResolver.registerContentObserver].'
+ *
* Implicitly calls [getUriFor] on the passed in name.
*/
+ @WorkerThread
fun registerContentObserverSync(
name: String,
notifyForDescendants: Boolean,
@@ -158,6 +198,7 @@
*
* API corresponding to [registerContentObserver] for Java usage.
*/
+ @AnyThread
fun registerContentObserverAsync(
name: String,
notifyForDescendants: Boolean,
@@ -168,6 +209,25 @@
}
/**
+ * Convenience wrapper around [ContentResolver.registerContentObserver].'
+ *
+ * API corresponding to [registerContentObserver] for Java usage. After registration is
+ * complete, the callback block is called on the <b>background thread</b> to allow for update of
+ * value.
+ */
+ @AnyThread
+ fun registerContentObserverAsync(
+ name: String,
+ notifyForDescendants: Boolean,
+ settingsObserver: ContentObserver,
+ @WorkerThread registered: Runnable
+ ) =
+ CoroutineScope(backgroundDispatcher).launch {
+ registerContentObserverSync(getUriFor(name), notifyForDescendants, settingsObserver)
+ registered.run()
+ }
+
+ /**
* Registers listener for a given content observer <b>while blocking the current thread</b>.
*
* This should not be called from the main thread, use [registerContentObserver] or
@@ -207,6 +267,7 @@
*
* API corresponding to [registerContentObserver] for Java usage.
*/
+ @AnyThread
fun registerContentObserverAsync(
uri: Uri,
notifyForDescendants: Boolean,
@@ -217,6 +278,25 @@
}
/**
+ * Convenience wrapper around [ContentResolver.registerContentObserver].'
+ *
+ * API corresponding to [registerContentObserver] for Java usage. After registration is
+ * complete, the callback block is called on the <b>background thread</b> to allow for update of
+ * value.
+ */
+ @AnyThread
+ fun registerContentObserverAsync(
+ uri: Uri,
+ notifyForDescendants: Boolean,
+ settingsObserver: ContentObserver,
+ @WorkerThread registered: Runnable
+ ) =
+ CoroutineScope(backgroundDispatcher).launch {
+ registerContentObserverSync(uri, notifyForDescendants, settingsObserver)
+ registered.run()
+ }
+
+ /**
* Unregisters the given content observer <b>while blocking the current thread</b>.
*
* This should not be called from the main thread, use [unregisterContentObserver] or
@@ -246,6 +326,7 @@
* API corresponding to [unregisterContentObserver] for Java usage to ensure that
* [ContentObserver] registration happens on a worker thread.
*/
+ @AnyThread
fun unregisterContentObserverAsync(settingsObserver: ContentObserver) =
CoroutineScope(backgroundDispatcher).launch { unregisterContentObserver(settingsObserver) }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ReduceBrightColorsTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ReduceBrightColorsTileTest.java
index 798e9fb..d6bde27 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ReduceBrightColorsTileTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ReduceBrightColorsTileTest.java
@@ -24,6 +24,7 @@
import static org.mockito.Mockito.when;
import android.os.Handler;
+import android.platform.test.annotations.RequiresFlagsDisabled;
import android.service.quicksettings.Tile;
import android.testing.TestableLooper;
@@ -32,6 +33,7 @@
import com.android.internal.R;
import com.android.internal.logging.MetricsLogger;
+import com.android.server.display.feature.flags.Flags;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.classifier.FalsingManagerFake;
import com.android.systemui.plugins.ActivityStarter;
@@ -131,6 +133,7 @@
}
@Test
+ @RequiresFlagsDisabled(Flags.FLAG_EVEN_DIMMER)
public void testActive_clicked_featureIsActivated() {
when(mReduceBrightColorsController.isReduceBrightColorsActivated()).thenReturn(false);
mTile.refreshState();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/settings/SettingsProxyTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/settings/SettingsProxyTest.kt
index dd791e7..5ac6110 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/settings/SettingsProxyTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/settings/SettingsProxyTest.kt
@@ -28,9 +28,10 @@
import com.android.systemui.SysuiTestCase
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.launch
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertThrows
import org.junit.Before
@@ -44,6 +45,7 @@
@RunWith(AndroidJUnit4::class)
@SmallTest
@TestableLooper.RunWithLooper
+@OptIn(ExperimentalCoroutinesApi::class)
class SettingsProxyTest : SysuiTestCase() {
private val testDispatcher = StandardTestDispatcher()
@@ -60,11 +62,12 @@
}
@Test
- fun registerContentObserver_inputString_success() {
- mSettings.registerContentObserverSync(TEST_SETTING, mContentObserver)
- verify(mSettings.getContentResolver())
- .registerContentObserver(eq(TEST_SETTING_URI), eq(false), eq(mContentObserver))
- }
+ fun registerContentObserver_inputString_success() =
+ testScope.runTest {
+ mSettings.registerContentObserverSync(TEST_SETTING, mContentObserver)
+ verify(mSettings.getContentResolver())
+ .registerContentObserver(eq(TEST_SETTING_URI), eq(false), eq(mContentObserver))
+ }
@Test
fun registerContentObserverSuspend_inputString_success() =
@@ -75,24 +78,25 @@
}
@Test
- fun registerContentObserverAsync_inputString_success() {
- mSettings.registerContentObserverAsync(TEST_SETTING, mContentObserver)
- testScope.launch {
+ fun registerContentObserverAsync_inputString_success() =
+ testScope.runTest {
+ mSettings.registerContentObserverAsync(TEST_SETTING, mContentObserver)
+ testScope.advanceUntilIdle()
verify(mSettings.getContentResolver())
.registerContentObserver(eq(TEST_SETTING_URI), eq(false), eq(mContentObserver))
}
- }
@Test
- fun registerContentObserver_inputString_notifyForDescendants_true() {
- mSettings.registerContentObserverSync(
- TEST_SETTING,
- notifyForDescendants = true,
- mContentObserver
- )
- verify(mSettings.getContentResolver())
- .registerContentObserver(eq(TEST_SETTING_URI), eq(true), eq(mContentObserver))
- }
+ fun registerContentObserver_inputString_notifyForDescendants_true() =
+ testScope.runTest {
+ mSettings.registerContentObserverSync(
+ TEST_SETTING,
+ notifyForDescendants = true,
+ mContentObserver
+ )
+ verify(mSettings.getContentResolver())
+ .registerContentObserver(eq(TEST_SETTING_URI), eq(true), eq(mContentObserver))
+ }
@Test
fun registerContentObserverSuspend_inputString_notifyForDescendants_true() =
@@ -107,24 +111,25 @@
}
@Test
- fun registerContentObserverAsync_inputString_notifyForDescendants_true() {
- mSettings.registerContentObserverAsync(
- TEST_SETTING,
- notifyForDescendants = true,
- mContentObserver
- )
- testScope.launch {
+ fun registerContentObserverAsync_inputString_notifyForDescendants_true() =
+ testScope.runTest {
+ mSettings.registerContentObserverAsync(
+ TEST_SETTING,
+ notifyForDescendants = true,
+ mContentObserver
+ )
+ testScope.advanceUntilIdle()
verify(mSettings.getContentResolver())
.registerContentObserver(eq(TEST_SETTING_URI), eq(true), eq(mContentObserver))
}
- }
@Test
- fun registerContentObserver_inputUri_success() {
- mSettings.registerContentObserverSync(TEST_SETTING_URI, mContentObserver)
- verify(mSettings.getContentResolver())
- .registerContentObserver(eq(TEST_SETTING_URI), eq(false), eq(mContentObserver))
- }
+ fun registerContentObserver_inputUri_success() =
+ testScope.runTest {
+ mSettings.registerContentObserverSync(TEST_SETTING_URI, mContentObserver)
+ verify(mSettings.getContentResolver())
+ .registerContentObserver(eq(TEST_SETTING_URI), eq(false), eq(mContentObserver))
+ }
@Test
fun registerContentObserverSuspend_inputUri_success() =
@@ -135,24 +140,25 @@
}
@Test
- fun registerContentObserverAsync_inputUri_success() {
- mSettings.registerContentObserverAsync(TEST_SETTING_URI, mContentObserver)
- testScope.launch {
+ fun registerContentObserverAsync_inputUri_success() =
+ testScope.runTest {
+ mSettings.registerContentObserverAsync(TEST_SETTING_URI, mContentObserver)
+ testScope.advanceUntilIdle()
verify(mSettings.getContentResolver())
.registerContentObserver(eq(TEST_SETTING_URI), eq(false), eq(mContentObserver))
}
- }
@Test
- fun registerContentObserver_inputUri_notifyForDescendants_true() {
- mSettings.registerContentObserverSync(
- TEST_SETTING_URI,
- notifyForDescendants = true,
- mContentObserver
- )
- verify(mSettings.getContentResolver())
- .registerContentObserver(eq(TEST_SETTING_URI), eq(true), eq(mContentObserver))
- }
+ fun registerContentObserver_inputUri_notifyForDescendants_true() =
+ testScope.runTest {
+ mSettings.registerContentObserverSync(
+ TEST_SETTING_URI,
+ notifyForDescendants = true,
+ mContentObserver
+ )
+ verify(mSettings.getContentResolver())
+ .registerContentObserver(eq(TEST_SETTING_URI), eq(true), eq(mContentObserver))
+ }
@Test
fun registerContentObserverSuspend_inputUri_notifyForDescendants_true() =
@@ -167,23 +173,56 @@
}
@Test
- fun registerContentObserverAsync_inputUri_notifyForDescendants_true() {
- mSettings.registerContentObserverAsync(
- TEST_SETTING_URI,
- notifyForDescendants = true,
- mContentObserver
- )
- testScope.launch {
+ fun registerContentObserverAsync_inputUri_notifyForDescendants_true() =
+ testScope.runTest {
+ mSettings.registerContentObserverAsync(
+ TEST_SETTING_URI,
+ notifyForDescendants = true,
+ mContentObserver
+ )
+ testScope.advanceUntilIdle()
verify(mSettings.getContentResolver())
.registerContentObserver(eq(TEST_SETTING_URI), eq(true), eq(mContentObserver))
}
+
+ @Test
+ fun registerContentObserverAsync_registeredLambdaPassed_callsCallback() =
+ testScope.runTest {
+ verifyRegisteredCallbackForRegistration {
+ mSettings.registerContentObserverAsync(TEST_SETTING, mContentObserver, it)
+ }
+ verifyRegisteredCallbackForRegistration {
+ mSettings.registerContentObserverAsync(TEST_SETTING_URI, mContentObserver, it)
+ }
+ verifyRegisteredCallbackForRegistration {
+ mSettings.registerContentObserverAsync(TEST_SETTING, false, mContentObserver, it)
+ }
+ verifyRegisteredCallbackForRegistration {
+ mSettings.registerContentObserverAsync(
+ TEST_SETTING_URI,
+ false,
+ mContentObserver,
+ it
+ )
+ }
+ }
+
+ private fun verifyRegisteredCallbackForRegistration(
+ call: (registeredRunnable: Runnable) -> Unit
+ ) {
+ var callbackCalled = false
+ val runnable = { callbackCalled = true }
+ call(runnable)
+ testScope.advanceUntilIdle()
+ assertThat(callbackCalled).isTrue()
}
@Test
- fun unregisterContentObserverSync() {
- mSettings.unregisterContentObserverSync(mContentObserver)
- verify(mSettings.getContentResolver()).unregisterContentObserver(eq(mContentObserver))
- }
+ fun unregisterContentObserverSync() =
+ testScope.runTest {
+ mSettings.unregisterContentObserverSync(mContentObserver)
+ verify(mSettings.getContentResolver()).unregisterContentObserver(eq(mContentObserver))
+ }
@Test
fun unregisterContentObserverSuspend_inputString_success() =
@@ -193,12 +232,12 @@
}
@Test
- fun unregisterContentObserverAsync_inputString_success() {
- mSettings.unregisterContentObserverAsync(mContentObserver)
- testScope.launch {
+ fun unregisterContentObserverAsync_inputString_success() =
+ testScope.runTest {
+ mSettings.unregisterContentObserverAsync(mContentObserver)
+ testScope.advanceUntilIdle()
verify(mSettings.getContentResolver()).unregisterContentObserver(eq(mContentObserver))
}
- }
@Test
fun getString_keyPresent_returnValidValue() {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/FakeReduceBrightColorsController.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/FakeReduceBrightColorsController.kt
index 8b0affe2..e02042d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/FakeReduceBrightColorsController.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/FakeReduceBrightColorsController.kt
@@ -16,6 +16,8 @@
package com.android.systemui.accessibility
+import android.content.res.Resources
+import com.android.server.display.feature.flags.Flags
import com.android.systemui.qs.ReduceBrightColorsController
class FakeReduceBrightColorsController : ReduceBrightColorsController {
@@ -44,4 +46,20 @@
}
}
}
+
+ override fun setReduceBrightColorsFeatureAvailable(enabled: Boolean) {
+ // do nothing
+ }
+
+ override fun isReduceBrightColorsFeatureAvailable(): Boolean {
+ return true
+ }
+
+ override fun isInUpgradeMode(resources: Resources?): Boolean {
+ if (resources != null) {
+ return Flags.evenDimmer() &&
+ resources.getBoolean(com.android.internal.R.bool.config_evenDimmerEnabled)
+ }
+ return false
+ }
}
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
index 9fc64a9..099cb28 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
@@ -26,7 +26,6 @@
import android.annotation.NonNull;
import android.content.Context;
import android.graphics.Region;
-import android.hardware.input.InputManager;
import android.os.Looper;
import android.os.PowerManager;
import android.os.SystemClock;
@@ -56,7 +55,6 @@
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
-import java.util.Objects;
import java.util.StringJoiner;
/**
@@ -748,8 +746,6 @@
if ((mEnabledFeatures & FLAG_FEATURE_MOUSE_KEYS) != 0) {
mMouseKeysInterceptor = new MouseKeysInterceptor(mAms,
- Objects.requireNonNull(mContext.getSystemService(
- InputManager.class)),
Looper.myLooper(),
Display.DEFAULT_DISPLAY);
addFirstEventHandler(Display.DEFAULT_DISPLAY, mMouseKeysInterceptor);
diff --git a/services/accessibility/java/com/android/server/accessibility/MouseKeysInterceptor.java b/services/accessibility/java/com/android/server/accessibility/MouseKeysInterceptor.java
index 3f0f23f..56da231 100644
--- a/services/accessibility/java/com/android/server/accessibility/MouseKeysInterceptor.java
+++ b/services/accessibility/java/com/android/server/accessibility/MouseKeysInterceptor.java
@@ -23,7 +23,6 @@
import android.annotation.RequiresPermission;
import android.companion.virtual.VirtualDeviceManager;
import android.companion.virtual.VirtualDeviceParams;
-import android.hardware.input.InputManager;
import android.hardware.input.VirtualMouse;
import android.hardware.input.VirtualMouseButtonEvent;
import android.hardware.input.VirtualMouseConfig;
@@ -60,8 +59,8 @@
* In case multiple physical keyboard are connected to a device,
* mouse keys of each physical keyboard will control a single (global) mouse pointer.
*/
-public class MouseKeysInterceptor extends BaseEventStreamTransformation implements Handler.Callback,
- InputManager.InputDeviceListener {
+public class MouseKeysInterceptor extends BaseEventStreamTransformation
+ implements Handler.Callback {
private static final String LOG_TAG = "MouseKeysInterceptor";
// To enable these logs, run: 'adb shell setprop log.tag.MouseKeysInterceptor DEBUG'
@@ -77,11 +76,8 @@
private static final int INTERVAL_MILLIS = 10;
private final AccessibilityManagerService mAms;
- private final InputManager mInputManager;
private final Handler mHandler;
- private final int mDisplayId;
-
VirtualDeviceManager.VirtualDevice mVirtualDevice = null;
private VirtualMouse mVirtualMouse = null;
@@ -100,23 +96,23 @@
/** Last time the key action was performed */
private long mLastTimeKeyActionPerformed = 0;
- // TODO (b/346706749): This is currently using the numpad key bindings for mouse keys.
- // Decide the final mouse key bindings with UX input.
+ /** Whether scroll toggle is on */
+ private boolean mScrollToggleOn = false;
+
public enum MouseKeyEvent {
- DIAGONAL_DOWN_LEFT_MOVE(KeyEvent.KEYCODE_NUMPAD_1),
- DOWN_MOVE(KeyEvent.KEYCODE_NUMPAD_2),
- DIAGONAL_DOWN_RIGHT_MOVE(KeyEvent.KEYCODE_NUMPAD_3),
- LEFT_MOVE(KeyEvent.KEYCODE_NUMPAD_4),
- RIGHT_MOVE(KeyEvent.KEYCODE_NUMPAD_6),
- DIAGONAL_UP_LEFT_MOVE(KeyEvent.KEYCODE_NUMPAD_7),
- UP_MOVE(KeyEvent.KEYCODE_NUMPAD_8),
- DIAGONAL_UP_RIGHT_MOVE(KeyEvent.KEYCODE_NUMPAD_9),
- LEFT_CLICK(KeyEvent.KEYCODE_NUMPAD_5),
- RIGHT_CLICK(KeyEvent.KEYCODE_NUMPAD_DOT),
- HOLD(KeyEvent.KEYCODE_NUMPAD_MULTIPLY),
- RELEASE(KeyEvent.KEYCODE_NUMPAD_SUBTRACT),
- SCROLL_UP(KeyEvent.KEYCODE_A),
- SCROLL_DOWN(KeyEvent.KEYCODE_S);
+ DIAGONAL_UP_LEFT_MOVE(KeyEvent.KEYCODE_7),
+ UP_MOVE_OR_SCROLL(KeyEvent.KEYCODE_8),
+ DIAGONAL_UP_RIGHT_MOVE(KeyEvent.KEYCODE_9),
+ LEFT_MOVE(KeyEvent.KEYCODE_U),
+ RIGHT_MOVE(KeyEvent.KEYCODE_O),
+ DIAGONAL_DOWN_LEFT_MOVE(KeyEvent.KEYCODE_J),
+ DOWN_MOVE_OR_SCROLL(KeyEvent.KEYCODE_K),
+ DIAGONAL_DOWN_RIGHT_MOVE(KeyEvent.KEYCODE_L),
+ LEFT_CLICK(KeyEvent.KEYCODE_I),
+ RIGHT_CLICK(KeyEvent.KEYCODE_SLASH),
+ HOLD(KeyEvent.KEYCODE_M),
+ RELEASE(KeyEvent.KEYCODE_COMMA),
+ SCROLL_TOGGLE(KeyEvent.KEYCODE_PERIOD);
private final int mKeyCode;
MouseKeyEvent(int enumValue) {
@@ -149,22 +145,19 @@
* Construct a new MouseKeysInterceptor.
*
* @param service The service to notify of key events
- * @param inputManager InputManager to track changes to connected input devices
* @param looper Looper to use for callbacks and messages
* @param displayId Display ID to send mouse events to
*/
@RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
- public MouseKeysInterceptor(AccessibilityManagerService service, InputManager inputManager,
- Looper looper, int displayId) {
+ public MouseKeysInterceptor(AccessibilityManagerService service, Looper looper, int displayId) {
mAms = service;
- mInputManager = inputManager;
mHandler = new Handler(looper, this);
- mInputManager.registerInputDeviceListener(this, mHandler);
- mDisplayId = displayId;
// Create the virtual mouse on a separate thread since virtual device creation
// should happen on an auxiliary thread, and not from the handler's thread.
+ // This is because virtual device creation is a blocking operation and can cause a
+ // deadlock if it is called from the handler's thread.
new Thread(() -> {
- mVirtualMouse = createVirtualMouse();
+ mVirtualMouse = createVirtualMouse(displayId);
}).start();
}
@@ -193,22 +186,23 @@
/**
* Performs a mouse scroll action based on the provided key code.
+ * The scroll action will only be performed if the scroll toggle is on.
* This method interprets the key code as a mouse scroll and sends
* the corresponding {@code VirtualMouseScrollEvent#mYAxisMovement}.
* @param keyCode The key code representing the mouse scroll action.
* Supported keys are:
* <ul>
- * <li>{@link MouseKeysInterceptor.MouseKeyEvent SCROLL_UP}
- * <li>{@link MouseKeysInterceptor.MouseKeyEvent SCROLL_DOWN}
+ * <li>{@link MouseKeysInterceptor.MouseKeyEvent#UP_MOVE_OR_SCROLL}
+ * <li>{@link MouseKeysInterceptor.MouseKeyEvent#DOWN_MOVE_OR_SCROLL}
* </ul>
*/
@RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
private void performMouseScrollAction(int keyCode) {
MouseKeyEvent mouseKeyEvent = MouseKeyEvent.from(keyCode);
float y = switch (mouseKeyEvent) {
- case SCROLL_UP -> 1.0f;
- case SCROLL_DOWN -> -1.0f;
+ case UP_MOVE_OR_SCROLL -> 1.0f;
+ case DOWN_MOVE_OR_SCROLL -> -1.0f;
default -> 0.0f;
};
if (mVirtualMouse != null) {
@@ -231,8 +225,8 @@
* @param keyCode The key code representing the mouse button action.
* Supported keys are:
* <ul>
- * <li>{@link MouseKeysInterceptor.MouseKeyEvent LEFT_CLICK} (Primary Button)
- * <li>{@link MouseKeysInterceptor.MouseKeyEvent RIGHT_CLICK} (Secondary
+ * <li>{@link MouseKeysInterceptor.MouseKeyEvent#LEFT_CLICK} (Primary Button)
+ * <li>{@link MouseKeysInterceptor.MouseKeyEvent#RIGHT_CLICK} (Secondary
* Button)
* </ul>
*/
@@ -264,17 +258,20 @@
* The method calculates the relative movement of the mouse pointer
* and sends the corresponding event to the virtual mouse.
*
+ * The UP and DOWN pointer actions will only take place for their respective keys
+ * if the scroll toggle is off.
+ *
* @param keyCode The key code representing the direction or button press.
* Supported keys are:
* <ul>
- * <li>{@link MouseKeysInterceptor.MouseKeyEvent DIAGONAL_DOWN_LEFT}
- * <li>{@link MouseKeysInterceptor.MouseKeyEvent DOWN}
- * <li>{@link MouseKeysInterceptor.MouseKeyEvent DIAGONAL_DOWN_RIGHT}
- * <li>{@link MouseKeysInterceptor.MouseKeyEvent LEFT}
- * <li>{@link MouseKeysInterceptor.MouseKeyEvent RIGHT}
- * <li>{@link MouseKeysInterceptor.MouseKeyEvent DIAGONAL_UP_LEFT}
- * <li>{@link MouseKeysInterceptor.MouseKeyEvent UP}
- * <li>{@link MouseKeysInterceptor.MouseKeyEvent DIAGONAL_UP_RIGHT}
+ * <li>{@link MouseKeysInterceptor.MouseKeyEvent#DIAGONAL_DOWN_LEFT_MOVE}
+ * <li>{@link MouseKeysInterceptor.MouseKeyEvent#DOWN_MOVE_OR_SCROLL}
+ * <li>{@link MouseKeysInterceptor.MouseKeyEvent#DIAGONAL_DOWN_RIGHT_MOVE}
+ * <li>{@link MouseKeysInterceptor.MouseKeyEvent#LEFT_MOVE}
+ * <li>{@link MouseKeysInterceptor.MouseKeyEvent#RIGHT_MOVE}
+ * <li>{@link MouseKeysInterceptor.MouseKeyEvent#DIAGONAL_UP_LEFT_MOVE}
+ * <li>{@link MouseKeysInterceptor.MouseKeyEvent#UP_MOVE_OR_SCROLL}
+ * <li>{@link MouseKeysInterceptor.MouseKeyEvent#DIAGONAL_UP_RIGHT_MOVE}
* </ul>
*/
@RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
@@ -287,8 +284,10 @@
x = -MOUSE_POINTER_MOVEMENT_STEP / sqrt(2);
y = MOUSE_POINTER_MOVEMENT_STEP / sqrt(2);
}
- case DOWN_MOVE -> {
- y = MOUSE_POINTER_MOVEMENT_STEP;
+ case DOWN_MOVE_OR_SCROLL -> {
+ if (!mScrollToggleOn) {
+ y = MOUSE_POINTER_MOVEMENT_STEP;
+ }
}
case DIAGONAL_DOWN_RIGHT_MOVE -> {
x = MOUSE_POINTER_MOVEMENT_STEP / sqrt(2);
@@ -304,8 +303,10 @@
x = -MOUSE_POINTER_MOVEMENT_STEP / sqrt(2);
y = -MOUSE_POINTER_MOVEMENT_STEP / sqrt(2);
}
- case UP_MOVE -> {
- y = -MOUSE_POINTER_MOVEMENT_STEP;
+ case UP_MOVE_OR_SCROLL -> {
+ if (!mScrollToggleOn) {
+ y = -MOUSE_POINTER_MOVEMENT_STEP;
+ }
}
case DIAGONAL_UP_RIGHT_MOVE -> {
x = MOUSE_POINTER_MOVEMENT_STEP / sqrt(2);
@@ -333,8 +334,8 @@
}
private boolean isMouseScrollKey(int keyCode) {
- return keyCode == MouseKeyEvent.SCROLL_UP.getKeyCodeValue()
- || keyCode == MouseKeyEvent.SCROLL_DOWN.getKeyCodeValue();
+ return keyCode == MouseKeyEvent.UP_MOVE_OR_SCROLL.getKeyCodeValue()
+ || keyCode == MouseKeyEvent.DOWN_MOVE_OR_SCROLL.getKeyCodeValue();
}
/**
@@ -343,7 +344,7 @@
* @return The created VirtualMouse.
*/
@RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
- private VirtualMouse createVirtualMouse() {
+ private VirtualMouse createVirtualMouse(int displayId) {
final VirtualDeviceManagerInternal localVdm =
LocalServices.getService(VirtualDeviceManagerInternal.class);
mVirtualDevice = localVdm.createVirtualDevice(
@@ -351,7 +352,7 @@
VirtualMouse virtualMouse = mVirtualDevice.createVirtualMouse(
new VirtualMouseConfig.Builder()
.setInputDeviceName("Mouse Keys Virtual Mouse")
- .setAssociatedDisplayId(mDisplayId)
+ .setAssociatedDisplayId(displayId)
.build());
return virtualMouse;
}
@@ -375,42 +376,56 @@
if (!isMouseKey(keyCode)) {
// Pass non-mouse key events to the next handler
super.onKeyEvent(event, policyFlags);
- } else if (keyCode == MouseKeyEvent.HOLD.getKeyCodeValue()) {
- sendVirtualMouseButtonEvent(VirtualMouseButtonEvent.BUTTON_PRIMARY,
- VirtualMouseButtonEvent.ACTION_BUTTON_PRESS);
- } else if (keyCode == MouseKeyEvent.RELEASE.getKeyCodeValue()) {
- sendVirtualMouseButtonEvent(VirtualMouseButtonEvent.BUTTON_PRIMARY,
- VirtualMouseButtonEvent.ACTION_BUTTON_RELEASE);
- } else if (isDown && isMouseButtonKey(keyCode)) {
- performMouseButtonAction(keyCode);
- } else if (isDown && isMouseScrollKey(keyCode)) {
- // If the scroll key is pressed down and no other key is active,
- // set it as the active key and send a message to scroll the pointer
- if (mActiveScrollKey == KEY_NOT_SET) {
- mActiveScrollKey = keyCode;
- mLastTimeKeyActionPerformed = event.getDownTime();
- mHandler.sendEmptyMessage(MESSAGE_SCROLL_MOUSE_POINTER);
- }
} else if (isDown) {
- // This is a directional key.
- // If the key is pressed down and no other key is active,
- // set it as the active key and send a message to move the pointer
- if (mActiveMoveKey == KEY_NOT_SET) {
- mActiveMoveKey = keyCode;
- mLastTimeKeyActionPerformed = event.getDownTime();
- mHandler.sendEmptyMessage(MESSAGE_MOVE_MOUSE_POINTER);
+ if (keyCode == MouseKeyEvent.SCROLL_TOGGLE.getKeyCodeValue()) {
+ mScrollToggleOn = !mScrollToggleOn;
+ if (DEBUG) {
+ Slog.d(LOG_TAG, "Scroll toggle " + (mScrollToggleOn ? "ON" : "OFF"));
+ }
+ } else if (keyCode == MouseKeyEvent.HOLD.getKeyCodeValue()) {
+ sendVirtualMouseButtonEvent(
+ VirtualMouseButtonEvent.BUTTON_PRIMARY,
+ VirtualMouseButtonEvent.ACTION_BUTTON_PRESS
+ );
+ } else if (keyCode == MouseKeyEvent.RELEASE.getKeyCodeValue()) {
+ sendVirtualMouseButtonEvent(
+ VirtualMouseButtonEvent.BUTTON_PRIMARY,
+ VirtualMouseButtonEvent.ACTION_BUTTON_RELEASE
+ );
+ } else if (isMouseButtonKey(keyCode)) {
+ performMouseButtonAction(keyCode);
+ } else if (mScrollToggleOn && isMouseScrollKey(keyCode)) {
+ // If the scroll key is pressed down and no other key is active,
+ // set it as the active key and send a message to scroll the pointer
+ if (mActiveScrollKey == KEY_NOT_SET) {
+ mActiveScrollKey = keyCode;
+ mLastTimeKeyActionPerformed = event.getDownTime();
+ mHandler.sendEmptyMessage(MESSAGE_SCROLL_MOUSE_POINTER);
+ }
+ } else {
+ // This is a directional key.
+ // If the key is pressed down and no other key is active,
+ // set it as the active key and send a message to move the pointer
+ if (mActiveMoveKey == KEY_NOT_SET) {
+ mActiveMoveKey = keyCode;
+ mLastTimeKeyActionPerformed = event.getDownTime();
+ mHandler.sendEmptyMessage(MESSAGE_MOVE_MOUSE_POINTER);
+ }
}
- } else if (mActiveMoveKey == keyCode) {
- // If the key is released, and it is the active key, stop moving the pointer
- mActiveMoveKey = KEY_NOT_SET;
- mHandler.removeMessages(MESSAGE_MOVE_MOUSE_POINTER);
- } else if (mActiveScrollKey == keyCode) {
- // If the key is released, and it is the active key, stop scrolling the pointer
- mActiveScrollKey = KEY_NOT_SET;
- mHandler.removeMessages(MESSAGE_SCROLL_MOUSE_POINTER);
} else {
- Slog.i(LOG_TAG, "Dropping event with key code: '" + keyCode
- + "', with no matching down event from deviceId = " + event.getDeviceId());
+ // Up event received
+ if (mActiveMoveKey == keyCode) {
+ // If the key is released, and it is the active key, stop moving the pointer
+ mActiveMoveKey = KEY_NOT_SET;
+ mHandler.removeMessages(MESSAGE_MOVE_MOUSE_POINTER);
+ } else if (mActiveScrollKey == keyCode) {
+ // If the key is released, and it is the active key, stop scrolling the pointer
+ mActiveScrollKey = KEY_NOT_SET;
+ mHandler.removeMessages(MESSAGE_SCROLL_MOUSE_POINTER);
+ } else {
+ Slog.i(LOG_TAG, "Dropping event with key code: '" + keyCode
+ + "', with no matching down event from deviceId = " + event.getDeviceId());
+ }
}
}
@@ -470,14 +485,6 @@
}
}
- @Override
- public void onInputDeviceAdded(int deviceId) {
- }
-
- @Override
- public void onInputDeviceRemoved(int deviceId) {
- }
-
@RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
@Override
public void onDestroy() {
@@ -485,14 +492,8 @@
mActiveMoveKey = KEY_NOT_SET;
mActiveScrollKey = KEY_NOT_SET;
mLastTimeKeyActionPerformed = 0;
+
mHandler.removeCallbacksAndMessages(null);
-
mVirtualDevice.close();
- mInputManager.unregisterInputDeviceListener(this);
}
-
- @Override
- public void onInputDeviceChanged(int deviceId) {
- }
-
}
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index b9a9d64f..2c233f8 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -1901,6 +1901,7 @@
Slog.i(LOG_TAG, "Quiet mode is already " + enableQuietMode);
return;
}
+ UserManager.invalidateQuietModeEnabledCache();
profile.flags ^= UserInfo.FLAG_QUIET_MODE;
profileUserData = getUserDataLU(profile.id);
}
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/MouseKeysInterceptorTest.kt b/services/tests/servicestests/src/com/android/server/accessibility/MouseKeysInterceptorTest.kt
index dc8d239..0def516 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/MouseKeysInterceptorTest.kt
+++ b/services/tests/servicestests/src/com/android/server/accessibility/MouseKeysInterceptorTest.kt
@@ -16,6 +16,8 @@
package com.android.server.accessibility
+import android.util.MathUtils.sqrt
+
import android.companion.virtual.VirtualDeviceManager
import android.companion.virtual.VirtualDeviceParams
import android.content.Context
@@ -59,6 +61,7 @@
companion object {
const val DISPLAY_ID = 1
const val DEVICE_ID = 123
+ const val MOUSE_POINTER_MOVEMENT_STEP = 1.8f
// This delay is required for key events to be sent and handled correctly.
// The handler only performs a move/scroll event if it receives the key event
// at INTERVAL_MILLIS (which happens in practice). Hence, we need this delay in the tests.
@@ -113,8 +116,7 @@
Mockito.`when`(iInputManager.inputDeviceIds).thenReturn(intArrayOf(DEVICE_ID))
Mockito.`when`(mockAms.traceManager).thenReturn(mockTraceManager)
- mouseKeysInterceptor = MouseKeysInterceptor(mockAms, mockInputManager,
- testLooper.looper, DISPLAY_ID)
+ mouseKeysInterceptor = MouseKeysInterceptor(mockAms, testLooper.looper, DISPLAY_ID)
// VirtualMouse is created on a separate thread.
// Wait for VirtualMouse to be created before running tests
TimeUnit.MILLISECONDS.sleep(20L)
@@ -145,7 +147,7 @@
fun whenMouseDirectionalKeyIsPressed_relativeEventIsSent() {
// There should be some delay between the downTime of the key event and calling onKeyEvent
val downTime = clock.now() - KEYBOARD_POST_EVENT_DELAY_MILLIS
- val keyCode = MouseKeysInterceptor.MouseKeyEvent.DOWN_MOVE.getKeyCodeValue()
+ val keyCode = MouseKeysInterceptor.MouseKeyEvent.DIAGONAL_DOWN_LEFT_MOVE.keyCodeValue
val downEvent = KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN,
keyCode, 0, 0, DEVICE_ID, 0)
@@ -153,14 +155,15 @@
testLooper.dispatchAll()
// Verify the sendRelativeEvent method is called once and capture the arguments
- verifyRelativeEvents(arrayOf<Float>(0f), arrayOf<Float>(1.8f))
+ verifyRelativeEvents(arrayOf(-MOUSE_POINTER_MOVEMENT_STEP / sqrt(2.0f)),
+ arrayOf(MOUSE_POINTER_MOVEMENT_STEP / sqrt(2.0f)))
}
@Test
fun whenClickKeyIsPressed_buttonEventIsSent() {
// There should be some delay between the downTime of the key event and calling onKeyEvent
val downTime = clock.now() - KEYBOARD_POST_EVENT_DELAY_MILLIS
- val keyCode = MouseKeysInterceptor.MouseKeyEvent.LEFT_CLICK.getKeyCodeValue()
+ val keyCode = MouseKeysInterceptor.MouseKeyEvent.LEFT_CLICK.keyCodeValue
val downEvent = KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN,
keyCode, 0, 0, DEVICE_ID, 0)
mouseKeysInterceptor.onKeyEvent(downEvent, 0)
@@ -179,7 +182,7 @@
@Test
fun whenHoldKeyIsPressed_buttonEventIsSent() {
val downTime = clock.now() - KEYBOARD_POST_EVENT_DELAY_MILLIS
- val keyCode = MouseKeysInterceptor.MouseKeyEvent.HOLD.getKeyCodeValue()
+ val keyCode = MouseKeysInterceptor.MouseKeyEvent.HOLD.keyCodeValue
val downEvent = KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN,
keyCode, 0, 0, DEVICE_ID, 0)
mouseKeysInterceptor.onKeyEvent(downEvent, 0)
@@ -195,7 +198,7 @@
@Test
fun whenReleaseKeyIsPressed_buttonEventIsSent() {
val downTime = clock.now() - KEYBOARD_POST_EVENT_DELAY_MILLIS
- val keyCode = MouseKeysInterceptor.MouseKeyEvent.RELEASE.getKeyCodeValue()
+ val keyCode = MouseKeysInterceptor.MouseKeyEvent.RELEASE.keyCodeValue
val downEvent = KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN,
keyCode, 0, 0, DEVICE_ID, 0)
mouseKeysInterceptor.onKeyEvent(downEvent, 0)
@@ -209,18 +212,38 @@
}
@Test
- fun whenScrollUpKeyIsPressed_scrollEventIsSent() {
+ fun whenScrollToggleOn_ScrollUpKeyIsPressed_scrollEventIsSent() {
// There should be some delay between the downTime of the key event and calling onKeyEvent
val downTime = clock.now() - KEYBOARD_POST_EVENT_DELAY_MILLIS
- val keyCode = MouseKeysInterceptor.MouseKeyEvent.SCROLL_UP.getKeyCodeValue()
+ val keyCodeScrollToggle = MouseKeysInterceptor.MouseKeyEvent.SCROLL_TOGGLE.keyCodeValue
+ val keyCodeScroll = MouseKeysInterceptor.MouseKeyEvent.UP_MOVE_OR_SCROLL.keyCodeValue
+
+ val scrollToggleDownEvent = KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN,
+ keyCodeScrollToggle, 0, 0, DEVICE_ID, 0)
+ val scrollDownEvent = KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN,
+ keyCodeScroll, 0, 0, DEVICE_ID, 0)
+
+ mouseKeysInterceptor.onKeyEvent(scrollToggleDownEvent, 0)
+ mouseKeysInterceptor.onKeyEvent(scrollDownEvent, 0)
+ testLooper.dispatchAll()
+
+ // Verify the sendScrollEvent method is called once and capture the arguments
+ verifyScrollEvents(arrayOf<Float>(0f), arrayOf<Float>(1.0f))
+ }
+
+ @Test
+ fun whenScrollToggleOff_DirectionalUpKeyIsPressed_RelativeEventIsSent() {
+ // There should be some delay between the downTime of the key event and calling onKeyEvent
+ val downTime = clock.now() - KEYBOARD_POST_EVENT_DELAY_MILLIS
+ val keyCode = MouseKeysInterceptor.MouseKeyEvent.UP_MOVE_OR_SCROLL.keyCodeValue
val downEvent = KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN,
keyCode, 0, 0, DEVICE_ID, 0)
mouseKeysInterceptor.onKeyEvent(downEvent, 0)
testLooper.dispatchAll()
- // Verify the sendScrollEvent method is called once and capture the arguments
- verifyScrollEvents(arrayOf<Float>(0f), arrayOf<Float>(1.0f))
+ // Verify the sendRelativeEvent method is called once and capture the arguments
+ verifyRelativeEvents(arrayOf<Float>(0f), arrayOf<Float>(-MOUSE_POINTER_MOVEMENT_STEP))
}
private fun verifyRelativeEvents(expectedX: Array<Float>, expectedY: Array<Float>) {
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
index 9003ab6..d714db99 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
@@ -1862,6 +1862,25 @@
assertThat(profilesExcludingHidden).asList().doesNotContain(profile.id);
}
+ /**
+ * Test that UserManager.isQuietModeEnabled return false for unsupported
+ * arguments such as UserHandle.NULL, UserHandle.CURRENT or UserHandle.ALL.
+ **/
+ @MediumTest
+ @Test
+ public void testQuietModeEnabledForUnsupportedUserHandles() throws Exception {
+ assumeManagedUsersSupported();
+ final int mainUserId = mUserManager.getMainUser().getIdentifier();
+ UserInfo userInfo = createProfileForUser("Profile",
+ UserManager.USER_TYPE_PROFILE_MANAGED, mainUserId);
+ mUserManager.requestQuietModeEnabled(true, userInfo.getUserHandle());
+ assertThat(mUserManager.isQuietModeEnabled(userInfo.getUserHandle())).isTrue();
+ assertThat(mUserManager.isQuietModeEnabled(UserHandle.of(UserHandle.USER_NULL))).isFalse();
+ assertThat(mUserManager.isQuietModeEnabled(UserHandle.CURRENT)).isFalse();
+ assertThat(mUserManager.isQuietModeEnabled(UserHandle.CURRENT_OR_SELF)).isFalse();
+ assertThat(mUserManager.isQuietModeEnabled(UserHandle.ALL)).isFalse();
+ }
+
private String generateLongString() {
String partialString = "Test Name Test Name Test Name Test Name Test Name Test Name Test "
+ "Name Test Name Test Name Test Name "; //String of length 100