diff options
56 files changed, 1134 insertions, 608 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index 56b6136bcaaf..f2c59dacd6d7 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -1058,7 +1058,7 @@ package android { field public static final int label = 16842753; // 0x1010001 field public static final int labelFor = 16843718; // 0x10103c6 field @Deprecated public static final int labelTextSize = 16843317; // 0x1010235 - field @FlaggedApi("android.view.inputmethod.ime_switcher_revamp") public static final int languageSettingsActivity; + field @FlaggedApi("android.view.inputmethod.ime_switcher_revamp_api") public static final int languageSettingsActivity; field public static final int languageTag = 16844040; // 0x1010508 field public static final int largeHeap = 16843610; // 0x101035a field public static final int largeScreens = 16843398; // 0x1010286 @@ -56333,7 +56333,7 @@ package android.view.inputmethod { public final class InputMethodInfo implements android.os.Parcelable { ctor public InputMethodInfo(android.content.Context, android.content.pm.ResolveInfo) throws java.io.IOException, org.xmlpull.v1.XmlPullParserException; ctor public InputMethodInfo(String, String, CharSequence, String); - method @FlaggedApi("android.view.inputmethod.ime_switcher_revamp") @Nullable public android.content.Intent createImeLanguageSettingsActivityIntent(); + method @FlaggedApi("android.view.inputmethod.ime_switcher_revamp_api") @Nullable public android.content.Intent createImeLanguageSettingsActivityIntent(); method @Nullable public android.content.Intent createStylusHandwritingSettingsActivityIntent(); method public int describeContents(); method public void dump(android.util.Printer, String); @@ -56354,7 +56354,7 @@ package android.view.inputmethod { method public boolean supportsStylusHandwriting(); method public boolean suppressesSpellChecker(); method public void writeToParcel(android.os.Parcel, int); - field @FlaggedApi("android.view.inputmethod.ime_switcher_revamp") public static final String ACTION_IME_LANGUAGE_SETTINGS = "android.view.inputmethod.action.IME_LANGUAGE_SETTINGS"; + field @FlaggedApi("android.view.inputmethod.ime_switcher_revamp_api") public static final String ACTION_IME_LANGUAGE_SETTINGS = "android.view.inputmethod.action.IME_LANGUAGE_SETTINGS"; field public static final String ACTION_STYLUS_HANDWRITING_SETTINGS = "android.view.inputmethod.action.STYLUS_HANDWRITING_SETTINGS"; field @NonNull public static final android.os.Parcelable.Creator<android.view.inputmethod.InputMethodInfo> CREATOR; } diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig index d05d23c336c2..55278f617ba9 100644 --- a/core/java/android/companion/virtual/flags/flags.aconfig +++ b/core/java/android/companion/virtual/flags/flags.aconfig @@ -32,6 +32,13 @@ flag { } 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 3be911abe732..8c98abd6dbfe 100644 --- a/core/java/android/hardware/input/VirtualRotaryEncoderScrollEvent.java +++ b/core/java/android/hardware/input/VirtualRotaryEncoderScrollEvent.java @@ -107,7 +107,8 @@ public final class VirtualRotaryEncoderScrollEvent implements Parcelable { } /** - * 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/view/inputmethod/InputMethodInfo.java b/core/java/android/view/inputmethod/InputMethodInfo.java index 11ee286652a1..098f65575928 100644 --- a/core/java/android/view/inputmethod/InputMethodInfo.java +++ b/core/java/android/view/inputmethod/InputMethodInfo.java @@ -92,7 +92,7 @@ public final class InputMethodInfo implements Parcelable { * * @see #createImeLanguageSettingsActivityIntent() */ - @FlaggedApi(android.view.inputmethod.Flags.FLAG_IME_SWITCHER_REVAMP) + @FlaggedApi(android.view.inputmethod.Flags.FLAG_IME_SWITCHER_REVAMP_API) public static final String ACTION_IME_LANGUAGE_SETTINGS = "android.view.inputmethod.action.IME_LANGUAGE_SETTINGS"; @@ -298,7 +298,7 @@ public final class InputMethodInfo implements Parcelable { com.android.internal.R.styleable.InputMethod); settingsActivityComponent = sa.getString( com.android.internal.R.styleable.InputMethod_settingsActivity); - if (Flags.imeSwitcherRevamp()) { + if (Flags.imeSwitcherRevampApi()) { languageSettingsActivityComponent = sa.getString( com.android.internal.R.styleable.InputMethod_languageSettingsActivity); } @@ -888,7 +888,7 @@ public final class InputMethodInfo implements Parcelable { * * @attr ref R.styleable#InputMethod_languageSettingsActivity */ - @FlaggedApi(android.view.inputmethod.Flags.FLAG_IME_SWITCHER_REVAMP) + @FlaggedApi(android.view.inputmethod.Flags.FLAG_IME_SWITCHER_REVAMP_API) @Nullable public Intent createImeLanguageSettingsActivityIntent() { if (TextUtils.isEmpty(mLanguageSettingsActivityName)) { diff --git a/core/java/android/view/inputmethod/flags.aconfig b/core/java/android/view/inputmethod/flags.aconfig index 56e5bcf79933..e294ee2d3a91 100644 --- a/core/java/android/view/inputmethod/flags.aconfig +++ b/core/java/android/view/inputmethod/flags.aconfig @@ -82,6 +82,15 @@ flag { } flag { + name: "ime_switcher_revamp_api" + is_exported: true + namespace: "input_method" + description: "Feature flag for APIs for revamping the Input Method Switcher menu" + bug: "311791923" + is_fixed_read_only: true +} + +flag { name: "initiation_without_input_connection" namespace: "input_method" description: "Feature flag for initiating handwriting without InputConnection" diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig index 92db37eea4ad..0322e4ec214d 100644 --- a/core/java/android/window/flags/windowing_frontend.aconfig +++ b/core/java/android/window/flags/windowing_frontend.aconfig @@ -206,3 +206,13 @@ flag { } } +flag { + name: "enforce_shell_thread_model" + namespace: "windowing_frentend" + description: "Crash the shell process if someone calls in from the wrong thread" + bug: "351189446" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index 4892f59bd9fb..0975eda3f9ff 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -4304,7 +4304,7 @@ <attr name="settingsActivity" format="string" /> <!-- Component name of an activity that allows the user to modify on-screen keyboards variants (e.g. different language or layout) for this service. --> - <!-- @FlaggedApi("android.view.inputmethod.ime_switcher_revamp") --> + <!-- @FlaggedApi("android.view.inputmethod.ime_switcher_revamp_api") --> <attr name="languageSettingsActivity" format="string"/> <!-- Set to true in all of the configurations for which this input method should be considered an option as the default. --> diff --git a/core/res/res/values/public-staging.xml b/core/res/res/values/public-staging.xml index b64334f7f95a..b74b41c666c8 100644 --- a/core/res/res/values/public-staging.xml +++ b/core/res/res/values/public-staging.xml @@ -114,7 +114,7 @@ <public name="optional"/> <!-- @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") --> <public name="adServiceTypes" /> - <!-- @FlaggedApi("android.view.inputmethod.ime_switcher_revamp") --> + <!-- @FlaggedApi("android.view.inputmethod.ime_switcher_revamp_api") --> <public name="languageSettingsActivity"/> <!-- @FlaggedApi("android.service.controls.flags.Flags.FLAG_HOME_PANEL_DREAM") --> <public name="dreamCategory"/> diff --git a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodInfoTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodInfoTest.java index 36ab0d4b0868..ce85a76f478d 100644 --- a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodInfoTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodInfoTest.java @@ -70,7 +70,7 @@ public class InputMethodInfoTest { assertThat(imi.supportsInlineSuggestionsWithTouchExploration(), is(false)); assertThat(imi.supportsStylusHandwriting(), is(false)); assertThat(imi.createStylusHandwritingSettingsActivityIntent(), equalTo(null)); - if (Flags.imeSwitcherRevamp()) { + if (Flags.imeSwitcherRevampApi()) { assertThat(imi.createImeLanguageSettingsActivityIntent(), equalTo(null)); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/HandlerExecutor.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/HandlerExecutor.java index bfee820870f1..736d954513b1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/HandlerExecutor.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/HandlerExecutor.java @@ -54,4 +54,11 @@ public class HandlerExecutor implements ShellExecutor { public boolean hasCallback(Runnable r) { return mHandler.hasCallbacks(r); } + + @Override + public void assertCurrentThread() { + if (!mHandler.getLooper().isCurrentThread()) { + throw new IllegalStateException("must be called on " + mHandler); + } + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ShellExecutor.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ShellExecutor.java index f729164ed303..2c2961fd4b65 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ShellExecutor.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ShellExecutor.java @@ -96,4 +96,11 @@ public interface ShellExecutor extends Executor { * See {@link android.os.Handler#hasCallbacks(Runnable)}. */ boolean hasCallback(Runnable runnable); + + /** + * May throw if the caller is not on the same thread as the executor. + */ + default void assertCurrentThread() { + return; + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java index 5b4208a10907..8d53bebba7bb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java @@ -38,6 +38,7 @@ import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; import static android.window.TransitionInfo.FLAG_NO_ANIMATION; import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT; +import static com.android.window.flags.Flags.enforceShellThreadModel; import static com.android.window.flags.Flags.ensureWallpaperInTransitions; import static com.android.systemui.shared.Flags.returnAnimationFrameworkLibrary; import static com.android.wm.shell.shared.TransitionUtil.isClosingType; @@ -924,9 +925,12 @@ public class Transitions implements RemoteCallable<Transitions>, } // An existing animation is playing, so see if we can merge. final ActiveTransition playing = track.mActiveTransition; + final IBinder playingToken = playing.mToken; + final IBinder readyToken = ready.mToken; + if (ready.mAborted) { // record as merged since it is no-op. Calls back into processReadyQueue - onMerged(playing, ready); + onMerged(playingToken, readyToken); return; } ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition %s ready while" @@ -934,14 +938,31 @@ public class Transitions implements RemoteCallable<Transitions>, + " in case they can be merged", ready, playing); mTransitionTracer.logMergeRequested(ready.mInfo.getDebugId(), playing.mInfo.getDebugId()); playing.mHandler.mergeAnimation(ready.mToken, ready.mInfo, ready.mStartT, - playing.mToken, (wct) -> onMerged(playing, ready)); + playing.mToken, (wct) -> onMerged(playingToken, readyToken)); } - private void onMerged(@NonNull ActiveTransition playing, @NonNull ActiveTransition merged) { + private void onMerged(@NonNull IBinder playingToken, @NonNull IBinder mergedToken) { + if (enforceShellThreadModel()) { + mMainExecutor.assertCurrentThread(); + } + + ActiveTransition playing = mKnownTransitions.get(playingToken); + if (playing == null) { + Log.e(TAG, "Merging into a non-existent transition: " + playingToken); + return; + } + + ActiveTransition merged = mKnownTransitions.get(mergedToken); + if (merged == null) { + Log.e(TAG, "Merging a non-existent transition: " + mergedToken); + return; + } + if (playing.getTrack() != merged.getTrack()) { throw new IllegalStateException("Can't merge across tracks: " + merged + " into " + playing); } + final Track track = mTracks.get(playing.getTrack()); ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition was merged: %s into %s", merged, playing); @@ -1071,13 +1092,17 @@ public class Transitions implements RemoteCallable<Transitions>, info.releaseAnimSurfaces(); } - private void onFinish(IBinder token, - @Nullable WindowContainerTransaction wct) { + private void onFinish(IBinder token, @Nullable WindowContainerTransaction wct) { + if (enforceShellThreadModel()) { + mMainExecutor.assertCurrentThread(); + } + final ActiveTransition active = mKnownTransitions.get(token); if (active == null) { Log.e(TAG, "Trying to finish a non-existent transition: " + token); return; } + final Track track = mTracks.get(active.getTrack()); if (track == null || track.mActiveTransition != active) { Log.e(TAG, "Trying to finish a non-running transition. Either remote crashed or " diff --git a/packages/SettingsLib/res/layout/zen_mode_duration_dialog.xml b/packages/SettingsLib/res/layout/zen_mode_duration_dialog.xml index 6552296bc4e4..bc5ec6911939 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 273a63db801c..72c3c1719f70 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 @@ class ZenModeRepositoryImpl( val backgroundHandler: Handler?, ) : ZenModeRepository { - private val notificationBroadcasts = + private val notificationBroadcasts by lazy { callbackFlow { val receiver = object : BroadcastReceiver() { @@ -95,8 +95,9 @@ class ZenModeRepositoryImpl( ) } } + } - 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 @@ class ZenModeRepositoryImpl( 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 096c25db8629..06333b61eeb1 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 @@ import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) @SmallTest class ZenModeRepositoryTest { - @Mock private lateinit var context: Context @Mock private lateinit var notificationManager: NotificationManager @@ -73,7 +72,7 @@ class ZenModeRepositoryTest { ) } - @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 @@ class ZenModeRepositoryTest { 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 @@ class ZenModeRepositoryTest { 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 @@ class ZenModeRepositoryTest { 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 e2ecda320065..63ce7eb5ae2b 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -1143,3 +1143,13 @@ flag { 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 b8f9ca82f072..f655ac1d207b 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 androidx.lifecycle.compose.collectAsStateWithLifecycle 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 @@ private fun FoldAware( 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 067315381773..0cd4b6816a61 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.foundation.layout.wrapContentSize 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.Density 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 @@ constructor( 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 dfb8c499b4c9..734241e2faf6 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 @@ internal fun PredictiveBackHandler( 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 @@ private class PredictiveBackTransition( 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 7c8fce8f297d..45758c53d69a 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 @@ import androidx.compose.ui.unit.IntSize * @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 @@ fun SceneTransitionLayout( ) } -/** - * [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 5b4fbf036083..56c8752eb53c 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.Animatable 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 com.android.compose.animation.scene.transition.link.StateLink 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 @@ fun MutableSceneTransitionLayoutState( ) } -/** - * 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 @@ internal abstract class BaseSceneTransitionLayoutState( } } -/** - * 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 1ae999282afa..7988e0e4e416 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 @@ class ElementTest { 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 @@ class ElementTest { 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 @@ class ElementTest { @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 @@ class ElementTest { 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 @@ class ElementTest { 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 @@ class ElementTest { @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 @@ class ElementTest { 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 55431354b693..f717301dba38 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 @@ class ObservableTransitionStateTest { @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 @@ class ObservableTransitionStateTest { } 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 000000000000..6522eb33bd49 --- /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 1c8efb82fd24..1ec10793363e 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.material3.Text 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.assertHeightIsEqualTo 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.assertSizeIsEqualTo 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 @@ class SceneTransitionLayoutTest { 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 @@ class SceneTransitionLayoutTest { } @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 @@ class SceneTransitionLayoutTest { // 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 @@ class SceneTransitionLayoutTest { // 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 @@ class SceneTransitionLayoutTest { // 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 @@ class SceneTransitionLayoutTest { 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 @@ class SceneTransitionLayoutTest { 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 @@ class SceneTransitionLayoutTest { 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 @@ class SceneTransitionLayoutTest { 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 @@ class SceneTransitionLayoutTest { 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 de46f7209c84..fbd557f3cbb3 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 @@ fun TestSceneScope( 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 6724851dbec5..a37d78ef8a71 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 @@ package com.android.compose.animation.scene 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 @@ fun ComposeContentTestRule.testTransition( 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 @@ fun MotionTestRule<ComposeToolkit>.recordTransition( ) } -/** - * 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 @@ fun ComposeContentTestRule.testTransition( } } - 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 @@ fun ComposeContentTestRule.testTransition( 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 000000000000..188f2aca5147 --- /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 index 89b9b7f30297..67e2fba30822 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 androidx.test.filters.SmallTest 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 index 8982d810ad8a..79fcc92a967c 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.FakeQSTileIntentUserInputHandl 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 @@ class AirplaneModeTileUserActionInteractorTest : SysuiTestCase() { 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 2e5fde8e4bd6..a5f98a739b49 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.android.systemui.qs.tiles.impl.reducebrightness.domain.model.ReduceBr 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 @@ class ReduceBrightColorsTileDataInteractorTest : SysuiTestCase() { @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 6ea5e63fdff6..313331286b45 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 @@ class ReduceBrightColorsTileUserActionInteractorTest : SysuiTestCase() { 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 @@ class ReduceBrightColorsTileUserActionInteractorTest : SysuiTestCase() { } @Test + @RequiresFlagsDisabled(Flags.FLAG_EVEN_DIMMER) fun handleClickWhenDisabled() = runTest { val wasEnabled = false controller.isReduceBrightColorsActivated = wasEnabled @@ -68,6 +84,7 @@ class ReduceBrightColorsTileUserActionInteractorTest : SysuiTestCase() { } @Test + @RequiresFlagsDisabled(Flags.FLAG_EVEN_DIMMER) fun handleLongClickWhenDisabled() = runTest { val enabled = false @@ -79,6 +96,7 @@ class ReduceBrightColorsTileUserActionInteractorTest : SysuiTestCase() { } @Test + @RequiresFlagsDisabled(Flags.FLAG_EVEN_DIMMER) fun handleLongClickWhenEnabled() = runTest { val enabled = true @@ -88,4 +106,58 @@ class ReduceBrightColorsTileUserActionInteractorTest : SysuiTestCase() { 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/res/layout/app_clips_screenshot.xml b/packages/SystemUI/res/layout/app_clips_screenshot.xml index b09d35d46ca0..5191895549b6 100644 --- a/packages/SystemUI/res/layout/app_clips_screenshot.xml +++ b/packages/SystemUI/res/layout/app_clips_screenshot.xml @@ -60,6 +60,7 @@ android:layout_marginStart="16dp" android:checked="true" android:text="@string/backlinks_include_link" + android:textColor="?android:textColorSecondary" android:visibility="gone" app:layout_constraintBottom_toTopOf="@id/preview" app:layout_constraintStart_toEndOf="@id/cancel" @@ -74,6 +75,7 @@ android:drawablePadding="4dp" android:gravity="center" android:paddingHorizontal="8dp" + android:textColor="?android:textColorSecondary" android:visibility="gone" app:layout_constraintBottom_toTopOf="@id/preview" app:layout_constraintStart_toEndOf="@id/backlinks_include_data" diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt index 33f9209fea09..80cf4c50866c 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.content.Context 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.END 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 @@ constructor( 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 @@ constructor( 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 10c8e530553a..cf1dca3cbb23 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 @@ public interface ReduceBrightColorsController extends /** 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 @@ public interface ReduceBrightColorsController extends */ 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 846d63f10875..d68b22b84f09 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 android.provider.Settings; 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 @@ public class ReduceBrightColorsControllerImpl implements 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 @@ public class ReduceBrightColorsControllerImpl implements 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 @@ public class ReduceBrightColorsControllerImpl implements 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 6092348b964e..2287f4d68933 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 @@ package com.android.systemui.qs; 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 @@ public abstract class SettingObserver extends ContentObserver implements Listena 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 a45d6f63cd81..89f85ab14dd6 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 android.widget.Button; 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 @@ public class TileQueryHelper { 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 ec9d151a26d3..86a29f91e51c 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 @@ constructor( /** * 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 9c1b85799648..4d823ab216f0 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 @@ class ReduceBrightColorsAutoAddable @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 @@ constructor( 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 34723523b84f..af5b31180159 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 @@ import javax.inject.Named; /** 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 @@ public class ReduceBrightColorsTile extends QSTileImpl<QSTile.BooleanState> 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 @@ public class ReduceBrightColorsTile extends QSTileImpl<QSTile.BooleanState> @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 class ReduceBrightColorsTile extends QSTileImpl<QSTile.BooleanState> 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 98fd561b27d6..00b1e41461f5 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.dagger.QSFlagsModule.RBC_AVAILABLE 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 @@ constructor( .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 14dbe0e8a69a..ed5e4fe74962 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 @@ import javax.inject.Inject 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/screenshot/appclips/AppClipsViewModel.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsViewModel.java index 8c833eccb1fb..bd9e295b58f8 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsViewModel.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsViewModel.java @@ -33,6 +33,7 @@ import android.content.ClipData; import android.content.ComponentName; import android.content.Intent; import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; import android.graphics.Bitmap; import android.graphics.HardwareRenderer; import android.graphics.RecordingCanvas; @@ -267,6 +268,8 @@ final class AppClipsViewModel extends ViewModel { } private boolean canAppStartThroughLauncher(String packageName) { + // Use Intent.resolveActivity API to check if the intent resolves as that is what Android + // uses internally when apps use Context.startActivity. return getMainLauncherIntentForPackage(packageName).resolveActivity(mPackageManager) != null; } @@ -366,10 +369,19 @@ final class AppClipsViewModel extends ViewModel { return taskInfo.topActivityInfo.loadLabel(mPackageManager).toString(); } - private Intent getMainLauncherIntentForPackage(String packageName) { - return new Intent(ACTION_MAIN) - .addCategory(CATEGORY_LAUNCHER) - .setPackage(packageName); + private Intent getMainLauncherIntentForPackage(String pkgName) { + Intent intent = new Intent(ACTION_MAIN).addCategory(CATEGORY_LAUNCHER).setPackage(pkgName); + + // Not all apps use DEFAULT_CATEGORY for their main launcher activity so the exact component + // needs to be queried and set on the Intent in order for note-taking apps to be able to + // start this intent. When starting an activity with an implicit intent, Android adds the + // DEFAULT_CATEGORY flag otherwise it fails to resolve the intent. + ResolveInfo resolvedActivity = mPackageManager.resolveActivity(intent, /* flags= */ 0); + if (resolvedActivity != null) { + intent.setComponent(resolvedActivity.getComponentInfo().getComponentName()); + } + + return intent; } /** Helper factory to help with injecting {@link AppClipsViewModel}. */ 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 91b5d0be6f04..a5388564d5fd 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 @@ public class AutoTileManager implements UserAwareController { } } + @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/util/kotlin/ReduceBrightColorsControllerExt.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/ReduceBrightColorsControllerExt.kt index ee00e8b04ef1..e6e2a0767012 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 @@ fun ReduceBrightColorsController.isEnabled(): Flow<Boolean> { } .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 fe540445793e..b5934ec680d3 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.content.ContentResolver 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 @@ interface SettingsProxy { * @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 @@ interface SettingsProxy { * * 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 @@ interface SettingsProxy { * * API corresponding to [registerContentObserver] for Java usage. */ + @AnyThread fun registerContentObserverAsync(uri: Uri, settingsObserver: ContentObserver) = CoroutineScope(backgroundDispatcher).launch { registerContentObserverSync(uri, settingsObserver) @@ -128,8 +149,27 @@ interface SettingsProxy { /** * 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 @@ interface SettingsProxy { * * API corresponding to [registerContentObserver] for Java usage. */ + @AnyThread fun registerContentObserverAsync( name: String, notifyForDescendants: Boolean, @@ -168,6 +209,25 @@ interface SettingsProxy { } /** + * 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 @@ interface SettingsProxy { * * API corresponding to [registerContentObserver] for Java usage. */ + @AnyThread fun registerContentObserverAsync( uri: Uri, notifyForDescendants: Boolean, @@ -217,6 +278,25 @@ interface SettingsProxy { } /** + * 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 @@ interface SettingsProxy { * 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 798e9fb208b7..d6bde27dfb62 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.verify; 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 androidx.test.filters.SmallTest; 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 @@ public class ReduceBrightColorsTileTest extends SysuiTestCase { } @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/screenshot/appclips/AppClipsViewModelTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsViewModelTest.java index baf1357a1ae0..193d29c1d550 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsViewModelTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsViewModelTest.java @@ -334,13 +334,17 @@ public final class AppClipsViewModelTest extends SysuiTestCase { } private void resetPackageManagerMockingForUsingFallbackBacklinks() { + ResolveInfo backlinksTaskResolveInfo = createBacklinksTaskResolveInfo(); reset(mPackageManager); when(mPackageManager.loadItemIcon(any(), any())).thenReturn(FAKE_DRAWABLE); when(mPackageManager.resolveActivity(any(Intent.class), anyInt())) - // First the logic queries whether a package has a launcher activity, this should + // Firstly, the logic queries whether a package has a launcher activity, this should // resolve otherwise the logic filters out the task. - .thenReturn(createBacklinksTaskResolveInfo()) - // Then logic queries with the backlinks intent, this should not resolve for the + .thenReturn(backlinksTaskResolveInfo) + // Secondly, the logic builds a fallback main launcher intent, this should also + // resolve for the fallback intent to build correctly. + .thenReturn(backlinksTaskResolveInfo) + // Lastly, logic queries with the backlinks intent, this should not resolve for the // logic to use the fallback intent. .thenReturn(null); } @@ -360,6 +364,8 @@ public final class AppClipsViewModelTest extends SysuiTestCase { assertThat(actualBacklinksIntent.getPackage()).isEqualTo(BACKLINKS_TASK_PACKAGE_NAME); assertThat(actualBacklinksIntent.getAction()).isEqualTo(ACTION_MAIN); assertThat(actualBacklinksIntent.getCategories()).containsExactly(CATEGORY_LAUNCHER); + assertThat(actualBacklinksIntent.getComponent()).isEqualTo( + new ComponentName(BACKLINKS_TASK_PACKAGE_NAME, BACKLINKS_TASK_APP_NAME)); } private static ResolveInfo createBacklinksTaskResolveInfo() { 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 dd791e764e01..5ac61102fa99 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 androidx.test.filters.SmallTest 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 @@ import org.mockito.kotlin.eq @RunWith(AndroidJUnit4::class) @SmallTest @TestableLooper.RunWithLooper +@OptIn(ExperimentalCoroutinesApi::class) class SettingsProxyTest : SysuiTestCase() { private val testDispatcher = StandardTestDispatcher() @@ -60,11 +62,12 @@ class SettingsProxyTest : SysuiTestCase() { } @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 @@ class SettingsProxyTest : SysuiTestCase() { } @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 @@ class SettingsProxyTest : SysuiTestCase() { } @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 @@ class SettingsProxyTest : SysuiTestCase() { } @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,25 +173,58 @@ class SettingsProxyTest : SysuiTestCase() { } @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 unregisterContentObserverSync() { - mSettings.unregisterContentObserverSync(mContentObserver) - verify(mSettings.getContentResolver()).unregisterContentObserver(eq(mContentObserver)) + 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() = + testScope.runTest { + mSettings.unregisterContentObserverSync(mContentObserver) + verify(mSettings.getContentResolver()).unregisterContentObserver(eq(mContentObserver)) + } + + @Test fun unregisterContentObserverSuspend_inputString_success() = testScope.runTest { mSettings.unregisterContentObserver(mContentObserver) @@ -193,12 +232,12 @@ class SettingsProxyTest : SysuiTestCase() { } @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 8b0affe2d99d..e02042d26d45 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 @@ class FakeReduceBrightColorsController : ReduceBrightColorsController { } } } + + 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 9fc64a965f4b..099cb2894515 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.MainThread; 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 com.android.server.policy.WindowManagerPolicy; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; -import java.util.Objects; import java.util.StringJoiner; /** @@ -748,8 +746,6 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo 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 3f0f23f4a2f9..56da231ad31a 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.Nullable; 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 @@ import com.android.server.companion.virtual.VirtualDeviceManagerInternal; * 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 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen 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 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen /** 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 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen * 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 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen /** * 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 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen * @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 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen * 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 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen 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 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen 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 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen } 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 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen * @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 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen VirtualMouse virtualMouse = mVirtualDevice.createVirtualMouse( new VirtualMouseConfig.Builder() .setInputDeviceName("Mouse Keys Virtual Mouse") - .setAssociatedDisplayId(mDisplayId) + .setAssociatedDisplayId(displayId) .build()); return virtualMouse; } @@ -375,42 +376,56 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen 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 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen } } - @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 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation implemen mActiveMoveKey = KEY_NOT_SET; mActiveScrollKey = KEY_NOT_SET; mLastTimeKeyActionPerformed = 0; - mHandler.removeCallbacksAndMessages(null); + mHandler.removeCallbacksAndMessages(null); mVirtualDevice.close(); - mInputManager.unregisterInputDeviceListener(this); } - - @Override - public void onInputDeviceChanged(int deviceId) { - } - } 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 dc8d2390ef2d..0def51691efa 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 @@ class MouseKeysInterceptorTest { 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 @@ class MouseKeysInterceptorTest { 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 @@ class MouseKeysInterceptorTest { 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 @@ class MouseKeysInterceptorTest { 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 @@ class MouseKeysInterceptorTest { @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 @@ class MouseKeysInterceptorTest { @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 @@ class MouseKeysInterceptorTest { } @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/tests/Input/AndroidTest.xml b/tests/Input/AndroidTest.xml index 8db37058af2b..4a99bd4f1801 100644 --- a/tests/Input/AndroidTest.xml +++ b/tests/Input/AndroidTest.xml @@ -31,7 +31,7 @@ <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector"> <option name="pull-pattern-keys" value="input_.*" /> <!-- Pull files created by tests, like the output of screenshot tests --> - <option name="directory-keys" value="/storage/emulated/0/InputTests" /> + <option name="directory-keys" value="/sdcard/Download/InputTests" /> <option name="collect-on-run-ended-only" value="false" /> </metrics_collector> </configuration> diff --git a/tests/Input/src/com/android/test/input/PointerIconLoadingTest.kt b/tests/Input/src/com/android/test/input/PointerIconLoadingTest.kt index e0f8c6d6ff4a..d0148fbbee3d 100644 --- a/tests/Input/src/com/android/test/input/PointerIconLoadingTest.kt +++ b/tests/Input/src/com/android/test/input/PointerIconLoadingTest.kt @@ -19,7 +19,6 @@ package com.android.test.input import android.content.Context import android.content.res.Configuration import android.content.res.Resources -import android.os.Environment import android.view.ContextThemeWrapper import android.view.PointerIcon import android.view.flags.Flags.enableVectorCursorA11ySettings @@ -158,8 +157,7 @@ class PointerIconLoadingTest { const val SCREEN_WIDTH_DP = 480 const val SCREEN_HEIGHT_DP = 800 const val ASSETS_PATH = "tests/input/assets" - val TEST_OUTPUT_PATH = Environment.getExternalStorageDirectory().absolutePath + - "/InputTests/" + - PointerIconLoadingTest::class.java.simpleName + val TEST_OUTPUT_PATH = + "/sdcard/Download/InputTests/" + PointerIconLoadingTest::class.java.simpleName } } |