summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/api/current.txt6
-rw-r--r--core/java/android/companion/virtual/flags/flags.aconfig7
-rw-r--r--core/java/android/hardware/input/VirtualRotaryEncoderScrollEvent.java3
-rw-r--r--core/java/android/view/inputmethod/InputMethodInfo.java6
-rw-r--r--core/java/android/view/inputmethod/flags.aconfig9
-rw-r--r--core/java/android/window/flags/windowing_frontend.aconfig10
-rw-r--r--core/res/res/values/attrs.xml2
-rw-r--r--core/res/res/values/public-staging.xml2
-rw-r--r--core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodInfoTest.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/HandlerExecutor.java7
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/ShellExecutor.java7
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java35
-rw-r--r--packages/SettingsLib/res/layout/zen_mode_duration_dialog.xml4
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt9
-rw-r--r--packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt16
-rw-r--r--packages/SystemUI/aconfig/systemui.aconfig10
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt24
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt25
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt17
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt51
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt107
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt52
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ObservableTransitionStateTest.kt19
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt159
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt147
-rw-r--r--packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestSceneScope.kt10
-rw-r--r--packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt56
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/SettingObserverTest.kt117
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/airplane/domain/interactor/AirplaneModeTileDataInteractorTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/airplate/domain/interactor/AirplaneModeTileDataInteractorTest.kt)3
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/airplane/domain/interactor/AirplaneModeTileUserActionInteractorTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/airplate/domain/interactor/AirplaneModeTileUserActionInteractorTest.kt)5
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileDataInteractorTest.kt4
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileUserActionInteractorTest.kt72
-rw-r--r--packages/SystemUI/res/layout/app_clips_screenshot.xml2
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt14
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/ReduceBrightColorsController.java17
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/ReduceBrightColorsControllerImpl.java29
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/SettingObserver.java17
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java5
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/StockTilesRepository.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/ReduceBrightColorsAutoAddable.kt13
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/ReduceBrightColorsTile.java34
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileDataInteractor.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileUserActionInteractor.kt23
-rw-r--r--packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsViewModel.java20
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java8
-rw-r--r--packages/SystemUI/src/com/android/systemui/util/kotlin/ReduceBrightColorsControllerExt.kt14
-rw-r--r--packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.kt83
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ReduceBrightColorsTileTest.java3
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsViewModelTest.java12
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/util/settings/SettingsProxyTest.kt159
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/FakeReduceBrightColorsController.kt18
-rw-r--r--services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java4
-rw-r--r--services/accessibility/java/com/android/server/accessibility/MouseKeysInterceptor.java199
-rw-r--r--services/tests/servicestests/src/com/android/server/accessibility/MouseKeysInterceptorTest.kt45
-rw-r--r--tests/Input/AndroidTest.xml2
-rw-r--r--tests/Input/src/com/android/test/input/PointerIconLoadingTest.kt6
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
}
}