diff options
290 files changed, 9091 insertions, 3529 deletions
diff --git a/Ravenwood.bp b/Ravenwood.bp index 7faa33f8834e..258796942ca9 100644 --- a/Ravenwood.bp +++ b/Ravenwood.bp @@ -25,13 +25,15 @@ java_library { visibility: ["//visibility:private"], } -// Generate the stub/impl from framework-all, with hidden APIs. +// Process framework-all with hoststubgen for Ravenwood. // This step takes several tens of seconds, so we manually shard it to multiple modules. // All the copies have to be kept in sync. -// TODO: Do the sharding better. +// TODO: Do the sharding better, either by making hostsubgen support sharding natively, or +// making a better build rule. genrule_defaults { name: "framework-minus-apex.ravenwood-base_defaults", + defaults: ["ravenwood-internal-only-visibility-genrule"], tools: ["hoststubgen"], srcs: [ ":framework-minus-apex-for-hoststubgen", @@ -50,139 +52,94 @@ genrule_defaults { "hoststubgen_framework-minus-apex_stats.csv", "hoststubgen_framework-minus-apex_apis.csv", ], - visibility: ["//visibility:private"], } -java_genrule { - name: "framework-minus-apex.ravenwood-base_X0", - defaults: ["framework-minus-apex.ravenwood-base_defaults"], - cmd: "$(location hoststubgen) " + - "--num-shards 6 --shard-index 0 " + // Only this line differs +framework_minus_apex_cmd = "$(location hoststubgen) " + + "@$(location :ravenwood-standard-options) " + - "@$(location :ravenwood-standard-options) " + + "--debug-log $(location hoststubgen_framework-minus-apex.log) " + + "--stats-file $(location hoststubgen_framework-minus-apex_stats.csv) " + + "--supported-api-list-file $(location hoststubgen_framework-minus-apex_apis.csv) " + - "--debug-log $(location hoststubgen_framework-minus-apex.log) " + - "--stats-file $(location hoststubgen_framework-minus-apex_stats.csv) " + - "--supported-api-list-file $(location hoststubgen_framework-minus-apex_apis.csv) " + + "--out-impl-jar $(location ravenwood.jar) " + - "--out-impl-jar $(location ravenwood.jar) " + + "--gen-keep-all-file $(location hoststubgen_framework-minus-apex_keep_all.txt) " + + "--gen-input-dump-file $(location hoststubgen_framework-minus-apex_dump.txt) " + - "--gen-keep-all-file $(location hoststubgen_framework-minus-apex_keep_all.txt) " + - "--gen-input-dump-file $(location hoststubgen_framework-minus-apex_dump.txt) " + + "--in-jar $(location :framework-minus-apex-for-hoststubgen) " + + "--policy-override-file $(location :ravenwood-framework-policies) " + + "--annotation-allowed-classes-file $(location :ravenwood-annotation-allowed-classes) " - "--in-jar $(location :framework-minus-apex-for-hoststubgen) " + - "--policy-override-file $(location :ravenwood-framework-policies) " + - "--annotation-allowed-classes-file $(location :ravenwood-annotation-allowed-classes) ", +java_genrule { + name: "framework-minus-apex.ravenwood-base_X0", + defaults: ["framework-minus-apex.ravenwood-base_defaults"], + cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 0", } java_genrule { name: "framework-minus-apex.ravenwood-base_X1", defaults: ["framework-minus-apex.ravenwood-base_defaults"], - cmd: "$(location hoststubgen) " + - "--num-shards 6 --shard-index 1 " + // Only this line differs - - "@$(location :ravenwood-standard-options) " + - - "--debug-log $(location hoststubgen_framework-minus-apex.log) " + - "--stats-file $(location hoststubgen_framework-minus-apex_stats.csv) " + - "--supported-api-list-file $(location hoststubgen_framework-minus-apex_apis.csv) " + - - "--out-impl-jar $(location ravenwood.jar) " + - - "--gen-keep-all-file $(location hoststubgen_framework-minus-apex_keep_all.txt) " + - "--gen-input-dump-file $(location hoststubgen_framework-minus-apex_dump.txt) " + - - "--in-jar $(location :framework-minus-apex-for-hoststubgen) " + - "--policy-override-file $(location :ravenwood-framework-policies) " + - "--annotation-allowed-classes-file $(location :ravenwood-annotation-allowed-classes) ", + cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 1", } java_genrule { name: "framework-minus-apex.ravenwood-base_X2", defaults: ["framework-minus-apex.ravenwood-base_defaults"], - cmd: "$(location hoststubgen) " + - "--num-shards 6 --shard-index 2 " + // Only this line differs - - "@$(location :ravenwood-standard-options) " + - - "--debug-log $(location hoststubgen_framework-minus-apex.log) " + - "--stats-file $(location hoststubgen_framework-minus-apex_stats.csv) " + - "--supported-api-list-file $(location hoststubgen_framework-minus-apex_apis.csv) " + - - "--out-impl-jar $(location ravenwood.jar) " + - - "--gen-keep-all-file $(location hoststubgen_framework-minus-apex_keep_all.txt) " + - "--gen-input-dump-file $(location hoststubgen_framework-minus-apex_dump.txt) " + - - "--in-jar $(location :framework-minus-apex-for-hoststubgen) " + - "--policy-override-file $(location :ravenwood-framework-policies) " + - "--annotation-allowed-classes-file $(location :ravenwood-annotation-allowed-classes) ", + cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 2", } java_genrule { name: "framework-minus-apex.ravenwood-base_X3", defaults: ["framework-minus-apex.ravenwood-base_defaults"], - cmd: "$(location hoststubgen) " + - "--num-shards 6 --shard-index 3 " + // Only this line differs - - "@$(location :ravenwood-standard-options) " + - - "--debug-log $(location hoststubgen_framework-minus-apex.log) " + - "--stats-file $(location hoststubgen_framework-minus-apex_stats.csv) " + - "--supported-api-list-file $(location hoststubgen_framework-minus-apex_apis.csv) " + - - "--out-impl-jar $(location ravenwood.jar) " + - - "--gen-keep-all-file $(location hoststubgen_framework-minus-apex_keep_all.txt) " + - "--gen-input-dump-file $(location hoststubgen_framework-minus-apex_dump.txt) " + - - "--in-jar $(location :framework-minus-apex-for-hoststubgen) " + - "--policy-override-file $(location :ravenwood-framework-policies) " + - "--annotation-allowed-classes-file $(location :ravenwood-annotation-allowed-classes) ", + cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 3", } java_genrule { name: "framework-minus-apex.ravenwood-base_X4", defaults: ["framework-minus-apex.ravenwood-base_defaults"], - cmd: "$(location hoststubgen) " + - "--num-shards 6 --shard-index 4 " + // Only this line differs - - "@$(location :ravenwood-standard-options) " + - - "--debug-log $(location hoststubgen_framework-minus-apex.log) " + - "--stats-file $(location hoststubgen_framework-minus-apex_stats.csv) " + - "--supported-api-list-file $(location hoststubgen_framework-minus-apex_apis.csv) " + - - "--out-impl-jar $(location ravenwood.jar) " + - - "--gen-keep-all-file $(location hoststubgen_framework-minus-apex_keep_all.txt) " + - "--gen-input-dump-file $(location hoststubgen_framework-minus-apex_dump.txt) " + - - "--in-jar $(location :framework-minus-apex-for-hoststubgen) " + - "--policy-override-file $(location :ravenwood-framework-policies) " + - "--annotation-allowed-classes-file $(location :ravenwood-annotation-allowed-classes) ", + cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 4", } java_genrule { name: "framework-minus-apex.ravenwood-base_X5", defaults: ["framework-minus-apex.ravenwood-base_defaults"], - cmd: "$(location hoststubgen) " + - "--num-shards 6 --shard-index 5 " + // Only this line differs + cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 5", +} - "@$(location :ravenwood-standard-options) " + +java_genrule { + name: "framework-minus-apex.ravenwood-base_X6", + defaults: ["framework-minus-apex.ravenwood-base_defaults"], + cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 6", +} - "--debug-log $(location hoststubgen_framework-minus-apex.log) " + - "--stats-file $(location hoststubgen_framework-minus-apex_stats.csv) " + - "--supported-api-list-file $(location hoststubgen_framework-minus-apex_apis.csv) " + +java_genrule { + name: "framework-minus-apex.ravenwood-base_X7", + defaults: ["framework-minus-apex.ravenwood-base_defaults"], + cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 7", +} - "--out-impl-jar $(location ravenwood.jar) " + +java_genrule { + name: "framework-minus-apex.ravenwood-base_X8", + defaults: ["framework-minus-apex.ravenwood-base_defaults"], + cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 8", +} - "--gen-keep-all-file $(location hoststubgen_framework-minus-apex_keep_all.txt) " + - "--gen-input-dump-file $(location hoststubgen_framework-minus-apex_dump.txt) " + +java_genrule { + name: "framework-minus-apex.ravenwood-base_X9", + defaults: ["framework-minus-apex.ravenwood-base_defaults"], + cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 9", +} - "--in-jar $(location :framework-minus-apex-for-hoststubgen) " + - "--policy-override-file $(location :ravenwood-framework-policies) " + - "--annotation-allowed-classes-file $(location :ravenwood-annotation-allowed-classes) ", +// Build framework-minus-apex.ravenwood-base without sharding. +// We extract the various dump files from this one, rather than the sharded ones, because +// some dumps use the output from other classes (e.g. base classes) which may not be in the +// same shard. +// Not using sharding is fine for this module because it's only used for collecting the +// dump / stats files, which don't have to happen regularly. +java_genrule { + name: "framework-minus-apex.ravenwood-base_all", + defaults: ["framework-minus-apex.ravenwood-base_defaults"], + cmd: framework_minus_apex_cmd, } // Marge all the sharded jars @@ -198,73 +155,16 @@ java_genrule { ":framework-minus-apex.ravenwood-base_X3{ravenwood.jar}", ":framework-minus-apex.ravenwood-base_X4{ravenwood.jar}", ":framework-minus-apex.ravenwood-base_X5{ravenwood.jar}", + ":framework-minus-apex.ravenwood-base_X6{ravenwood.jar}", + ":framework-minus-apex.ravenwood-base_X7{ravenwood.jar}", + ":framework-minus-apex.ravenwood-base_X8{ravenwood.jar}", + ":framework-minus-apex.ravenwood-base_X9{ravenwood.jar}", ], out: [ "framework-minus-apex.ravenwood.jar", ], } -// Merge the sharded text files -genrule { - name: "hoststubgen_framework-minus-apex_stats.csv", - defaults: ["ravenwood-internal-only-visibility-genrule"], - cmd: "cat $(in) > $(out)", - srcs: [ - ":framework-minus-apex.ravenwood-base_X0{hoststubgen_framework-minus-apex_stats.csv}", - ":framework-minus-apex.ravenwood-base_X1{hoststubgen_framework-minus-apex_stats.csv}", - ":framework-minus-apex.ravenwood-base_X2{hoststubgen_framework-minus-apex_stats.csv}", - ":framework-minus-apex.ravenwood-base_X3{hoststubgen_framework-minus-apex_stats.csv}", - ":framework-minus-apex.ravenwood-base_X4{hoststubgen_framework-minus-apex_stats.csv}", - ":framework-minus-apex.ravenwood-base_X5{hoststubgen_framework-minus-apex_stats.csv}", - ], - out: ["hoststubgen_framework-minus-apex_stats.csv"], -} - -genrule { - name: "hoststubgen_framework-minus-apex_apis.csv", - defaults: ["ravenwood-internal-only-visibility-genrule"], - cmd: "cat $(in) > $(out)", - srcs: [ - ":framework-minus-apex.ravenwood-base_X0{hoststubgen_framework-minus-apex_apis.csv}", - ":framework-minus-apex.ravenwood-base_X1{hoststubgen_framework-minus-apex_apis.csv}", - ":framework-minus-apex.ravenwood-base_X2{hoststubgen_framework-minus-apex_apis.csv}", - ":framework-minus-apex.ravenwood-base_X3{hoststubgen_framework-minus-apex_apis.csv}", - ":framework-minus-apex.ravenwood-base_X4{hoststubgen_framework-minus-apex_apis.csv}", - ":framework-minus-apex.ravenwood-base_X5{hoststubgen_framework-minus-apex_apis.csv}", - ], - out: ["hoststubgen_framework-minus-apex_apis.csv"], -} - -genrule { - name: "hoststubgen_framework-minus-apex_keep_all.txt", - defaults: ["ravenwood-internal-only-visibility-genrule"], - cmd: "cat $(in) > $(out)", - srcs: [ - ":framework-minus-apex.ravenwood-base_X0{hoststubgen_framework-minus-apex_keep_all.txt}", - ":framework-minus-apex.ravenwood-base_X1{hoststubgen_framework-minus-apex_keep_all.txt}", - ":framework-minus-apex.ravenwood-base_X2{hoststubgen_framework-minus-apex_keep_all.txt}", - ":framework-minus-apex.ravenwood-base_X3{hoststubgen_framework-minus-apex_keep_all.txt}", - ":framework-minus-apex.ravenwood-base_X4{hoststubgen_framework-minus-apex_keep_all.txt}", - ":framework-minus-apex.ravenwood-base_X5{hoststubgen_framework-minus-apex_keep_all.txt}", - ], - out: ["hoststubgen_framework-minus-apex_keep_all.txt"], -} - -genrule { - name: "hoststubgen_framework-minus-apex_dump.txt", - defaults: ["ravenwood-internal-only-visibility-genrule"], - cmd: "cat $(in) > $(out)", - srcs: [ - ":framework-minus-apex.ravenwood-base_X0{hoststubgen_framework-minus-apex_dump.txt}", - ":framework-minus-apex.ravenwood-base_X1{hoststubgen_framework-minus-apex_dump.txt}", - ":framework-minus-apex.ravenwood-base_X2{hoststubgen_framework-minus-apex_dump.txt}", - ":framework-minus-apex.ravenwood-base_X3{hoststubgen_framework-minus-apex_dump.txt}", - ":framework-minus-apex.ravenwood-base_X4{hoststubgen_framework-minus-apex_dump.txt}", - ":framework-minus-apex.ravenwood-base_X5{hoststubgen_framework-minus-apex_dump.txt}", - ], - out: ["hoststubgen_framework-minus-apex_dump.txt"], -} - java_library { name: "services.core-for-hoststubgen", installable: false, // host only jar. @@ -325,6 +225,9 @@ java_genrule { ], } +// TODO(b/313930116) This jarjar is a bit slow. We should use hoststubgen for renaming, +// but services.core.ravenwood has complex dependencies, so it'll take more than +// just using hoststubgen "rename"s. java_library { name: "services.core.ravenwood-jarjar", defaults: ["ravenwood-internal-only-visibility-java"], @@ -337,7 +240,6 @@ java_library { // Jars in "ravenwood-runtime" are set to the classpath, sorted alphabetically. // Rename some of the dependencies to make sure they're included in the intended order. -// Also apply jarjar. java_library { name: "100-framework-minus-apex.ravenwood", defaults: ["ravenwood-internal-only-visibility-java"], diff --git a/apct-tests/perftests/core/src/android/text/VariableFontPerfTest.java b/apct-tests/perftests/core/src/android/text/VariableFontPerfTest.java index fbe67a477f5d..c34936f930f9 100644 --- a/apct-tests/perftests/core/src/android/text/VariableFontPerfTest.java +++ b/apct-tests/perftests/core/src/android/text/VariableFontPerfTest.java @@ -19,6 +19,7 @@ package android.text; import android.graphics.Paint; import android.graphics.RecordingCanvas; import android.graphics.RenderNode; +import android.graphics.Typeface; import android.perftests.utils.BenchmarkState; import android.perftests.utils.PerfStatusReporter; @@ -120,13 +121,34 @@ public class VariableFontPerfTest { public void testSetFontVariationSettings() { final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); final Paint paint = new Paint(PAINT); - final Random random = new Random(0); while (state.keepRunning()) { state.pauseTiming(); - int weight = random.nextInt(1000); + paint.setTypeface(null); + paint.setFontVariationSettings(null); + Typeface.clearTypefaceCachesForTestingPurpose(); state.resumeTiming(); - paint.setFontVariationSettings("'wght' " + weight); + paint.setFontVariationSettings("'wght' 450"); + } + Typeface.clearTypefaceCachesForTestingPurpose(); + } + + @Test + public void testSetFontVariationSettings_Cached() { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + final Paint paint = new Paint(PAINT); + Typeface.clearTypefaceCachesForTestingPurpose(); + + while (state.keepRunning()) { + state.pauseTiming(); + paint.setTypeface(null); + paint.setFontVariationSettings(null); + state.resumeTiming(); + + paint.setFontVariationSettings("'wght' 450"); } + + Typeface.clearTypefaceCachesForTestingPurpose(); } + } diff --git a/core/api/current.txt b/core/api/current.txt index 354e26b2eb02..ea039a7103a3 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -59034,7 +59034,7 @@ package android.widget { } public static interface CompoundButton.OnCheckedChangeListener { - method public void onCheckedChanged(android.widget.CompoundButton, boolean); + method public void onCheckedChanged(@NonNull android.widget.CompoundButton, boolean); } public abstract class CursorAdapter extends android.widget.BaseAdapter implements android.widget.Filterable android.widget.ThemedSpinnerAdapter { @@ -60099,7 +60099,7 @@ package android.widget { } public static interface RadioGroup.OnCheckedChangeListener { - method public void onCheckedChanged(android.widget.RadioGroup, @IdRes int); + method public void onCheckedChanged(@NonNull android.widget.RadioGroup, @IdRes int); } public class RatingBar extends android.widget.AbsSeekBar { diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 15e5706db9d1..445a57220757 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -11981,6 +11981,7 @@ package android.provider { field public static final String ACTION_MANAGE_APP_OVERLAY_PERMISSION = "android.settings.MANAGE_APP_OVERLAY_PERMISSION"; field public static final String ACTION_MANAGE_DOMAIN_URLS = "android.settings.MANAGE_DOMAIN_URLS"; field public static final String ACTION_MANAGE_MORE_DEFAULT_APPS_SETTINGS = "android.settings.MANAGE_MORE_DEFAULT_APPS_SETTINGS"; + field @FlaggedApi("android.nfc.nfc_action_manage_services_settings") public static final String ACTION_MANAGE_OTHER_NFC_SERVICES_SETTINGS = "android.settings.MANAGE_OTHER_NFC_SERVICES_SETTINGS"; field public static final String ACTION_NOTIFICATION_POLICY_ACCESS_DETAIL_SETTINGS = "android.settings.NOTIFICATION_POLICY_ACCESS_DETAIL_SETTINGS"; field public static final String ACTION_REQUEST_ENABLE_CONTENT_CAPTURE = "android.settings.REQUEST_ENABLE_CONTENT_CAPTURE"; field public static final String ACTION_SHOW_ADMIN_SUPPORT_DETAILS = "android.settings.SHOW_ADMIN_SUPPORT_DETAILS"; diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index 9d63be7239a0..21396a1a36e5 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -10768,8 +10768,13 @@ public class AppOpsManager { final long key = makeKey(uidState, flag); NoteOpEvent event = events.get(key); - if (lastEvent == null - || event != null && event.getNoteTime() > lastEvent.getNoteTime()) { + if (event == null) { + continue; + } + + if (lastEvent == null || event.getNoteTime() > lastEvent.getNoteTime() + || (event.getNoteTime() == lastEvent.getNoteTime() + && event.getDuration() > lastEvent.getDuration())) { lastEvent = event; } } diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 8365840b1efb..9aebfc8e5fd7 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -6676,6 +6676,16 @@ public abstract class Context { public static final String BLOCKED_NUMBERS_SERVICE = "blocked_numbers"; /** + * Use with {@link #getSystemService(String)} to retrieve the + * {@link com.android.internal.protolog.ProtoLogService} for registering ProtoLog clients. + * + * @see #getSystemService(String) + * @see com.android.internal.protolog.ProtoLogService + * @hide + */ + public static final String PROTOLOG_SERVICE = "protolog"; + + /** * Determine whether the given permission is allowed for a particular * process and user ID running in the system. * diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index 111e6a8e93ef..cb57c7bd565d 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -7485,7 +7485,7 @@ public class Intent implements Parcelable, Cloneable { /** * This flag is only used for split-screen multi-window mode. The new activity will be displayed - * adjacent to the one launching it. This can only be used in conjunction with + * adjacent to the one launching it if possible. This can only be used in conjunction with * {@link #FLAG_ACTIVITY_NEW_TASK}. Also, setting {@link #FLAG_ACTIVITY_MULTIPLE_TASK} is * required if you want a new instance of an existing activity to be created. */ diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig index e370e85278d5..2bf8e1c99c61 100644 --- a/core/java/android/content/pm/multiuser.aconfig +++ b/core/java/android/content/pm/multiuser.aconfig @@ -151,6 +151,16 @@ flag { } flag { + name: "fix_avatar_cross_user_leak" + namespace: "multiuser" + description: "Fix cross-user picture uri leak for avatar picker apps." + bug: "341688848" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "fix_get_user_property_cache" namespace: "multiuser" description: "Cache is not optimised for getUserProperty for values below 0, eg. UserHandler.USER_NULL or UserHandle.USER_ALL" diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl index 1767d6438999..98e11375f077 100644 --- a/core/java/android/hardware/input/IInputManager.aidl +++ b/core/java/android/hardware/input/IInputManager.aidl @@ -25,6 +25,7 @@ import android.hardware.input.IInputDeviceBatteryListener; import android.hardware.input.IInputDeviceBatteryState; import android.hardware.input.IKeyboardBacklightListener; import android.hardware.input.IKeyboardBacklightState; +import android.hardware.input.IKeyboardSystemShortcutListener; import android.hardware.input.IStickyModifierStateListener; import android.hardware.input.ITabletModeChangedListener; import android.hardware.input.KeyboardLayoutSelectionResult; @@ -239,4 +240,14 @@ interface IInputManager { void unregisterStickyModifierStateListener(IStickyModifierStateListener listener); KeyGlyphMap getKeyGlyphMap(int deviceId); + + @EnforcePermission("MONITOR_KEYBOARD_SYSTEM_SHORTCUTS") + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " + + "android.Manifest.permission.MONITOR_KEYBOARD_SYSTEM_SHORTCUTS)") + void registerKeyboardSystemShortcutListener(IKeyboardSystemShortcutListener listener); + + @EnforcePermission("MONITOR_KEYBOARD_SYSTEM_SHORTCUTS") + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " + + "android.Manifest.permission.MONITOR_KEYBOARD_SYSTEM_SHORTCUTS)") + void unregisterKeyboardSystemShortcutListener(IKeyboardSystemShortcutListener listener); } diff --git a/core/java/android/hardware/input/IKeyboardSystemShortcutListener.aidl b/core/java/android/hardware/input/IKeyboardSystemShortcutListener.aidl new file mode 100644 index 000000000000..8d44917845f4 --- /dev/null +++ b/core/java/android/hardware/input/IKeyboardSystemShortcutListener.aidl @@ -0,0 +1,27 @@ +/* + * Copyright 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 android.hardware.input; + +/** @hide */ +oneway interface IKeyboardSystemShortcutListener { + + /** + * Called when the keyboard system shortcut is triggered. + */ + void onKeyboardSystemShortcutTriggered(int deviceId, in int[] keycodes, int modifierState, + int shortcut); +} diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java index d7952eb26f7e..6bc522b2b386 100644 --- a/core/java/android/hardware/input/InputManager.java +++ b/core/java/android/hardware/input/InputManager.java @@ -1378,6 +1378,36 @@ public final class InputManager { } /** + * Registers a keyboard system shortcut listener for {@link KeyboardSystemShortcut} being + * triggered. + * + * @param executor an executor on which the callback will be called + * @param listener the {@link KeyboardSystemShortcutListener} + * @throws IllegalArgumentException if {@code listener} has already been registered previously. + * @throws NullPointerException if {@code listener} or {@code executor} is null. + * @hide + * @see #unregisterKeyboardSystemShortcutListener(KeyboardSystemShortcutListener) + */ + @RequiresPermission(Manifest.permission.MONITOR_KEYBOARD_SYSTEM_SHORTCUTS) + public void registerKeyboardSystemShortcutListener(@NonNull Executor executor, + @NonNull KeyboardSystemShortcutListener listener) throws IllegalArgumentException { + mGlobal.registerKeyboardSystemShortcutListener(executor, listener); + } + + /** + * Unregisters a previously added keyboard system shortcut listener. + * + * @param listener the {@link KeyboardSystemShortcutListener} + * @hide + * @see #registerKeyboardSystemShortcutListener(Executor, KeyboardSystemShortcutListener) + */ + @RequiresPermission(Manifest.permission.MONITOR_KEYBOARD_SYSTEM_SHORTCUTS) + public void unregisterKeyboardSystemShortcutListener( + @NonNull KeyboardSystemShortcutListener listener) { + mGlobal.unregisterKeyboardSystemShortcutListener(listener); + } + + /** * A callback used to be notified about battery state changes for an input device. The * {@link #onBatteryStateChanged(int, long, BatteryState)} method will be called once after the * listener is successfully registered to provide the initial battery state of the device. @@ -1478,4 +1508,21 @@ public final class InputManager { */ void onStickyModifierStateChanged(@NonNull StickyModifierState state); } + + /** + * A callback used to be notified about keyboard system shortcuts being triggered. + * + * @see #registerKeyboardSystemShortcutListener(Executor, KeyboardSystemShortcutListener) + * @see #unregisterKeyboardSystemShortcutListener(KeyboardSystemShortcutListener) + * @hide + */ + public interface KeyboardSystemShortcutListener { + /** + * Called when a keyboard system shortcut is triggered. + * + * @param systemShortcut the shortcut info about the shortcut that was triggered. + */ + void onKeyboardSystemShortcutTriggered(int deviceId, + @NonNull KeyboardSystemShortcut systemShortcut); + } } diff --git a/core/java/android/hardware/input/InputManagerGlobal.java b/core/java/android/hardware/input/InputManagerGlobal.java index 7b471806cfc1..f7fa5577a047 100644 --- a/core/java/android/hardware/input/InputManagerGlobal.java +++ b/core/java/android/hardware/input/InputManagerGlobal.java @@ -26,6 +26,7 @@ import android.hardware.SensorManager; import android.hardware.input.InputManager.InputDeviceBatteryListener; import android.hardware.input.InputManager.InputDeviceListener; import android.hardware.input.InputManager.KeyboardBacklightListener; +import android.hardware.input.InputManager.KeyboardSystemShortcutListener; import android.hardware.input.InputManager.OnTabletModeChangedListener; import android.hardware.input.InputManager.StickyModifierStateListener; import android.hardware.lights.Light; @@ -110,6 +111,14 @@ public final class InputManagerGlobal { @Nullable private IStickyModifierStateListener mStickyModifierStateListener; + private final Object mKeyboardSystemShortcutListenerLock = new Object(); + @GuardedBy("mKeyboardSystemShortcutListenerLock") + @Nullable + private ArrayList<KeyboardSystemShortcutListenerDelegate> mKeyboardSystemShortcutListeners; + @GuardedBy("mKeyboardSystemShortcutListenerLock") + @Nullable + private IKeyboardSystemShortcutListener mKeyboardSystemShortcutListener; + // InputDeviceSensorManager gets notified synchronously from the binder thread when input // devices change, so it must be synchronized with the input device listeners. @GuardedBy("mInputDeviceListeners") @@ -1055,6 +1064,98 @@ public final class InputManagerGlobal { } } + private static final class KeyboardSystemShortcutListenerDelegate { + final KeyboardSystemShortcutListener mListener; + final Executor mExecutor; + + KeyboardSystemShortcutListenerDelegate(KeyboardSystemShortcutListener listener, + Executor executor) { + mListener = listener; + mExecutor = executor; + } + + void onKeyboardSystemShortcutTriggered(int deviceId, + KeyboardSystemShortcut systemShortcut) { + mExecutor.execute(() -> + mListener.onKeyboardSystemShortcutTriggered(deviceId, systemShortcut)); + } + } + + private class LocalKeyboardSystemShortcutListener extends IKeyboardSystemShortcutListener.Stub { + + @Override + public void onKeyboardSystemShortcutTriggered(int deviceId, int[] keycodes, + int modifierState, int shortcut) { + synchronized (mKeyboardSystemShortcutListenerLock) { + if (mKeyboardSystemShortcutListeners == null) return; + final int numListeners = mKeyboardSystemShortcutListeners.size(); + for (int i = 0; i < numListeners; i++) { + mKeyboardSystemShortcutListeners.get(i) + .onKeyboardSystemShortcutTriggered(deviceId, + new KeyboardSystemShortcut(keycodes, modifierState, shortcut)); + } + } + } + } + + /** + * @see InputManager#registerKeyboardSystemShortcutListener(Executor, + * KeyboardSystemShortcutListener) + */ + @RequiresPermission(Manifest.permission.MONITOR_KEYBOARD_SYSTEM_SHORTCUTS) + void registerKeyboardSystemShortcutListener(@NonNull Executor executor, + @NonNull KeyboardSystemShortcutListener listener) throws IllegalArgumentException { + Objects.requireNonNull(executor, "executor should not be null"); + Objects.requireNonNull(listener, "listener should not be null"); + + synchronized (mKeyboardSystemShortcutListenerLock) { + if (mKeyboardSystemShortcutListener == null) { + mKeyboardSystemShortcutListeners = new ArrayList<>(); + mKeyboardSystemShortcutListener = new LocalKeyboardSystemShortcutListener(); + + try { + mIm.registerKeyboardSystemShortcutListener(mKeyboardSystemShortcutListener); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + final int numListeners = mKeyboardSystemShortcutListeners.size(); + for (int i = 0; i < numListeners; i++) { + if (mKeyboardSystemShortcutListeners.get(i).mListener == listener) { + throw new IllegalArgumentException("Listener has already been registered!"); + } + } + KeyboardSystemShortcutListenerDelegate delegate = + new KeyboardSystemShortcutListenerDelegate(listener, executor); + mKeyboardSystemShortcutListeners.add(delegate); + } + } + + /** + * @see InputManager#unregisterKeyboardSystemShortcutListener(KeyboardSystemShortcutListener) + */ + @RequiresPermission(Manifest.permission.MONITOR_KEYBOARD_SYSTEM_SHORTCUTS) + void unregisterKeyboardSystemShortcutListener( + @NonNull KeyboardSystemShortcutListener listener) { + Objects.requireNonNull(listener, "listener should not be null"); + + synchronized (mKeyboardSystemShortcutListenerLock) { + if (mKeyboardSystemShortcutListeners == null) { + return; + } + mKeyboardSystemShortcutListeners.removeIf((delegate) -> delegate.mListener == listener); + if (mKeyboardSystemShortcutListeners.isEmpty()) { + try { + mIm.unregisterKeyboardSystemShortcutListener(mKeyboardSystemShortcutListener); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + mKeyboardSystemShortcutListeners = null; + mKeyboardSystemShortcutListener = null; + } + } + } + /** * TODO(b/330517633): Cleanup the unsupported API */ diff --git a/core/java/android/hardware/input/KeyboardSystemShortcut.java b/core/java/android/hardware/input/KeyboardSystemShortcut.java new file mode 100644 index 000000000000..89cf877c3aa8 --- /dev/null +++ b/core/java/android/hardware/input/KeyboardSystemShortcut.java @@ -0,0 +1,522 @@ +/* + * Copyright 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 android.hardware.input; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; + +import com.android.internal.util.DataClass; +import com.android.internal.util.FrameworkStatsLog; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Provides information about the keyboard shortcut being triggered by an external keyboard. + * + * @hide + */ +@DataClass(genToString = true, genEqualsHashCode = true) +public class KeyboardSystemShortcut { + + private static final String TAG = "KeyboardSystemShortcut"; + + @NonNull + private final int[] mKeycodes; + private final int mModifierState; + @SystemShortcut + private final int mSystemShortcut; + + + public static final int SYSTEM_SHORTCUT_UNSPECIFIED = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__UNSPECIFIED; + public static final int SYSTEM_SHORTCUT_HOME = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__HOME; + public static final int SYSTEM_SHORTCUT_RECENT_APPS = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__RECENT_APPS; + public static final int SYSTEM_SHORTCUT_BACK = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__BACK; + public static final int SYSTEM_SHORTCUT_APP_SWITCH = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__APP_SWITCH; + public static final int SYSTEM_SHORTCUT_LAUNCH_ASSISTANT = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_ASSISTANT; + public static final int SYSTEM_SHORTCUT_LAUNCH_VOICE_ASSISTANT = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_VOICE_ASSISTANT; + public static final int SYSTEM_SHORTCUT_LAUNCH_SYSTEM_SETTINGS = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_SYSTEM_SETTINGS; + public static final int SYSTEM_SHORTCUT_TOGGLE_NOTIFICATION_PANEL = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__TOGGLE_NOTIFICATION_PANEL; + public static final int SYSTEM_SHORTCUT_TOGGLE_TASKBAR = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__TOGGLE_TASKBAR; + public static final int SYSTEM_SHORTCUT_TAKE_SCREENSHOT = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__TAKE_SCREENSHOT; + public static final int SYSTEM_SHORTCUT_OPEN_SHORTCUT_HELPER = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__OPEN_SHORTCUT_HELPER; + public static final int SYSTEM_SHORTCUT_BRIGHTNESS_UP = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__BRIGHTNESS_UP; + public static final int SYSTEM_SHORTCUT_BRIGHTNESS_DOWN = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__BRIGHTNESS_DOWN; + public static final int SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_UP = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__KEYBOARD_BACKLIGHT_UP; + public static final int SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_DOWN = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__KEYBOARD_BACKLIGHT_DOWN; + public static final int SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_TOGGLE = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__KEYBOARD_BACKLIGHT_TOGGLE; + public static final int SYSTEM_SHORTCUT_VOLUME_UP = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__VOLUME_UP; + public static final int SYSTEM_SHORTCUT_VOLUME_DOWN = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__VOLUME_DOWN; + public static final int SYSTEM_SHORTCUT_VOLUME_MUTE = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__VOLUME_MUTE; + public static final int SYSTEM_SHORTCUT_ALL_APPS = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__ALL_APPS; + public static final int SYSTEM_SHORTCUT_LAUNCH_SEARCH = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_SEARCH; + public static final int SYSTEM_SHORTCUT_LANGUAGE_SWITCH = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LANGUAGE_SWITCH; + public static final int SYSTEM_SHORTCUT_ACCESSIBILITY_ALL_APPS = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__ACCESSIBILITY_ALL_APPS; + public static final int SYSTEM_SHORTCUT_TOGGLE_CAPS_LOCK = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__TOGGLE_CAPS_LOCK; + public static final int SYSTEM_SHORTCUT_SYSTEM_MUTE = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__SYSTEM_MUTE; + public static final int SYSTEM_SHORTCUT_SPLIT_SCREEN_NAVIGATION = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__SPLIT_SCREEN_NAVIGATION; + public static final int SYSTEM_SHORTCUT_CHANGE_SPLITSCREEN_FOCUS = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__CHANGE_SPLITSCREEN_FOCUS; + public static final int SYSTEM_SHORTCUT_TRIGGER_BUG_REPORT = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__TRIGGER_BUG_REPORT; + public static final int SYSTEM_SHORTCUT_LOCK_SCREEN = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LOCK_SCREEN; + public static final int SYSTEM_SHORTCUT_OPEN_NOTES = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__OPEN_NOTES; + public static final int SYSTEM_SHORTCUT_TOGGLE_POWER = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__TOGGLE_POWER; + public static final int SYSTEM_SHORTCUT_SYSTEM_NAVIGATION = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__SYSTEM_NAVIGATION; + public static final int SYSTEM_SHORTCUT_SLEEP = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__SLEEP; + public static final int SYSTEM_SHORTCUT_WAKEUP = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__WAKEUP; + public static final int SYSTEM_SHORTCUT_MEDIA_KEY = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__MEDIA_KEY; + public static final int SYSTEM_SHORTCUT_LAUNCH_DEFAULT_BROWSER = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_BROWSER; + public static final int SYSTEM_SHORTCUT_LAUNCH_DEFAULT_EMAIL = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_EMAIL; + public static final int SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CONTACTS = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_CONTACTS; + public static final int SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CALENDAR = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_CALENDAR; + public static final int SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CALCULATOR = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_CALCULATOR; + public static final int SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MUSIC = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_MUSIC; + public static final int SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MAPS = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_MAPS; + public static final int SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MESSAGING = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_MESSAGING; + public static final int SYSTEM_SHORTCUT_LAUNCH_DEFAULT_GALLERY = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_GALLERY; + public static final int SYSTEM_SHORTCUT_LAUNCH_DEFAULT_FILES = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_FILES; + public static final int SYSTEM_SHORTCUT_LAUNCH_DEFAULT_WEATHER = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_WEATHER; + public static final int SYSTEM_SHORTCUT_LAUNCH_DEFAULT_FITNESS = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_FITNESS; + public static final int SYSTEM_SHORTCUT_LAUNCH_APPLICATION_BY_PACKAGE_NAME = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_APPLICATION_BY_PACKAGE_NAME; + public static final int SYSTEM_SHORTCUT_DESKTOP_MODE = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__DESKTOP_MODE; + public static final int SYSTEM_SHORTCUT_MULTI_WINDOW_NAVIGATION = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__MULTI_WINDOW_NAVIGATION; + + + + // Code below generated by codegen v1.0.23. + // + // DO NOT MODIFY! + // CHECKSTYLE:OFF Generated code + // + // To regenerate run: + // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/hardware/input/KeyboardSystemShortcut.java + // + // To exclude the generated code from IntelliJ auto-formatting enable (one-time): + // Settings > Editor > Code Style > Formatter Control + //@formatter:off + + + @IntDef(prefix = "SYSTEM_SHORTCUT_", value = { + SYSTEM_SHORTCUT_UNSPECIFIED, + SYSTEM_SHORTCUT_HOME, + SYSTEM_SHORTCUT_RECENT_APPS, + SYSTEM_SHORTCUT_BACK, + SYSTEM_SHORTCUT_APP_SWITCH, + SYSTEM_SHORTCUT_LAUNCH_ASSISTANT, + SYSTEM_SHORTCUT_LAUNCH_VOICE_ASSISTANT, + SYSTEM_SHORTCUT_LAUNCH_SYSTEM_SETTINGS, + SYSTEM_SHORTCUT_TOGGLE_NOTIFICATION_PANEL, + SYSTEM_SHORTCUT_TOGGLE_TASKBAR, + SYSTEM_SHORTCUT_TAKE_SCREENSHOT, + SYSTEM_SHORTCUT_OPEN_SHORTCUT_HELPER, + SYSTEM_SHORTCUT_BRIGHTNESS_UP, + SYSTEM_SHORTCUT_BRIGHTNESS_DOWN, + SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_UP, + SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_DOWN, + SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_TOGGLE, + SYSTEM_SHORTCUT_VOLUME_UP, + SYSTEM_SHORTCUT_VOLUME_DOWN, + SYSTEM_SHORTCUT_VOLUME_MUTE, + SYSTEM_SHORTCUT_ALL_APPS, + SYSTEM_SHORTCUT_LAUNCH_SEARCH, + SYSTEM_SHORTCUT_LANGUAGE_SWITCH, + SYSTEM_SHORTCUT_ACCESSIBILITY_ALL_APPS, + SYSTEM_SHORTCUT_TOGGLE_CAPS_LOCK, + SYSTEM_SHORTCUT_SYSTEM_MUTE, + SYSTEM_SHORTCUT_SPLIT_SCREEN_NAVIGATION, + SYSTEM_SHORTCUT_CHANGE_SPLITSCREEN_FOCUS, + SYSTEM_SHORTCUT_TRIGGER_BUG_REPORT, + SYSTEM_SHORTCUT_LOCK_SCREEN, + SYSTEM_SHORTCUT_OPEN_NOTES, + SYSTEM_SHORTCUT_TOGGLE_POWER, + SYSTEM_SHORTCUT_SYSTEM_NAVIGATION, + SYSTEM_SHORTCUT_SLEEP, + SYSTEM_SHORTCUT_WAKEUP, + SYSTEM_SHORTCUT_MEDIA_KEY, + SYSTEM_SHORTCUT_LAUNCH_DEFAULT_BROWSER, + SYSTEM_SHORTCUT_LAUNCH_DEFAULT_EMAIL, + SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CONTACTS, + SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CALENDAR, + SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CALCULATOR, + SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MUSIC, + SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MAPS, + SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MESSAGING, + SYSTEM_SHORTCUT_LAUNCH_DEFAULT_GALLERY, + SYSTEM_SHORTCUT_LAUNCH_DEFAULT_FILES, + SYSTEM_SHORTCUT_LAUNCH_DEFAULT_WEATHER, + SYSTEM_SHORTCUT_LAUNCH_DEFAULT_FITNESS, + SYSTEM_SHORTCUT_LAUNCH_APPLICATION_BY_PACKAGE_NAME, + SYSTEM_SHORTCUT_DESKTOP_MODE, + SYSTEM_SHORTCUT_MULTI_WINDOW_NAVIGATION + }) + @Retention(RetentionPolicy.SOURCE) + @DataClass.Generated.Member + public @interface SystemShortcut {} + + @DataClass.Generated.Member + public static String systemShortcutToString(@SystemShortcut int value) { + switch (value) { + case SYSTEM_SHORTCUT_UNSPECIFIED: + return "SYSTEM_SHORTCUT_UNSPECIFIED"; + case SYSTEM_SHORTCUT_HOME: + return "SYSTEM_SHORTCUT_HOME"; + case SYSTEM_SHORTCUT_RECENT_APPS: + return "SYSTEM_SHORTCUT_RECENT_APPS"; + case SYSTEM_SHORTCUT_BACK: + return "SYSTEM_SHORTCUT_BACK"; + case SYSTEM_SHORTCUT_APP_SWITCH: + return "SYSTEM_SHORTCUT_APP_SWITCH"; + case SYSTEM_SHORTCUT_LAUNCH_ASSISTANT: + return "SYSTEM_SHORTCUT_LAUNCH_ASSISTANT"; + case SYSTEM_SHORTCUT_LAUNCH_VOICE_ASSISTANT: + return "SYSTEM_SHORTCUT_LAUNCH_VOICE_ASSISTANT"; + case SYSTEM_SHORTCUT_LAUNCH_SYSTEM_SETTINGS: + return "SYSTEM_SHORTCUT_LAUNCH_SYSTEM_SETTINGS"; + case SYSTEM_SHORTCUT_TOGGLE_NOTIFICATION_PANEL: + return "SYSTEM_SHORTCUT_TOGGLE_NOTIFICATION_PANEL"; + case SYSTEM_SHORTCUT_TOGGLE_TASKBAR: + return "SYSTEM_SHORTCUT_TOGGLE_TASKBAR"; + case SYSTEM_SHORTCUT_TAKE_SCREENSHOT: + return "SYSTEM_SHORTCUT_TAKE_SCREENSHOT"; + case SYSTEM_SHORTCUT_OPEN_SHORTCUT_HELPER: + return "SYSTEM_SHORTCUT_OPEN_SHORTCUT_HELPER"; + case SYSTEM_SHORTCUT_BRIGHTNESS_UP: + return "SYSTEM_SHORTCUT_BRIGHTNESS_UP"; + case SYSTEM_SHORTCUT_BRIGHTNESS_DOWN: + return "SYSTEM_SHORTCUT_BRIGHTNESS_DOWN"; + case SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_UP: + return "SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_UP"; + case SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_DOWN: + return "SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_DOWN"; + case SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_TOGGLE: + return "SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_TOGGLE"; + case SYSTEM_SHORTCUT_VOLUME_UP: + return "SYSTEM_SHORTCUT_VOLUME_UP"; + case SYSTEM_SHORTCUT_VOLUME_DOWN: + return "SYSTEM_SHORTCUT_VOLUME_DOWN"; + case SYSTEM_SHORTCUT_VOLUME_MUTE: + return "SYSTEM_SHORTCUT_VOLUME_MUTE"; + case SYSTEM_SHORTCUT_ALL_APPS: + return "SYSTEM_SHORTCUT_ALL_APPS"; + case SYSTEM_SHORTCUT_LAUNCH_SEARCH: + return "SYSTEM_SHORTCUT_LAUNCH_SEARCH"; + case SYSTEM_SHORTCUT_LANGUAGE_SWITCH: + return "SYSTEM_SHORTCUT_LANGUAGE_SWITCH"; + case SYSTEM_SHORTCUT_ACCESSIBILITY_ALL_APPS: + return "SYSTEM_SHORTCUT_ACCESSIBILITY_ALL_APPS"; + case SYSTEM_SHORTCUT_TOGGLE_CAPS_LOCK: + return "SYSTEM_SHORTCUT_TOGGLE_CAPS_LOCK"; + case SYSTEM_SHORTCUT_SYSTEM_MUTE: + return "SYSTEM_SHORTCUT_SYSTEM_MUTE"; + case SYSTEM_SHORTCUT_SPLIT_SCREEN_NAVIGATION: + return "SYSTEM_SHORTCUT_SPLIT_SCREEN_NAVIGATION"; + case SYSTEM_SHORTCUT_CHANGE_SPLITSCREEN_FOCUS: + return "SYSTEM_SHORTCUT_CHANGE_SPLITSCREEN_FOCUS"; + case SYSTEM_SHORTCUT_TRIGGER_BUG_REPORT: + return "SYSTEM_SHORTCUT_TRIGGER_BUG_REPORT"; + case SYSTEM_SHORTCUT_LOCK_SCREEN: + return "SYSTEM_SHORTCUT_LOCK_SCREEN"; + case SYSTEM_SHORTCUT_OPEN_NOTES: + return "SYSTEM_SHORTCUT_OPEN_NOTES"; + case SYSTEM_SHORTCUT_TOGGLE_POWER: + return "SYSTEM_SHORTCUT_TOGGLE_POWER"; + case SYSTEM_SHORTCUT_SYSTEM_NAVIGATION: + return "SYSTEM_SHORTCUT_SYSTEM_NAVIGATION"; + case SYSTEM_SHORTCUT_SLEEP: + return "SYSTEM_SHORTCUT_SLEEP"; + case SYSTEM_SHORTCUT_WAKEUP: + return "SYSTEM_SHORTCUT_WAKEUP"; + case SYSTEM_SHORTCUT_MEDIA_KEY: + return "SYSTEM_SHORTCUT_MEDIA_KEY"; + case SYSTEM_SHORTCUT_LAUNCH_DEFAULT_BROWSER: + return "SYSTEM_SHORTCUT_LAUNCH_DEFAULT_BROWSER"; + case SYSTEM_SHORTCUT_LAUNCH_DEFAULT_EMAIL: + return "SYSTEM_SHORTCUT_LAUNCH_DEFAULT_EMAIL"; + case SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CONTACTS: + return "SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CONTACTS"; + case SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CALENDAR: + return "SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CALENDAR"; + case SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CALCULATOR: + return "SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CALCULATOR"; + case SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MUSIC: + return "SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MUSIC"; + case SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MAPS: + return "SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MAPS"; + case SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MESSAGING: + return "SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MESSAGING"; + case SYSTEM_SHORTCUT_LAUNCH_DEFAULT_GALLERY: + return "SYSTEM_SHORTCUT_LAUNCH_DEFAULT_GALLERY"; + case SYSTEM_SHORTCUT_LAUNCH_DEFAULT_FILES: + return "SYSTEM_SHORTCUT_LAUNCH_DEFAULT_FILES"; + case SYSTEM_SHORTCUT_LAUNCH_DEFAULT_WEATHER: + return "SYSTEM_SHORTCUT_LAUNCH_DEFAULT_WEATHER"; + case SYSTEM_SHORTCUT_LAUNCH_DEFAULT_FITNESS: + return "SYSTEM_SHORTCUT_LAUNCH_DEFAULT_FITNESS"; + case SYSTEM_SHORTCUT_LAUNCH_APPLICATION_BY_PACKAGE_NAME: + return "SYSTEM_SHORTCUT_LAUNCH_APPLICATION_BY_PACKAGE_NAME"; + case SYSTEM_SHORTCUT_DESKTOP_MODE: + return "SYSTEM_SHORTCUT_DESKTOP_MODE"; + case SYSTEM_SHORTCUT_MULTI_WINDOW_NAVIGATION: + return "SYSTEM_SHORTCUT_MULTI_WINDOW_NAVIGATION"; + default: return Integer.toHexString(value); + } + } + + @DataClass.Generated.Member + public KeyboardSystemShortcut( + @NonNull int[] keycodes, + int modifierState, + @SystemShortcut int systemShortcut) { + this.mKeycodes = keycodes; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mKeycodes); + this.mModifierState = modifierState; + this.mSystemShortcut = systemShortcut; + + if (!(mSystemShortcut == SYSTEM_SHORTCUT_UNSPECIFIED) + && !(mSystemShortcut == SYSTEM_SHORTCUT_HOME) + && !(mSystemShortcut == SYSTEM_SHORTCUT_RECENT_APPS) + && !(mSystemShortcut == SYSTEM_SHORTCUT_BACK) + && !(mSystemShortcut == SYSTEM_SHORTCUT_APP_SWITCH) + && !(mSystemShortcut == SYSTEM_SHORTCUT_LAUNCH_ASSISTANT) + && !(mSystemShortcut == SYSTEM_SHORTCUT_LAUNCH_VOICE_ASSISTANT) + && !(mSystemShortcut == SYSTEM_SHORTCUT_LAUNCH_SYSTEM_SETTINGS) + && !(mSystemShortcut == SYSTEM_SHORTCUT_TOGGLE_NOTIFICATION_PANEL) + && !(mSystemShortcut == SYSTEM_SHORTCUT_TOGGLE_TASKBAR) + && !(mSystemShortcut == SYSTEM_SHORTCUT_TAKE_SCREENSHOT) + && !(mSystemShortcut == SYSTEM_SHORTCUT_OPEN_SHORTCUT_HELPER) + && !(mSystemShortcut == SYSTEM_SHORTCUT_BRIGHTNESS_UP) + && !(mSystemShortcut == SYSTEM_SHORTCUT_BRIGHTNESS_DOWN) + && !(mSystemShortcut == SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_UP) + && !(mSystemShortcut == SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_DOWN) + && !(mSystemShortcut == SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_TOGGLE) + && !(mSystemShortcut == SYSTEM_SHORTCUT_VOLUME_UP) + && !(mSystemShortcut == SYSTEM_SHORTCUT_VOLUME_DOWN) + && !(mSystemShortcut == SYSTEM_SHORTCUT_VOLUME_MUTE) + && !(mSystemShortcut == SYSTEM_SHORTCUT_ALL_APPS) + && !(mSystemShortcut == SYSTEM_SHORTCUT_LAUNCH_SEARCH) + && !(mSystemShortcut == SYSTEM_SHORTCUT_LANGUAGE_SWITCH) + && !(mSystemShortcut == SYSTEM_SHORTCUT_ACCESSIBILITY_ALL_APPS) + && !(mSystemShortcut == SYSTEM_SHORTCUT_TOGGLE_CAPS_LOCK) + && !(mSystemShortcut == SYSTEM_SHORTCUT_SYSTEM_MUTE) + && !(mSystemShortcut == SYSTEM_SHORTCUT_SPLIT_SCREEN_NAVIGATION) + && !(mSystemShortcut == SYSTEM_SHORTCUT_CHANGE_SPLITSCREEN_FOCUS) + && !(mSystemShortcut == SYSTEM_SHORTCUT_TRIGGER_BUG_REPORT) + && !(mSystemShortcut == SYSTEM_SHORTCUT_LOCK_SCREEN) + && !(mSystemShortcut == SYSTEM_SHORTCUT_OPEN_NOTES) + && !(mSystemShortcut == SYSTEM_SHORTCUT_TOGGLE_POWER) + && !(mSystemShortcut == SYSTEM_SHORTCUT_SYSTEM_NAVIGATION) + && !(mSystemShortcut == SYSTEM_SHORTCUT_SLEEP) + && !(mSystemShortcut == SYSTEM_SHORTCUT_WAKEUP) + && !(mSystemShortcut == SYSTEM_SHORTCUT_MEDIA_KEY) + && !(mSystemShortcut == SYSTEM_SHORTCUT_LAUNCH_DEFAULT_BROWSER) + && !(mSystemShortcut == SYSTEM_SHORTCUT_LAUNCH_DEFAULT_EMAIL) + && !(mSystemShortcut == SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CONTACTS) + && !(mSystemShortcut == SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CALENDAR) + && !(mSystemShortcut == SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CALCULATOR) + && !(mSystemShortcut == SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MUSIC) + && !(mSystemShortcut == SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MAPS) + && !(mSystemShortcut == SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MESSAGING) + && !(mSystemShortcut == SYSTEM_SHORTCUT_LAUNCH_DEFAULT_GALLERY) + && !(mSystemShortcut == SYSTEM_SHORTCUT_LAUNCH_DEFAULT_FILES) + && !(mSystemShortcut == SYSTEM_SHORTCUT_LAUNCH_DEFAULT_WEATHER) + && !(mSystemShortcut == SYSTEM_SHORTCUT_LAUNCH_DEFAULT_FITNESS) + && !(mSystemShortcut == SYSTEM_SHORTCUT_LAUNCH_APPLICATION_BY_PACKAGE_NAME) + && !(mSystemShortcut == SYSTEM_SHORTCUT_DESKTOP_MODE) + && !(mSystemShortcut == SYSTEM_SHORTCUT_MULTI_WINDOW_NAVIGATION)) { + throw new java.lang.IllegalArgumentException( + "systemShortcut was " + mSystemShortcut + " but must be one of: " + + "SYSTEM_SHORTCUT_UNSPECIFIED(" + SYSTEM_SHORTCUT_UNSPECIFIED + "), " + + "SYSTEM_SHORTCUT_HOME(" + SYSTEM_SHORTCUT_HOME + "), " + + "SYSTEM_SHORTCUT_RECENT_APPS(" + SYSTEM_SHORTCUT_RECENT_APPS + "), " + + "SYSTEM_SHORTCUT_BACK(" + SYSTEM_SHORTCUT_BACK + "), " + + "SYSTEM_SHORTCUT_APP_SWITCH(" + SYSTEM_SHORTCUT_APP_SWITCH + "), " + + "SYSTEM_SHORTCUT_LAUNCH_ASSISTANT(" + SYSTEM_SHORTCUT_LAUNCH_ASSISTANT + "), " + + "SYSTEM_SHORTCUT_LAUNCH_VOICE_ASSISTANT(" + SYSTEM_SHORTCUT_LAUNCH_VOICE_ASSISTANT + "), " + + "SYSTEM_SHORTCUT_LAUNCH_SYSTEM_SETTINGS(" + SYSTEM_SHORTCUT_LAUNCH_SYSTEM_SETTINGS + "), " + + "SYSTEM_SHORTCUT_TOGGLE_NOTIFICATION_PANEL(" + SYSTEM_SHORTCUT_TOGGLE_NOTIFICATION_PANEL + "), " + + "SYSTEM_SHORTCUT_TOGGLE_TASKBAR(" + SYSTEM_SHORTCUT_TOGGLE_TASKBAR + "), " + + "SYSTEM_SHORTCUT_TAKE_SCREENSHOT(" + SYSTEM_SHORTCUT_TAKE_SCREENSHOT + "), " + + "SYSTEM_SHORTCUT_OPEN_SHORTCUT_HELPER(" + SYSTEM_SHORTCUT_OPEN_SHORTCUT_HELPER + "), " + + "SYSTEM_SHORTCUT_BRIGHTNESS_UP(" + SYSTEM_SHORTCUT_BRIGHTNESS_UP + "), " + + "SYSTEM_SHORTCUT_BRIGHTNESS_DOWN(" + SYSTEM_SHORTCUT_BRIGHTNESS_DOWN + "), " + + "SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_UP(" + SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_UP + "), " + + "SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_DOWN(" + SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_DOWN + "), " + + "SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_TOGGLE(" + SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_TOGGLE + "), " + + "SYSTEM_SHORTCUT_VOLUME_UP(" + SYSTEM_SHORTCUT_VOLUME_UP + "), " + + "SYSTEM_SHORTCUT_VOLUME_DOWN(" + SYSTEM_SHORTCUT_VOLUME_DOWN + "), " + + "SYSTEM_SHORTCUT_VOLUME_MUTE(" + SYSTEM_SHORTCUT_VOLUME_MUTE + "), " + + "SYSTEM_SHORTCUT_ALL_APPS(" + SYSTEM_SHORTCUT_ALL_APPS + "), " + + "SYSTEM_SHORTCUT_LAUNCH_SEARCH(" + SYSTEM_SHORTCUT_LAUNCH_SEARCH + "), " + + "SYSTEM_SHORTCUT_LANGUAGE_SWITCH(" + SYSTEM_SHORTCUT_LANGUAGE_SWITCH + "), " + + "SYSTEM_SHORTCUT_ACCESSIBILITY_ALL_APPS(" + SYSTEM_SHORTCUT_ACCESSIBILITY_ALL_APPS + "), " + + "SYSTEM_SHORTCUT_TOGGLE_CAPS_LOCK(" + SYSTEM_SHORTCUT_TOGGLE_CAPS_LOCK + "), " + + "SYSTEM_SHORTCUT_SYSTEM_MUTE(" + SYSTEM_SHORTCUT_SYSTEM_MUTE + "), " + + "SYSTEM_SHORTCUT_SPLIT_SCREEN_NAVIGATION(" + SYSTEM_SHORTCUT_SPLIT_SCREEN_NAVIGATION + "), " + + "SYSTEM_SHORTCUT_CHANGE_SPLITSCREEN_FOCUS(" + SYSTEM_SHORTCUT_CHANGE_SPLITSCREEN_FOCUS + "), " + + "SYSTEM_SHORTCUT_TRIGGER_BUG_REPORT(" + SYSTEM_SHORTCUT_TRIGGER_BUG_REPORT + "), " + + "SYSTEM_SHORTCUT_LOCK_SCREEN(" + SYSTEM_SHORTCUT_LOCK_SCREEN + "), " + + "SYSTEM_SHORTCUT_OPEN_NOTES(" + SYSTEM_SHORTCUT_OPEN_NOTES + "), " + + "SYSTEM_SHORTCUT_TOGGLE_POWER(" + SYSTEM_SHORTCUT_TOGGLE_POWER + "), " + + "SYSTEM_SHORTCUT_SYSTEM_NAVIGATION(" + SYSTEM_SHORTCUT_SYSTEM_NAVIGATION + "), " + + "SYSTEM_SHORTCUT_SLEEP(" + SYSTEM_SHORTCUT_SLEEP + "), " + + "SYSTEM_SHORTCUT_WAKEUP(" + SYSTEM_SHORTCUT_WAKEUP + "), " + + "SYSTEM_SHORTCUT_MEDIA_KEY(" + SYSTEM_SHORTCUT_MEDIA_KEY + "), " + + "SYSTEM_SHORTCUT_LAUNCH_DEFAULT_BROWSER(" + SYSTEM_SHORTCUT_LAUNCH_DEFAULT_BROWSER + "), " + + "SYSTEM_SHORTCUT_LAUNCH_DEFAULT_EMAIL(" + SYSTEM_SHORTCUT_LAUNCH_DEFAULT_EMAIL + "), " + + "SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CONTACTS(" + SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CONTACTS + "), " + + "SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CALENDAR(" + SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CALENDAR + "), " + + "SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CALCULATOR(" + SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CALCULATOR + "), " + + "SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MUSIC(" + SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MUSIC + "), " + + "SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MAPS(" + SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MAPS + "), " + + "SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MESSAGING(" + SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MESSAGING + "), " + + "SYSTEM_SHORTCUT_LAUNCH_DEFAULT_GALLERY(" + SYSTEM_SHORTCUT_LAUNCH_DEFAULT_GALLERY + "), " + + "SYSTEM_SHORTCUT_LAUNCH_DEFAULT_FILES(" + SYSTEM_SHORTCUT_LAUNCH_DEFAULT_FILES + "), " + + "SYSTEM_SHORTCUT_LAUNCH_DEFAULT_WEATHER(" + SYSTEM_SHORTCUT_LAUNCH_DEFAULT_WEATHER + "), " + + "SYSTEM_SHORTCUT_LAUNCH_DEFAULT_FITNESS(" + SYSTEM_SHORTCUT_LAUNCH_DEFAULT_FITNESS + "), " + + "SYSTEM_SHORTCUT_LAUNCH_APPLICATION_BY_PACKAGE_NAME(" + SYSTEM_SHORTCUT_LAUNCH_APPLICATION_BY_PACKAGE_NAME + "), " + + "SYSTEM_SHORTCUT_DESKTOP_MODE(" + SYSTEM_SHORTCUT_DESKTOP_MODE + "), " + + "SYSTEM_SHORTCUT_MULTI_WINDOW_NAVIGATION(" + SYSTEM_SHORTCUT_MULTI_WINDOW_NAVIGATION + ")"); + } + + + // onConstructed(); // You can define this method to get a callback + } + + @DataClass.Generated.Member + public @NonNull int[] getKeycodes() { + return mKeycodes; + } + + @DataClass.Generated.Member + public int getModifierState() { + return mModifierState; + } + + @DataClass.Generated.Member + public @SystemShortcut int getSystemShortcut() { + return mSystemShortcut; + } + + @Override + @DataClass.Generated.Member + public String toString() { + // You can override field toString logic by defining methods like: + // String fieldNameToString() { ... } + + return "KeyboardSystemShortcut { " + + "keycodes = " + java.util.Arrays.toString(mKeycodes) + ", " + + "modifierState = " + mModifierState + ", " + + "systemShortcut = " + systemShortcutToString(mSystemShortcut) + + " }"; + } + + @Override + @DataClass.Generated.Member + public boolean equals(@Nullable Object o) { + // You can override field equality logic by defining either of the methods like: + // boolean fieldNameEquals(KeyboardSystemShortcut other) { ... } + // boolean fieldNameEquals(FieldType otherValue) { ... } + + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + @SuppressWarnings("unchecked") + KeyboardSystemShortcut that = (KeyboardSystemShortcut) o; + //noinspection PointlessBooleanExpression + return true + && java.util.Arrays.equals(mKeycodes, that.mKeycodes) + && mModifierState == that.mModifierState + && mSystemShortcut == that.mSystemShortcut; + } + + @Override + @DataClass.Generated.Member + public int hashCode() { + // You can override field hashCode logic by defining methods like: + // int fieldNameHashCode() { ... } + + int _hash = 1; + _hash = 31 * _hash + java.util.Arrays.hashCode(mKeycodes); + _hash = 31 * _hash + mModifierState; + _hash = 31 * _hash + mSystemShortcut; + return _hash; + } + + @DataClass.Generated( + time = 1722890917041L, + codegenVersion = "1.0.23", + sourceFile = "frameworks/base/core/java/android/hardware/input/KeyboardSystemShortcut.java", + inputSignatures = "private static final java.lang.String TAG\nprivate final @android.annotation.NonNull int[] mKeycodes\nprivate final int mModifierState\nprivate final @android.hardware.input.KeyboardSystemShortcut.SystemShortcut int mSystemShortcut\npublic static final int SYSTEM_SHORTCUT_UNSPECIFIED\npublic static final int SYSTEM_SHORTCUT_HOME\npublic static final int SYSTEM_SHORTCUT_RECENT_APPS\npublic static final int SYSTEM_SHORTCUT_BACK\npublic static final int SYSTEM_SHORTCUT_APP_SWITCH\npublic static final int SYSTEM_SHORTCUT_LAUNCH_ASSISTANT\npublic static final int SYSTEM_SHORTCUT_LAUNCH_VOICE_ASSISTANT\npublic static final int SYSTEM_SHORTCUT_LAUNCH_SYSTEM_SETTINGS\npublic static final int SYSTEM_SHORTCUT_TOGGLE_NOTIFICATION_PANEL\npublic static final int SYSTEM_SHORTCUT_TOGGLE_TASKBAR\npublic static final int SYSTEM_SHORTCUT_TAKE_SCREENSHOT\npublic static final int SYSTEM_SHORTCUT_OPEN_SHORTCUT_HELPER\npublic static final int SYSTEM_SHORTCUT_BRIGHTNESS_UP\npublic static final int SYSTEM_SHORTCUT_BRIGHTNESS_DOWN\npublic static final int SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_UP\npublic static final int SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_DOWN\npublic static final int SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_TOGGLE\npublic static final int SYSTEM_SHORTCUT_VOLUME_UP\npublic static final int SYSTEM_SHORTCUT_VOLUME_DOWN\npublic static final int SYSTEM_SHORTCUT_VOLUME_MUTE\npublic static final int SYSTEM_SHORTCUT_ALL_APPS\npublic static final int SYSTEM_SHORTCUT_LAUNCH_SEARCH\npublic static final int SYSTEM_SHORTCUT_LANGUAGE_SWITCH\npublic static final int SYSTEM_SHORTCUT_ACCESSIBILITY_ALL_APPS\npublic static final int SYSTEM_SHORTCUT_TOGGLE_CAPS_LOCK\npublic static final int SYSTEM_SHORTCUT_SYSTEM_MUTE\npublic static final int SYSTEM_SHORTCUT_SPLIT_SCREEN_NAVIGATION\npublic static final int SYSTEM_SHORTCUT_CHANGE_SPLITSCREEN_FOCUS\npublic static final int SYSTEM_SHORTCUT_TRIGGER_BUG_REPORT\npublic static final int SYSTEM_SHORTCUT_LOCK_SCREEN\npublic static final int SYSTEM_SHORTCUT_OPEN_NOTES\npublic static final int SYSTEM_SHORTCUT_TOGGLE_POWER\npublic static final int SYSTEM_SHORTCUT_SYSTEM_NAVIGATION\npublic static final int SYSTEM_SHORTCUT_SLEEP\npublic static final int SYSTEM_SHORTCUT_WAKEUP\npublic static final int SYSTEM_SHORTCUT_MEDIA_KEY\npublic static final int SYSTEM_SHORTCUT_LAUNCH_DEFAULT_BROWSER\npublic static final int SYSTEM_SHORTCUT_LAUNCH_DEFAULT_EMAIL\npublic static final int SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CONTACTS\npublic static final int SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CALENDAR\npublic static final int SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CALCULATOR\npublic static final int SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MUSIC\npublic static final int SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MAPS\npublic static final int SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MESSAGING\npublic static final int SYSTEM_SHORTCUT_LAUNCH_DEFAULT_GALLERY\npublic static final int SYSTEM_SHORTCUT_LAUNCH_DEFAULT_FILES\npublic static final int SYSTEM_SHORTCUT_LAUNCH_DEFAULT_WEATHER\npublic static final int SYSTEM_SHORTCUT_LAUNCH_DEFAULT_FITNESS\npublic static final int SYSTEM_SHORTCUT_LAUNCH_APPLICATION_BY_PACKAGE_NAME\npublic static final int SYSTEM_SHORTCUT_DESKTOP_MODE\npublic static final int SYSTEM_SHORTCUT_MULTI_WINDOW_NAVIGATION\nclass KeyboardSystemShortcut extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genToString=true, genEqualsHashCode=true)") + @Deprecated + private void __metadata() {} + + + //@formatter:on + // End of generated code + +} diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java index e79b8f3f52e6..de3984756416 100644 --- a/core/java/android/inputmethodservice/InputMethodService.java +++ b/core/java/android/inputmethodservice/InputMethodService.java @@ -524,19 +524,12 @@ public class InputMethodService extends AbstractInputMethodService { /** * @hide - * The IME is active and ready with views but set invisible. - * This flag cannot be combined with {@link #IME_VISIBLE}. - */ - public static final int IME_INVISIBLE = 0x4; - - /** - * @hide * The IME is visible, but not yet perceptible to the user (e.g. fading in) * by {@link android.view.WindowInsetsController}. * * @see InputMethodManager#reportPerceptible */ - public static final int IME_VISIBLE_IMPERCEPTIBLE = 0x8; + public static final int IME_VISIBLE_IMPERCEPTIBLE = 0x4; // Min and max values for back disposition. private static final int BACK_DISPOSITION_MIN = BACK_DISPOSITION_DEFAULT; @@ -3125,7 +3118,7 @@ public class InputMethodService extends AbstractInputMethodService { mInShowWindow = true; final int previousImeWindowStatus = (mDecorViewVisible ? IME_ACTIVE : 0) | (isInputViewShown() - ? (!mWindowVisible ? IME_INVISIBLE : IME_VISIBLE) : 0); + ? (!mWindowVisible ? -1 : IME_VISIBLE) : 0); startViews(prepareWindow(showInput)); final int nextImeWindowStatus = mapToImeWindowStatus(); if (previousImeWindowStatus != nextImeWindowStatus) { diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 5703f693792c..7ca40ea23d57 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -2287,6 +2287,26 @@ public final class Settings { "android.settings.MANAGE_MORE_DEFAULT_APPS_SETTINGS"; /** + * Activity Action: Show Other NFC services settings. + * <p> + * If a Settings activity handles this intent action, an "Other NFC services" entry will be + * shown in the Default payment app settings, and clicking it will launch that activity. + * <p> + * In some cases, a matching Activity may not exist, so ensure you safeguard against this. + * <p> + * Input: Nothing. + * <p> + * Output: Nothing. + * + * @hide + */ + @FlaggedApi(android.nfc.Flags.FLAG_NFC_ACTION_MANAGE_SERVICES_SETTINGS) + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + @SystemApi + public static final String ACTION_MANAGE_OTHER_NFC_SERVICES_SETTINGS = + "android.settings.MANAGE_OTHER_NFC_SERVICES_SETTINGS"; + + /** * Activity Action: Show app screen size list settings for user to override app aspect * ratio. * <p> @@ -12543,6 +12563,19 @@ public final class Settings { "launcher_taskbar_education_showing"; /** + * Whether any Compat UI Education is currently showing. + * + * <p>1 if true, 0 or unset otherwise. + * + * <p>This setting is used to inform other components that the Compat UI Education is + * currently showing, which can prevent them from showing something else to the user. + * + * @hide + */ + public static final String COMPAT_UI_EDUCATION_SHOWING = + "compat_ui_education_showing"; + + /** * Whether or not adaptive charging feature is enabled by user. * Type: int (0 for false, 1 for true) * Default: 1 @@ -20158,6 +20191,36 @@ public final class Settings { */ public static final int PHONE_SWITCHING_STATUS_IN_PROGRESS_MIGRATION_SUCCESS = 11; + /** + * Phone switching request source + * @hide + */ + public static final String PHONE_SWITCHING_REQUEST_SOURCE = + "phone_switching_request_source"; + + /** + * No phone switching request source + * @hide + */ + public static final int PHONE_SWITCHING_REQUEST_SOURCE_NONE = 0; + + /** + * Phone switching triggered by watch + * @hide + */ + public static final int PHONE_SWITCHING_REQUEST_SOURCE_WATCH = 1; + + /** + * Phone switching triggered by companion, user confirmation required + * @hide + */ + public static final int PHONE_SWITCHING_REQUEST_SOURCE_COMPANION_USER_CONFIRMATION = 2; + + /** + * Phone switching triggered by companion, user confirmation not required + * @hide + */ + public static final int PHONE_SWITCHING_REQUEST_SOURCE_COMPANION = 3; /** * Whether the device has enabled the feature to reduce motion and animation @@ -20205,14 +20268,6 @@ public final class Settings { public static final int TETHERED_CONFIG_RESTRICTED = 3; /** - * Whether phone switching is supported. - * - * (0 = false, 1 = true) - * @hide - */ - public static final String PHONE_SWITCHING_SUPPORTED = "phone_switching_supported"; - - /** * Setting indicating the name of the Wear OS package that hosts the Media Controls UI. * * @hide diff --git a/core/java/android/provider/Telephony.java b/core/java/android/provider/Telephony.java index d019bad68cd5..6eaef78ff608 100644 --- a/core/java/android/provider/Telephony.java +++ b/core/java/android/provider/Telephony.java @@ -4985,6 +4985,16 @@ public final class Telephony { */ public static final String COLUMN_SATELLITE_ESOS_SUPPORTED = "satellite_esos_supported"; + /** + * TelephonyProvider column name for satellite provisioned status. The value of this + * column is set based on whether carrier roaming or OEM-enabled NB-IOT satellite service is + * provisioned or not. By default, it's disabled. + * + * @hide + */ + public static final String COLUMN_IS_SATELLITE_PROVISIONED_FOR_NON_IP_DATAGRAM = + "is_satellite_provisioned_for_non_ip_datagram"; + /** All columns in {@link SimInfo} table. */ private static final List<String> ALL_COLUMNS = List.of( COLUMN_UNIQUE_KEY_SUBSCRIPTION_ID, @@ -5061,7 +5071,8 @@ public final class Telephony { COLUMN_TRANSFER_STATUS, COLUMN_SATELLITE_ENTITLEMENT_STATUS, COLUMN_SATELLITE_ENTITLEMENT_PLMNS, - COLUMN_SATELLITE_ESOS_SUPPORTED + COLUMN_SATELLITE_ESOS_SUPPORTED, + COLUMN_IS_SATELLITE_PROVISIONED_FOR_NON_IP_DATAGRAM ); /** diff --git a/core/java/android/service/dreams/DreamOverlayService.java b/core/java/android/service/dreams/DreamOverlayService.java index 17d2790eac96..013ec5f35761 100644 --- a/core/java/android/service/dreams/DreamOverlayService.java +++ b/core/java/android/service/dreams/DreamOverlayService.java @@ -28,7 +28,9 @@ import android.os.RemoteException; import android.util.Log; import android.view.WindowManager; +import java.lang.ref.WeakReference; import java.util.concurrent.Executor; +import java.util.function.Consumer; /** @@ -52,43 +54,51 @@ public abstract class DreamOverlayService extends Service { // An {@link IDreamOverlayClient} implementation that identifies itself when forwarding // requests to the {@link DreamOverlayService} private static class OverlayClient extends IDreamOverlayClient.Stub { - private final DreamOverlayService mService; + private final WeakReference<DreamOverlayService> mService; private boolean mShowComplications; private ComponentName mDreamComponent; IDreamOverlayCallback mDreamOverlayCallback; - OverlayClient(DreamOverlayService service) { + OverlayClient(WeakReference<DreamOverlayService> service) { mService = service; } + private void applyToDream(Consumer<DreamOverlayService> consumer) { + final DreamOverlayService service = mService.get(); + + if (service != null) { + consumer.accept(service); + } + } + @Override public void startDream(WindowManager.LayoutParams params, IDreamOverlayCallback callback, String dreamComponent, boolean shouldShowComplications) throws RemoteException { mDreamComponent = ComponentName.unflattenFromString(dreamComponent); mShowComplications = shouldShowComplications; mDreamOverlayCallback = callback; - mService.startDream(this, params); + applyToDream(dreamOverlayService -> dreamOverlayService.startDream(this, params)); } @Override public void wakeUp() { - mService.wakeUp(this); + applyToDream(dreamOverlayService -> dreamOverlayService.wakeUp(this)); } @Override public void endDream() { - mService.endDream(this); + applyToDream(dreamOverlayService -> dreamOverlayService.endDream(this)); } @Override public void comeToFront() { - mService.comeToFront(this); + applyToDream(dreamOverlayService -> dreamOverlayService.comeToFront(this)); } @Override public void onWakeRequested() { if (Flags.dreamWakeRedirect()) { - mService.onWakeRequested(); + applyToDream(DreamOverlayService::onWakeRequested); } } @@ -161,17 +171,24 @@ public abstract class DreamOverlayService extends Service { }); } - private IDreamOverlay mDreamOverlay = new IDreamOverlay.Stub() { + private static class DreamOverlay extends IDreamOverlay.Stub { + private final WeakReference<DreamOverlayService> mService; + + DreamOverlay(DreamOverlayService service) { + mService = new WeakReference<>(service); + } + @Override public void getClient(IDreamOverlayClientCallback callback) { try { - callback.onDreamOverlayClient( - new OverlayClient(DreamOverlayService.this)); + callback.onDreamOverlayClient(new OverlayClient(mService)); } catch (RemoteException e) { Log.e(TAG, "could not send client to callback", e); } } - }; + } + + private final IDreamOverlay mDreamOverlay = new DreamOverlay(this); public DreamOverlayService() { } @@ -195,6 +212,12 @@ public abstract class DreamOverlayService extends Service { } } + @Override + public void onDestroy() { + mCurrentClient = null; + super.onDestroy(); + } + @Nullable @Override public final IBinder onBind(@NonNull Intent intent) { diff --git a/core/java/android/text/flags/flags.aconfig b/core/java/android/text/flags/flags.aconfig index 1b85191015a1..4d176f2939a8 100644 --- a/core/java/android/text/flags/flags.aconfig +++ b/core/java/android/text/flags/flags.aconfig @@ -266,4 +266,24 @@ flag { metadata { purpose: PURPOSE_BUGFIX } +} + +flag { + name: "handwriting_gesture_with_transformation" + namespace: "text" + description: "Fix handwriting gesture is not working when view has transformation." + bug: "342619429" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "typeface_cache_for_var_settings" + namespace: "text" + description: "Cache Typeface instance for font variation settings." + bug: "355462362" + metadata { + purpose: PURPOSE_BUGFIX + } }
\ No newline at end of file diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java index d83f34436b1b..7896cbde678a 100644 --- a/core/java/android/view/InsetsController.java +++ b/core/java/android/view/InsetsController.java @@ -685,9 +685,6 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation */ private @InsetsType int mCancelledForNewAnimationTypes; - private final Runnable mInvokeControllableInsetsChangedListeners = - this::invokeControllableInsetsChangedListeners; - private final InsetsState.OnTraverseCallbacks mRemoveGoneSources = new InsetsState.OnTraverseCallbacks() { @@ -2206,7 +2203,6 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation * @return The types that are now animating due to a listener invoking control/show/hide */ private @InsetsType int invokeControllableInsetsChangedListeners() { - mHandler.removeCallbacks(mInvokeControllableInsetsChangedListeners); mLastStartedAnimTypes = 0; @InsetsType int types = calculateControllableTypes(); int size = mControllableInsetsChangedListeners.size(); diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index b8a885e64acb..1c0700f69ab6 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -2820,6 +2820,12 @@ public final class ViewRootImpl implements ViewParent, if (mAttachInfo.mThreadedRenderer != null) { mAttachInfo.mThreadedRenderer.setSurfaceControl(null, null); } + + // Also reset the VRR relevant values. + mPreferredFrameRateCategory = FRAME_RATE_CATEGORY_DEFAULT; + mLastPreferredFrameRateCategory = FRAME_RATE_CATEGORY_DEFAULT; + mPreferredFrameRate = 0; + mLastPreferredFrameRate = 0; } /** diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index 70614fbb80bf..017e004a7f13 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -483,6 +483,11 @@ public interface WindowManager extends ViewManager { * @hide */ int TRANSIT_PREPARE_BACK_NAVIGATION = 13; + /** + * An Activity was going to be invisible from back navigation. + * @hide + */ + int TRANSIT_CLOSE_PREPARE_BACK_NAVIGATION = 14; /** * The first slot for custom transition types. Callers (like Shell) can make use of custom @@ -513,6 +518,7 @@ public interface WindowManager extends ViewManager { TRANSIT_WAKE, TRANSIT_SLEEP, TRANSIT_PREPARE_BACK_NAVIGATION, + TRANSIT_CLOSE_PREPARE_BACK_NAVIGATION, TRANSIT_FIRST_CUSTOM }) @Retention(RetentionPolicy.SOURCE) @@ -1927,6 +1933,7 @@ public interface WindowManager extends ViewManager { case TRANSIT_WAKE: return "WAKE"; case TRANSIT_SLEEP: return "SLEEP"; case TRANSIT_PREPARE_BACK_NAVIGATION: return "PREDICTIVE_BACK"; + case TRANSIT_CLOSE_PREPARE_BACK_NAVIGATION: return "CLOSE_PREDICTIVE_BACK"; case TRANSIT_FIRST_CUSTOM: return "FIRST_CUSTOM"; default: if (type > TRANSIT_FIRST_CUSTOM) { diff --git a/core/java/android/widget/Chronometer.java b/core/java/android/widget/Chronometer.java index 0b67cad0112b..9931aea41913 100644 --- a/core/java/android/widget/Chronometer.java +++ b/core/java/android/widget/Chronometer.java @@ -328,7 +328,7 @@ public class Chronometer extends TextView { if (running) { updateText(SystemClock.elapsedRealtime()); dispatchChronometerTick(); - postDelayed(mTickRunnable, 1000); + postTickOnNextSecond(); } else { removeCallbacks(mTickRunnable); } @@ -342,11 +342,17 @@ public class Chronometer extends TextView { if (mRunning) { updateText(SystemClock.elapsedRealtime()); dispatchChronometerTick(); - postDelayed(mTickRunnable, 1000); + postTickOnNextSecond(); } } }; + private void postTickOnNextSecond() { + long nowMillis = SystemClock.elapsedRealtime(); + int millis = (int) ((nowMillis - mBase) % 1000); + postDelayed(mTickRunnable, 1000 - millis); + } + void dispatchChronometerTick() { if (mOnChronometerTickListener != null) { mOnChronometerTickListener.onChronometerTick(this); diff --git a/core/java/android/widget/CompoundButton.java b/core/java/android/widget/CompoundButton.java index 63f8ee7528f2..ed6ec32fca25 100644 --- a/core/java/android/widget/CompoundButton.java +++ b/core/java/android/widget/CompoundButton.java @@ -267,7 +267,7 @@ public abstract class CompoundButton extends Button implements Checkable { * @param buttonView The compound button view whose state has changed. * @param isChecked The new checked state of buttonView. */ - void onCheckedChanged(CompoundButton buttonView, boolean isChecked); + void onCheckedChanged(@NonNull CompoundButton buttonView, boolean isChecked); } /** diff --git a/core/java/android/widget/RadioGroup.java b/core/java/android/widget/RadioGroup.java index d445fdc01564..70fe6d5b5c9c 100644 --- a/core/java/android/widget/RadioGroup.java +++ b/core/java/android/widget/RadioGroup.java @@ -366,7 +366,7 @@ public class RadioGroup extends LinearLayout { * @param group the group in which the checked radio button has changed * @param checkedId the unique identifier of the newly checked radio button */ - public void onCheckedChanged(RadioGroup group, @IdRes int checkedId); + void onCheckedChanged(@NonNull RadioGroup group, @IdRes int checkedId); } private class CheckedStateTracker implements CompoundButton.OnCheckedChangeListener { diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index ac899f4c2b5e..61ecc6264ffa 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -28,10 +28,10 @@ import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_TEXT_C import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY; import static android.view.inputmethod.CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION; import static android.view.inputmethod.EditorInfo.STYLUS_HANDWRITING_ENABLED_ANDROIDX_EXTRAS_KEY; +import static android.view.inputmethod.Flags.initiationWithoutInputConnection; import static com.android.text.flags.Flags.FLAG_FIX_LINE_HEIGHT_FOR_LOCALE; import static com.android.text.flags.Flags.FLAG_USE_BOUNDS_FOR_WIDTH; -import static android.view.inputmethod.Flags.initiationWithoutInputConnection; import android.R; import android.annotation.CallSuper; @@ -937,6 +937,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private TextPaint mTempTextPaint; private Object mTempCursor; + private Matrix mTempMatrix; @UnsupportedAppUsage private BoringLayout.Metrics mBoring; @@ -12106,6 +12107,22 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } private PointF convertFromScreenToContentCoordinates(PointF point) { + if (Flags.handwritingGestureWithTransformation()) { + if (mTempMatrix == null) { + mTempMatrix = new Matrix(); + } + Matrix matrix = mTempMatrix; + matrix.reset(); + transformMatrixToLocal(matrix); + matrix.postTranslate( + -viewportToContentHorizontalOffset(), + -viewportToContentVerticalOffset() + ); + + float[] copy = new float[] { point.x, point.y }; + matrix.mapPoints(copy); + return new PointF(copy[0], copy[1]); + } int[] screenToViewport = getLocationOnScreen(); PointF copy = new PointF(point); copy.offset( @@ -12115,6 +12132,22 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } private RectF convertFromScreenToContentCoordinates(RectF rect) { + if (Flags.handwritingGestureWithTransformation()) { + if (mTempMatrix == null) { + mTempMatrix = new Matrix(); + } + Matrix matrix = mTempMatrix; + matrix.reset(); + transformMatrixToLocal(matrix); + matrix.postTranslate( + -viewportToContentHorizontalOffset(), + -viewportToContentVerticalOffset() + ); + + RectF copy = new RectF(rect); + matrix.mapRect(copy); + return copy; + } int[] screenToViewport = getLocationOnScreen(); RectF copy = new RectF(rect); copy.offset( @@ -14279,6 +14312,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } /** + * Don't use, it returns wrong result when the view is scaled. This method can be removed once + * Flags.handwritingGestureWithTransformation is enabled. + * Assume * Helper method to set {@code rect} to this TextView's non-clipped area in its own coordinates. * This method obtains the view's visible rectangle whereas the method * {@link #getContentVisibleRect} returns the text layout's visible rectangle. @@ -14299,6 +14335,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } /** + * Don't use, it returns wrong result when view is scaled. This method can be removed once + * Flags.handwritingGestureWithTransformation is enabled. * Helper method to set {@code rect} to the text content's non-clipped area in the view's * coordinates. * @@ -14314,6 +14352,58 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener getWidth() - getCompoundPaddingRight(), getHeight() - getCompoundPaddingBottom()); } + private boolean getEditorAndHandwritingBounds(@NonNull RectF editorBounds, + @Nullable RectF handwritingBounds) { + if (mTempRect == null) { + mTempRect = new Rect(); + } + Rect rect = mTempRect; + if (!getGlobalVisibleRect(rect)) { + return false; + } + if (mTempMatrix == null) { + mTempMatrix = new Matrix(); + } + + Matrix matrix = mTempMatrix; + matrix.reset(); + transformMatrixToLocal(matrix); + editorBounds.set(rect); + // When the view has transformations like scaleX/scaleY computing the global visible + // rectangle will already apply the transformations. The getLocalVisibleRect only offsets + // the global rectangle to local. And the result is wrong the View is scaled. + // + // This approach use the local transformation matrix to map the global rectangle to + // local instead. + // + // Note: it doesn't work well with rotation. Because Rect must be + // axis-aligned, when a rotated Rect becomes quadrilateral, the quadrilateral's + // bounding box is stored at Rect instead. It makes the returned Rect larger than + // the correct size. + matrix.mapRect(editorBounds); + + if (handwritingBounds != null) { + // Similar to editorBounds, handwritingBounds must be computed in global coordinates + // and then converted back to local coordinates. Otherwise, if the view is scaled, + // the handwritingBoundsOffsets are also scaled, which is not the expected behavior. + handwritingBounds.top = rect.top - getHandwritingBoundsOffsetTop(); + handwritingBounds.left = rect.left - getHandwritingBoundsOffsetLeft(); + handwritingBounds.bottom = rect.bottom + getHandwritingBoundsOffsetBottom(); + handwritingBounds.right = rect.right + getHandwritingBoundsOffsetRight(); + matrix.mapRect(handwritingBounds); + } + return true; + } + + private boolean getContentVisibleRect(RectF rect) { + if (!getEditorAndHandwritingBounds(rect, /* handwritingBounds= */null)) { + return false; + } + // Clip the view's visible rect with the text layout's visible rect. + return rect.intersect(getCompoundPaddingLeft(), getCompoundPaddingTop(), + getWidth() - getCompoundPaddingRight(), getHeight() - getCompoundPaddingBottom()); + } + /** * Populate requested character bounds in a {@link CursorAnchorInfo.Builder} * @@ -14333,9 +14423,15 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // character bounds in this case yet. return; } - final Rect rect = new Rect(); - getContentVisibleRect(rect); - final RectF visibleRect = new RectF(rect); + final RectF visibleRect = new RectF(); + + if (Flags.handwritingGestureWithTransformation()) { + getContentVisibleRect(visibleRect); + } else { + final Rect rect = new Rect(); + getContentVisibleRect(rect); + visibleRect.set(rect); + } final float[] characterBounds = getCharacterBounds(startIndex, endIndex, viewportToContentHorizontalOffset, viewportToContentVerticalOffset); @@ -14438,24 +14534,26 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener builder.setMatrix(viewToScreenMatrix); if (includeEditorBounds) { - if (mTempRect == null) { - mTempRect = new Rect(); - } - final Rect bounds = mTempRect; - final RectF editorBounds; - final RectF handwritingBounds; - if (getViewVisibleRect(bounds)) { - editorBounds = new RectF(bounds); - handwritingBounds = new RectF(editorBounds); - handwritingBounds.top -= getHandwritingBoundsOffsetTop(); - handwritingBounds.left -= getHandwritingBoundsOffsetLeft(); - handwritingBounds.bottom += getHandwritingBoundsOffsetBottom(); - handwritingBounds.right += getHandwritingBoundsOffsetRight(); + final RectF editorBounds = new RectF(); + final RectF handwritingBounds = new RectF(); + if (Flags.handwritingGestureWithTransformation()) { + getEditorAndHandwritingBounds(editorBounds, handwritingBounds); } else { - // The editor is not visible at all, return empty rectangles. We still need to + if (mTempRect == null) { + mTempRect = new Rect(); + } + final Rect bounds = mTempRect; + + // If the editor is not visible at all, return empty rectangles. We still need to // return an EditorBoundsInfo because IME has subscribed the EditorBoundsInfo. - editorBounds = new RectF(); - handwritingBounds = new RectF(); + if (getViewVisibleRect(bounds)) { + editorBounds.set(bounds); + handwritingBounds.set(editorBounds); + handwritingBounds.top -= getHandwritingBoundsOffsetTop(); + handwritingBounds.left -= getHandwritingBoundsOffsetLeft(); + handwritingBounds.bottom += getHandwritingBoundsOffsetBottom(); + handwritingBounds.right += getHandwritingBoundsOffsetRight(); + } } EditorBoundsInfo.Builder boundsBuilder = new EditorBoundsInfo.Builder(); EditorBoundsInfo editorBoundsInfo = boundsBuilder.setEditorBounds(editorBounds) @@ -14533,29 +14631,57 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } if (includeVisibleLineBounds) { - final Rect visibleRect = new Rect(); - if (getContentVisibleRect(visibleRect)) { - // Subtract the viewportToContentVerticalOffset to convert the view - // coordinates to layout coordinates. - final float visibleTop = - visibleRect.top - viewportToContentVerticalOffset; - final float visibleBottom = - visibleRect.bottom - viewportToContentVerticalOffset; - final int firstLine = - layout.getLineForVertical((int) Math.floor(visibleTop)); - final int lastLine = - layout.getLineForVertical((int) Math.ceil(visibleBottom)); - - for (int line = firstLine; line <= lastLine; ++line) { - final float left = layout.getLineLeft(line) - + viewportToContentHorizontalOffset; - final float top = layout.getLineTop(line) - + viewportToContentVerticalOffset; - final float right = layout.getLineRight(line) - + viewportToContentHorizontalOffset; - final float bottom = layout.getLineBottom(line, false) - + viewportToContentVerticalOffset; - builder.addVisibleLineBounds(left, top, right, bottom); + if (Flags.handwritingGestureWithTransformation()) { + RectF visibleRect = new RectF(); + if (getContentVisibleRect(visibleRect)) { + // Subtract the viewportToContentVerticalOffset to convert the view + // coordinates to layout coordinates. + final float visibleTop = + visibleRect.top - viewportToContentVerticalOffset; + final float visibleBottom = + visibleRect.bottom - viewportToContentVerticalOffset; + final int firstLine = + layout.getLineForVertical((int) Math.floor(visibleTop)); + final int lastLine = + layout.getLineForVertical((int) Math.ceil(visibleBottom)); + + for (int line = firstLine; line <= lastLine; ++line) { + final float left = layout.getLineLeft(line) + + viewportToContentHorizontalOffset; + final float top = layout.getLineTop(line) + + viewportToContentVerticalOffset; + final float right = layout.getLineRight(line) + + viewportToContentHorizontalOffset; + final float bottom = layout.getLineBottom(line, false) + + viewportToContentVerticalOffset; + builder.addVisibleLineBounds(left, top, right, bottom); + } + } + } else { + final Rect visibleRect = new Rect(); + if (getContentVisibleRect(visibleRect)) { + // Subtract the viewportToContentVerticalOffset to convert the view + // coordinates to layout coordinates. + final float visibleTop = + visibleRect.top - viewportToContentVerticalOffset; + final float visibleBottom = + visibleRect.bottom - viewportToContentVerticalOffset; + final int firstLine = + layout.getLineForVertical((int) Math.floor(visibleTop)); + final int lastLine = + layout.getLineForVertical((int) Math.ceil(visibleBottom)); + + for (int line = firstLine; line <= lastLine; ++line) { + final float left = layout.getLineLeft(line) + + viewportToContentHorizontalOffset; + final float top = layout.getLineTop(line) + + viewportToContentVerticalOffset; + final float right = layout.getLineRight(line) + + viewportToContentHorizontalOffset; + final float bottom = layout.getLineBottom(line, false) + + viewportToContentVerticalOffset; + builder.addVisibleLineBounds(left, top, right, bottom); + } } } } diff --git a/core/java/android/window/WindowContainerTransaction.java b/core/java/android/window/WindowContainerTransaction.java index e9d77f8aaf80..314bf8985cb4 100644 --- a/core/java/android/window/WindowContainerTransaction.java +++ b/core/java/android/window/WindowContainerTransaction.java @@ -700,6 +700,18 @@ public final class WindowContainerTransaction implements Parcelable { } /** + * Restore the back navigation target from visible to invisible for canceling gesture animation. + * @hide + */ + @NonNull + public WindowContainerTransaction restoreBackNavi() { + final HierarchyOp hierarchyOp = + new HierarchyOp.Builder(HierarchyOp.HIERARCHY_OP_TYPE_RESTORE_BACK_NAVIGATION) + .build(); + mHierarchyOps.add(hierarchyOp); + return this; + } + /** * Adds a given {@code Rect} as an insets source frame on the {@code receiver}. * * @param receiver The window container that the insets source is added to. @@ -1436,6 +1448,7 @@ public final class WindowContainerTransaction implements Parcelable { public static final int HIERARCHY_OP_TYPE_ADD_TASK_FRAGMENT_OPERATION = 17; public static final int HIERARCHY_OP_TYPE_MOVE_PIP_ACTIVITY_TO_PINNED_TASK = 18; public static final int HIERARCHY_OP_TYPE_SET_IS_TRIMMABLE = 19; + public static final int HIERARCHY_OP_TYPE_RESTORE_BACK_NAVIGATION = 20; // The following key(s) are for use with mLaunchOptions: // When launching a task (eg. from recents), this is the taskId to be launched. diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig index d5746e58ffe6..9aeccf4c3d9b 100644 --- a/core/java/android/window/flags/windowing_frontend.aconfig +++ b/core/java/android/window/flags/windowing_frontend.aconfig @@ -65,6 +65,14 @@ flag { } flag { + name: "keyguard_going_away_timeout" + namespace: "windowing_frontend" + description: "Allow a maximum of 10 seconds with keyguardGoingAway=true before force-resetting" + bug: "343598832" + is_fixed_read_only: true +} + +flag { name: "close_to_square_config_includes_status_bar" namespace: "windowing_frontend" description: "On close to square display, when necessary, configuration includes status bar" @@ -72,6 +80,17 @@ flag { } flag { + name: "reduce_keyguard_transitions" + namespace: "windowing_frontend" + description: "Avoid setting keyguard transitions ready unless there are no other changes" + bug: "354647472" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "transit_ready_tracking" namespace: "windowing_frontend" description: "Enable accurate transition readiness tracking" diff --git a/core/java/android/window/flags/windowing_sdk.aconfig b/core/java/android/window/flags/windowing_sdk.aconfig index b8c2a5f8eb6b..a6ae948604a5 100644 --- a/core/java/android/window/flags/windowing_sdk.aconfig +++ b/core/java/android/window/flags/windowing_sdk.aconfig @@ -19,13 +19,6 @@ flag { flag { namespace: "windowing_sdk" - name: "fullscreen_dim_flag" - description: "Whether to allow showing fullscreen dim on ActivityEmbedding split" - bug: "293797706" -} - -flag { - namespace: "windowing_sdk" name: "activity_embedding_interactive_divider_flag" description: "Whether the interactive divider feature is enabled" bug: "293654166" diff --git a/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java b/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java index 2daf0fd1f61c..921363c3e5af 100644 --- a/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java +++ b/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java @@ -108,7 +108,6 @@ public final class InputMethodPrivilegedOperations { * @param backDisposition disposition flags * @see android.inputmethodservice.InputMethodService#IME_ACTIVE * @see android.inputmethodservice.InputMethodService#IME_VISIBLE - * @see android.inputmethodservice.InputMethodService#IME_INVISIBLE * @see android.inputmethodservice.InputMethodService#BACK_DISPOSITION_DEFAULT * @see android.inputmethodservice.InputMethodService#BACK_DISPOSITION_ADJUST_NOTHING */ diff --git a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java index fbec1f104fc8..e0c90d83768c 100644 --- a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java +++ b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java @@ -332,6 +332,8 @@ public class PerfettoProtoLogImpl extends IProtoLogClient.Stub implements IProto } private void onTracingFlush() { + Log.d(LOG_TAG, "Executing onTracingFlush"); + final ExecutorService loggingService; try { mBackgroundServiceLock.lock(); @@ -352,15 +354,19 @@ public class PerfettoProtoLogImpl extends IProtoLogClient.Stub implements IProto Log.e(LOG_TAG, "Failed to wait for tracing to finish", e); } - dumpTransitionTraceConfig(); + dumpViewerConfig(); + + Log.d(LOG_TAG, "Finished onTracingFlush"); } - private void dumpTransitionTraceConfig() { + private void dumpViewerConfig() { if (mViewerConfigInputStreamProvider == null) { // No viewer config available return; } + Log.d(LOG_TAG, "Dumping viewer config to trace"); + ProtoInputStream pis = mViewerConfigInputStreamProvider.getInputStream(); if (pis == null) { @@ -390,6 +396,8 @@ public class PerfettoProtoLogImpl extends IProtoLogClient.Stub implements IProto Log.e(LOG_TAG, "Failed to read ProtoLog viewer config to dump on tracing end", e); } }); + + Log.d(LOG_TAG, "Dumped viewer config to trace"); } private static void writeViewerConfigGroup( @@ -770,6 +778,8 @@ public class PerfettoProtoLogImpl extends IProtoLogClient.Stub implements IProto private synchronized void onTracingInstanceStart( int instanceIdx, ProtoLogDataSource.ProtoLogConfig config) { + Log.d(LOG_TAG, "Executing onTracingInstanceStart"); + final LogLevel defaultLogFrom = config.getDefaultGroupConfig().logFrom; for (int i = defaultLogFrom.ordinal(); i < LogLevel.values().length; i++) { mDefaultLogLevelCounts[i]++; @@ -800,10 +810,13 @@ public class PerfettoProtoLogImpl extends IProtoLogClient.Stub implements IProto mCacheUpdater.run(); this.mTracingInstances.incrementAndGet(); + + Log.d(LOG_TAG, "Finished onTracingInstanceStart"); } private synchronized void onTracingInstanceStop( int instanceIdx, ProtoLogDataSource.ProtoLogConfig config) { + Log.d(LOG_TAG, "Executing onTracingInstanceStop"); this.mTracingInstances.decrementAndGet(); final LogLevel defaultLogFrom = config.getDefaultGroupConfig().logFrom; @@ -835,6 +848,7 @@ public class PerfettoProtoLogImpl extends IProtoLogClient.Stub implements IProto } mCacheUpdater.run(); + Log.d(LOG_TAG, "Finished onTracingInstanceStop"); } private static void logAndPrintln(@Nullable PrintWriter pw, String msg) { diff --git a/core/java/com/android/internal/protolog/ProtoLogCommandHandler.java b/core/java/com/android/internal/protolog/ProtoLogCommandHandler.java new file mode 100644 index 000000000000..3dab2e39b852 --- /dev/null +++ b/core/java/com/android/internal/protolog/ProtoLogCommandHandler.java @@ -0,0 +1,172 @@ +/* + * 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.internal.protolog; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.ShellCommand; + +import com.android.internal.annotations.VisibleForTesting; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +public class ProtoLogCommandHandler extends ShellCommand { + @NonNull + private final ProtoLogService mProtoLogService; + @Nullable + private final PrintWriter mPrintWriter; + + public ProtoLogCommandHandler(@NonNull ProtoLogService protoLogService) { + this(protoLogService, null); + } + + @VisibleForTesting + public ProtoLogCommandHandler( + @NonNull ProtoLogService protoLogService, @Nullable PrintWriter printWriter) { + this.mProtoLogService = protoLogService; + this.mPrintWriter = printWriter; + } + + @Override + public int onCommand(String cmd) { + if (cmd == null) { + onHelp(); + return 0; + } + + return switch (cmd) { + case "groups" -> handleGroupsCommands(getNextArg()); + case "logcat" -> handleLogcatCommands(getNextArg()); + default -> handleDefaultCommands(cmd); + }; + } + + @Override + public void onHelp() { + PrintWriter pw = getOutPrintWriter(); + pw.println("ProtoLog commands:"); + pw.println(" help"); + pw.println(" Print this help text."); + pw.println(); + pw.println(" groups (list | status)"); + pw.println(" list - lists all ProtoLog groups registered with ProtoLog service"); + pw.println(" status <group> - print the status of a ProtoLog group"); + pw.println(); + pw.println(" logcat (enable | disable) <group>"); + pw.println(" enable or disable ProtoLog to logcat"); + pw.println(); + } + + @NonNull + @Override + public PrintWriter getOutPrintWriter() { + if (mPrintWriter != null) { + return mPrintWriter; + } + + return super.getOutPrintWriter(); + } + + private int handleGroupsCommands(@Nullable String cmd) { + PrintWriter pw = getOutPrintWriter(); + + if (cmd == null) { + pw.println("Incomplete command. Use 'cmd protolog help' for guidance."); + return 0; + } + + switch (cmd) { + case "list": { + final String[] availableGroups = mProtoLogService.getGroups(); + if (availableGroups.length == 0) { + pw.println("No ProtoLog groups registered with ProtoLog service."); + return 0; + } + + pw.println("ProtoLog groups registered with service:"); + for (String group : availableGroups) { + pw.println("- " + group); + } + + return 0; + } + case "status": { + final String group = getNextArg(); + + if (group == null) { + pw.println("Incomplete command. Use 'cmd protolog help' for guidance."); + return 0; + } + + pw.println("ProtoLog group " + group + "'s status:"); + + if (!Set.of(mProtoLogService.getGroups()).contains(group)) { + pw.println("UNREGISTERED"); + return 0; + } + + pw.println("LOG_TO_LOGCAT = " + mProtoLogService.isLoggingToLogcat(group)); + return 0; + } + default: { + pw.println("Unknown command: " + cmd); + return -1; + } + } + } + + private int handleLogcatCommands(@Nullable String cmd) { + PrintWriter pw = getOutPrintWriter(); + + if (cmd == null || peekNextArg() == null) { + pw.println("Incomplete command. Use 'cmd protolog help' for guidance."); + return 0; + } + + switch (cmd) { + case "enable" -> { + mProtoLogService.enableProtoLogToLogcat(processGroups()); + return 0; + } + case "disable" -> { + mProtoLogService.disableProtoLogToLogcat(processGroups()); + return 0; + } + default -> { + pw.println("Unknown command: " + cmd); + return -1; + } + } + } + + @NonNull + private String[] processGroups() { + if (getRemainingArgsCount() == 0) { + return mProtoLogService.getGroups(); + } + + final List<String> groups = new ArrayList<>(); + while (getRemainingArgsCount() > 0) { + groups.add(getNextArg()); + } + + return groups.toArray(new String[0]); + } +} diff --git a/core/java/com/android/internal/protolog/ProtoLogService.java b/core/java/com/android/internal/protolog/ProtoLogService.java new file mode 100644 index 000000000000..2333a062d897 --- /dev/null +++ b/core/java/com/android/internal/protolog/ProtoLogService.java @@ -0,0 +1,458 @@ +/* + * 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.internal.protolog; + +import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.GROUPS; +import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.Group.ID; +import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.Group.NAME; +import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.Group.TAG; +import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MESSAGES; +import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.GROUP_ID; +import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.LEVEL; +import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.MESSAGE; +import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.MESSAGE_ID; +import static android.internal.perfetto.protos.TracePacketOuterClass.TracePacket.PROTOLOG_VIEWER_CONFIG; +import static android.internal.perfetto.protos.TracePacketOuterClass.TracePacket.TIMESTAMP; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemService; +import android.content.Context; +import android.os.RemoteException; +import android.os.ResultReceiver; +import android.os.ShellCallback; +import android.os.SystemClock; +import android.tracing.perfetto.DataSourceParams; +import android.tracing.perfetto.InitArguments; +import android.tracing.perfetto.Producer; +import android.util.Log; +import android.util.proto.ProtoInputStream; +import android.util.proto.ProtoOutputStream; + +import com.android.internal.annotations.VisibleForTesting; + +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +/** + * The ProtoLog service is responsible for orchestrating centralized actions of the protolog tracing + * system. Currently this service has the following roles: + * - Handle shell commands to toggle logging ProtoLog messages for specified groups to logcat. + * - Handle viewer config dumping (the mapping from message hash to message string) for all protolog + * clients. This is for two reasons: firstly, because client processes might be frozen so might + * not response to the request to dump their viewer config when the trace is stopped; secondly, + * multiple processes might be running the same code with the same viewer config, this centralized + * service ensures we don't dump the same viewer config multiple times across processes. + * <p> + * {@link com.android.internal.protolog.IProtoLogClient ProtoLog clients} register themselves to + * this service on initialization. + * <p> + * This service is intended to run on the system server, such that it never gets frozen. + */ +@SystemService(Context.PROTOLOG_SERVICE) +public final class ProtoLogService extends IProtoLogService.Stub { + private static final String LOG_TAG = "ProtoLogService"; + + private final ProtoLogDataSource mDataSource = new ProtoLogDataSource( + this::onTracingInstanceStart, + this::onTracingInstanceFlush, + this::onTracingInstanceStop + ); + + /** + * Keeps track of how many of each viewer config file is currently registered. + * Use to keep track of which viewer config files are actively being used in tracing and might + * need to be dumped on flush. + */ + private final Map<String, Integer> mConfigFileCounts = new HashMap<>(); + /** + * Keeps track of the viewer config file of each client if available. + */ + private final Map<IProtoLogClient, String> mClientConfigFiles = new HashMap<>(); + + /** + * Keeps track of all the protolog groups that have been registered by clients and are still + * being actively traced. + */ + private final Set<String> mRegisteredGroups = new HashSet<>(); + /** + * Keeps track of all the clients that are actively tracing a given protolog group. + */ + private final Map<String, Set<IProtoLogClient>> mGroupToClients = new HashMap<>(); + + /** + * Keeps track of whether or not a given group should be logged to logcat. + * True when logging to logcat, false otherwise. + */ + private final Map<String, Boolean> mLogGroupToLogcatStatus = new TreeMap<>(); + + /** + * Keeps track of all the tracing instance ids that are actively running for ProtoLog. + */ + private final Set<Integer> mRunningInstances = new HashSet<>(); + + private final ViewerConfigFileTracer mViewerConfigFileTracer; + + public ProtoLogService() { + this(ProtoLogService::dumpTransitionTraceConfig); + } + + @VisibleForTesting + public ProtoLogService(@NonNull ViewerConfigFileTracer tracer) { + // Initialize the Perfetto producer and register the Perfetto ProtoLog datasource to be + // receive the lifecycle callbacks of the datasource and write the viewer configs if and + // when required to the datasource. + Producer.init(InitArguments.DEFAULTS); + final var params = new DataSourceParams.Builder() + .setBufferExhaustedPolicy(DataSourceParams.PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_DROP) + .build(); + mDataSource.register(params); + + mViewerConfigFileTracer = tracer; + } + + public static class RegisterClientArgs extends IRegisterClientArgs.Stub { + /** + * The viewer config file to be registered for this client ProtoLog process. + */ + @Nullable + private String mViewerConfigFile = null; + /** + * The list of all groups that this client protolog process supports and might trace. + */ + @NonNull + private String[] mGroups = new String[0]; + /** + * The default logcat status of the ProtoLog client. True is logging to logcat, false + * otherwise. The indices should match the indices in {@link mGroups}. + */ + @NonNull + private boolean[] mLogcatStatus = new boolean[0]; + + public record GroupConfig(@NonNull String group, boolean logToLogcat) {} + + /** + * Specify groups to register with this client that will be used for protologging in this + * process. + * @param groups to register with this client. + * @return self + */ + public RegisterClientArgs setGroups(GroupConfig... groups) { + mGroups = new String[groups.length]; + mLogcatStatus = new boolean[groups.length]; + + for (int i = 0; i < groups.length; i++) { + mGroups[i] = groups[i].group; + mLogcatStatus[i] = groups[i].logToLogcat; + } + + return this; + } + + /** + * Set the viewer config file that the logs in this process are using. + * @param viewerConfigFile The file path of the viewer config. + * @return self + */ + public RegisterClientArgs setViewerConfigFile(@NonNull String viewerConfigFile) { + mViewerConfigFile = viewerConfigFile; + + return this; + } + + @Override + @NonNull + public String[] getGroups() { + return mGroups; + } + + @Override + @NonNull + public boolean[] getGroupsDefaultLogcatStatus() { + return mLogcatStatus; + } + + @Nullable + @Override + public String getViewerConfigFile() { + return mViewerConfigFile; + } + } + + @FunctionalInterface + public interface ViewerConfigFileTracer { + /** + * Write the viewer config data to the trace buffer. + * + * @param dataSource The target datasource to write the viewer config to. + * @param viewerConfigFilePath The path of the viewer config file which contains the data we + * want to write to the trace buffer. + * @throws FileNotFoundException if the viewerConfigFilePath is invalid. + */ + void trace(@NonNull ProtoLogDataSource dataSource, @NonNull String viewerConfigFilePath) + throws FileNotFoundException; + } + + @Override + public void registerClient(@NonNull IProtoLogClient client, @NonNull IRegisterClientArgs args) + throws RemoteException { + client.asBinder().linkToDeath(() -> onClientBinderDeath(client), /* flags */ 0); + + final String viewerConfigFile = args.getViewerConfigFile(); + if (viewerConfigFile != null) { + registerViewerConfigFile(client, viewerConfigFile); + } + + registerGroups(client, args.getGroups(), args.getGroupsDefaultLogcatStatus()); + } + + @Override + public void onShellCommand(@Nullable FileDescriptor in, @Nullable FileDescriptor out, + @Nullable FileDescriptor err, @NonNull String[] args, @Nullable ShellCallback callback, + @NonNull ResultReceiver resultReceiver) throws RemoteException { + new ProtoLogCommandHandler(this) + .exec(this, in, out, err, args, callback, resultReceiver); + } + + /** + * Get the list of groups clients have registered to the protolog service. + * @return The list of ProtoLog groups registered with this service. + */ + @NonNull + public String[] getGroups() { + return mRegisteredGroups.toArray(new String[0]); + } + + /** + * Enable logging target groups to logcat. + * @param groups we want to enable logging them to logcat for. + */ + public void enableProtoLogToLogcat(String... groups) { + toggleProtoLogToLogcat(true, groups); + } + + /** + * Disable logging target groups to logcat. + * @param groups we want to disable from being logged to logcat. + */ + public void disableProtoLogToLogcat(String... groups) { + toggleProtoLogToLogcat(false, groups); + } + + /** + * Check if a group is logging to logcat + * @param group The group we want to check for + * @return True iff we are logging this group to logcat. + */ + public boolean isLoggingToLogcat(@NonNull String group) { + final Boolean isLoggingToLogcat = mLogGroupToLogcatStatus.get(group); + + if (isLoggingToLogcat == null) { + throw new RuntimeException( + "Trying to get logcat logging status of non-registered group " + group); + } + + return isLoggingToLogcat; + } + + private void registerViewerConfigFile( + @NonNull IProtoLogClient client, @NonNull String viewerConfigFile) { + final var count = mConfigFileCounts.getOrDefault(viewerConfigFile, 0); + mConfigFileCounts.put(viewerConfigFile, count + 1); + mClientConfigFiles.put(client, viewerConfigFile); + } + + private void registerGroups(@NonNull IProtoLogClient client, @NonNull String[] groups, + @NonNull boolean[] logcatStatuses) throws RemoteException { + if (groups.length != logcatStatuses.length) { + throw new RuntimeException( + "Expected groups and logcatStatuses to have the same length, " + + "but groups has length " + groups.length + + " and logcatStatuses has length " + logcatStatuses.length); + } + + for (int i = 0; i < groups.length; i++) { + String group = groups[i]; + boolean logcatStatus = logcatStatuses[i]; + + mRegisteredGroups.add(group); + + mGroupToClients.putIfAbsent(group, new HashSet<>()); + mGroupToClients.get(group).add(client); + + if (!mLogGroupToLogcatStatus.containsKey(group)) { + mLogGroupToLogcatStatus.put(group, logcatStatus); + } + + boolean requestedLogToLogcat = mLogGroupToLogcatStatus.get(group); + if (requestedLogToLogcat != logcatStatus) { + client.toggleLogcat(requestedLogToLogcat, new String[] { group }); + } + } + } + + private void toggleProtoLogToLogcat(boolean enabled, @NonNull String[] groups) { + final var clientToGroups = new HashMap<IProtoLogClient, Set<String>>(); + + for (String group : groups) { + final var clients = mGroupToClients.get(group); + + if (clients == null) { + // No clients associated to this group + Log.w(LOG_TAG, "Attempting to toggle log to logcat for group " + group + + " with no registered clients."); + continue; + } + + for (IProtoLogClient client : clients) { + clientToGroups.putIfAbsent(client, new HashSet<>()); + clientToGroups.get(client).add(group); + } + } + + for (IProtoLogClient client : clientToGroups.keySet()) { + try { + client.toggleLogcat(enabled, clientToGroups.get(client).toArray(new String[0])); + } catch (RemoteException e) { + throw new RuntimeException( + "Failed to toggle logcat status for groups on client", e); + } + } + + for (String group : groups) { + mLogGroupToLogcatStatus.put(group, enabled); + } + } + + private void onTracingInstanceStart(int instanceIdx, ProtoLogDataSource.ProtoLogConfig config) { + mRunningInstances.add(instanceIdx); + } + + private void onTracingInstanceFlush() { + for (String fileName : mConfigFileCounts.keySet()) { + try { + mViewerConfigFileTracer.trace(mDataSource, fileName); + } catch (FileNotFoundException e) { + throw new RuntimeException(e); + } + } + } + + private void onTracingInstanceStop(int instanceIdx, ProtoLogDataSource.ProtoLogConfig config) { + mRunningInstances.remove(instanceIdx); + } + + private static void dumpTransitionTraceConfig(@NonNull ProtoLogDataSource dataSource, + @NonNull String viewerConfigFilePath) throws FileNotFoundException { + final var pis = new ProtoInputStream(new FileInputStream(viewerConfigFilePath)); + + dataSource.trace(ctx -> { + try { + final ProtoOutputStream os = ctx.newTracePacket(); + + os.write(TIMESTAMP, SystemClock.elapsedRealtimeNanos()); + + final long outProtologViewerConfigToken = os.start(PROTOLOG_VIEWER_CONFIG); + while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pis.getFieldNumber()) { + case (int) MESSAGES -> writeViewerConfigMessage(pis, os); + case (int) GROUPS -> writeViewerConfigGroup(pis, os); + } + } + + os.end(outProtologViewerConfigToken); + } catch (IOException e) { + Log.e(LOG_TAG, "Failed to read ProtoLog viewer config to dump on tracing end", e); + } + }); + } + + private void onClientBinderDeath(@NonNull IProtoLogClient client) { + // Dump the tracing config now if no other client is going to dump the same config file. + String configFile = mClientConfigFiles.get(client); + if (configFile != null) { + final var newCount = mConfigFileCounts.get(configFile) - 1; + mConfigFileCounts.put(configFile, newCount); + boolean lastProcessWithViewerConfig = newCount == 0; + if (lastProcessWithViewerConfig) { + try { + mViewerConfigFileTracer.trace(mDataSource, configFile); + } catch (FileNotFoundException e) { + throw new RuntimeException(e); + } + } + } + } + + private static void writeViewerConfigGroup( + @NonNull ProtoInputStream pis, @NonNull ProtoOutputStream os) throws IOException { + final long inGroupToken = pis.start(GROUPS); + final long outGroupToken = os.start(GROUPS); + + while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pis.getFieldNumber()) { + case (int) ID -> { + int id = pis.readInt(ID); + os.write(ID, id); + } + case (int) NAME -> { + String name = pis.readString(NAME); + os.write(NAME, name); + } + case (int) TAG -> { + String tag = pis.readString(TAG); + os.write(TAG, tag); + } + default -> + throw new RuntimeException( + "Unexpected field id " + pis.getFieldNumber()); + } + } + + pis.end(inGroupToken); + os.end(outGroupToken); + } + + private static void writeViewerConfigMessage( + @NonNull ProtoInputStream pis, @NonNull ProtoOutputStream os) throws IOException { + final long inMessageToken = pis.start(MESSAGES); + final long outMessagesToken = os.start(MESSAGES); + + while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pis.getFieldNumber()) { + case (int) MESSAGE_ID -> os.write(MESSAGE_ID, + pis.readLong(MESSAGE_ID)); + case (int) MESSAGE -> os.write(MESSAGE, pis.readString(MESSAGE)); + case (int) LEVEL -> os.write(LEVEL, pis.readInt(LEVEL)); + case (int) GROUP_ID -> os.write(GROUP_ID, pis.readInt(GROUP_ID)); + default -> + throw new RuntimeException( + "Unexpected field id " + pis.getFieldNumber()); + } + } + + pis.end(inMessageToken); + os.end(outMessagesToken); + } +} diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 50727a2415c6..7aeabeed2a08 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -8132,6 +8132,12 @@ <permission android:name="android.permission.MONITOR_STICKY_MODIFIER_STATE" android:protectionLevel="signature" /> + <!-- Allows low-level access to monitor keyboard system shortcuts + <p>Not for use by third-party applications. + @hide --> + <permission android:name="android.permission.MONITOR_KEYBOARD_SYSTEM_SHORTCUTS" + android:protectionLevel="signature" /> + <uses-permission android:name="android.permission.HANDLE_QUERY_PACKAGE_RESTART" /> <!-- Allows financed device kiosk apps to perform actions on the Device Lock service diff --git a/core/res/res/layout/list_menu_item_icon.xml b/core/res/res/layout/list_menu_item_icon.xml index a30be6a13db6..5854e816d2b0 100644 --- a/core/res/res/layout/list_menu_item_icon.xml +++ b/core/res/res/layout/list_menu_item_icon.xml @@ -18,6 +18,8 @@ android:id="@+id/icon" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:maxWidth="@dimen/list_menu_item_icon_max_width" + android:adjustViewBounds="true" android:layout_gravity="center_vertical" android:layout_marginStart="8dip" android:layout_marginEnd="-8dip" diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml index 77b5587e77be..f397ef2b151c 100644 --- a/core/res/res/values/dimens.xml +++ b/core/res/res/values/dimens.xml @@ -1065,4 +1065,7 @@ <!-- The non-linear progress interval when the screen is wider than the navigation_edge_action_progress_threshold. --> <item name="back_progress_non_linear_factor" format="float" type="dimen">0.2</item> + + <!-- The maximum width for a context menu icon --> + <dimen name="list_menu_item_icon_max_width">24dp</dimen> </resources> diff --git a/core/tests/coretests/AndroidManifest.xml b/core/tests/coretests/AndroidManifest.xml index c05ea3d65562..fc3c2f31459f 100644 --- a/core/tests/coretests/AndroidManifest.xml +++ b/core/tests/coretests/AndroidManifest.xml @@ -265,6 +265,17 @@ </intent-filter> </activity> + <activity android:name="android.widget.ChronometerActivity" + android:label="ChronometerActivity" + android:screenOrientation="portrait" + android:exported="true" + android:theme="@android:style/Theme.Material.Light"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" /> + </intent-filter> + </activity> + <activity android:name="android.widget.DatePickerActivity" android:label="DatePickerActivity" android:screenOrientation="portrait" diff --git a/core/tests/coretests/res/layout/chronometer_layout.xml b/core/tests/coretests/res/layout/chronometer_layout.xml new file mode 100644 index 000000000000..f209c4193afa --- /dev/null +++ b/core/tests/coretests/res/layout/chronometer_layout.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2015 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. +--> + +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent"> + <Chronometer + android:id="@+id/chronometer" + android:layout_width="wrap_content" + android:layout_height="wrap_content"/> +</FrameLayout> diff --git a/core/tests/coretests/src/android/graphics/PaintFontVariationTest.java b/core/tests/coretests/src/android/graphics/PaintFontVariationTest.java new file mode 100644 index 000000000000..8a54e5b998e7 --- /dev/null +++ b/core/tests/coretests/src/android/graphics/PaintFontVariationTest.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2015 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 android.graphics; + +import static com.google.common.truth.Truth.assertThat; + +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.test.InstrumentationTestCase; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.text.flags.Flags; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * PaintTest tests {@link Paint}. + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class PaintFontVariationTest extends InstrumentationTestCase { + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + + @RequiresFlagsEnabled(Flags.FLAG_TYPEFACE_CACHE_FOR_VAR_SETTINGS) + @Test + public void testDerivedFromSameTypeface() { + final Paint p = new Paint(); + + p.setTypeface(Typeface.SANS_SERIF); + assertThat(p.setFontVariationSettings("'wght' 450")).isTrue(); + Typeface first = p.getTypeface(); + + p.setTypeface(Typeface.SANS_SERIF); + assertThat(p.setFontVariationSettings("'wght' 480")).isTrue(); + Typeface second = p.getTypeface(); + + assertThat(first.getDerivedFrom()).isSameInstanceAs(second.getDerivedFrom()); + } + + @RequiresFlagsEnabled(Flags.FLAG_TYPEFACE_CACHE_FOR_VAR_SETTINGS) + @Test + public void testDerivedFromChained() { + final Paint p = new Paint(); + + p.setTypeface(Typeface.SANS_SERIF); + assertThat(p.setFontVariationSettings("'wght' 450")).isTrue(); + Typeface first = p.getTypeface(); + + assertThat(p.setFontVariationSettings("'wght' 480")).isTrue(); + Typeface second = p.getTypeface(); + + assertThat(first.getDerivedFrom()).isSameInstanceAs(second.getDerivedFrom()); + } +} diff --git a/core/tests/coretests/src/android/graphics/PaintTest.java b/core/tests/coretests/src/android/graphics/PaintTest.java index 0dec756d7611..878ba703c8fe 100644 --- a/core/tests/coretests/src/android/graphics/PaintTest.java +++ b/core/tests/coretests/src/android/graphics/PaintTest.java @@ -16,13 +16,22 @@ package android.graphics; +import static com.google.common.truth.Truth.assertThat; + import static org.junit.Assert.assertNotEquals; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.test.InstrumentationTestCase; import android.text.TextUtils; import androidx.test.filters.SmallTest; +import com.android.text.flags.Flags; + +import org.junit.Rule; + import java.util.Arrays; import java.util.HashSet; @@ -30,6 +39,9 @@ import java.util.HashSet; * PaintTest tests {@link Paint}. */ public class PaintTest extends InstrumentationTestCase { + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + private static final String FONT_PATH = "fonts/HintedAdvanceWidthTest-Regular.ttf"; static void assertEquals(String message, float[] expected, float[] actual) { @@ -403,4 +415,33 @@ public class PaintTest extends InstrumentationTestCase { assertEquals(6, getClusterCount(p, rtlStr + ltrStr)); assertEquals(9, getClusterCount(p, ltrStr + rtlStr + ltrStr)); } + + @RequiresFlagsEnabled(Flags.FLAG_TYPEFACE_CACHE_FOR_VAR_SETTINGS) + public void testDerivedFromSameTypeface() { + final Paint p = new Paint(); + + p.setTypeface(Typeface.SANS_SERIF); + assertThat(p.setFontVariationSettings("'wght' 450")).isTrue(); + Typeface first = p.getTypeface(); + + p.setTypeface(Typeface.SANS_SERIF); + assertThat(p.setFontVariationSettings("'wght' 480")).isTrue(); + Typeface second = p.getTypeface(); + + assertThat(first.getDerivedFrom()).isSameInstanceAs(second.getDerivedFrom()); + } + + @RequiresFlagsEnabled(Flags.FLAG_TYPEFACE_CACHE_FOR_VAR_SETTINGS) + public void testDerivedFromChained() { + final Paint p = new Paint(); + + p.setTypeface(Typeface.SANS_SERIF); + assertThat(p.setFontVariationSettings("'wght' 450")).isTrue(); + Typeface first = p.getTypeface(); + + assertThat(p.setFontVariationSettings("'wght' 480")).isTrue(); + Typeface second = p.getTypeface(); + + assertThat(first.getDerivedFrom()).isSameInstanceAs(second.getDerivedFrom()); + } } diff --git a/core/tests/coretests/src/android/view/ViewRootImplTest.java b/core/tests/coretests/src/android/view/ViewRootImplTest.java index b990f2486f9e..e240a0853f46 100644 --- a/core/tests/coretests/src/android/view/ViewRootImplTest.java +++ b/core/tests/coretests/src/android/view/ViewRootImplTest.java @@ -1434,8 +1434,47 @@ public class ViewRootImplTest { } @Test + @RequiresFlagsEnabled({FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY, + FLAG_TOOLKIT_FRAME_RATE_FUNCTION_ENABLING_READ_ONLY, + FLAG_TOOLKIT_FRAME_RATE_VIEW_ENABLING_READ_ONLY}) + public void votePreferredFrameRate_resetWhenDestroyingSurface() + throws Throwable { + if (!ViewProperties.vrr_enabled().orElse(true)) { + return; + } + mView = new View(sContext); + WindowManager.LayoutParams wmlp = new WindowManager.LayoutParams(TYPE_APPLICATION_OVERLAY); + wmlp.token = new Binder(); // Set a fake token to bypass 'is your activity running' check + + sInstrumentation.runOnMainSync(() -> { + WindowManager wm = sContext.getSystemService(WindowManager.class); + wm.addView(mView, wmlp); + }); + sInstrumentation.waitForIdleSync(); + + mViewRootImpl = mView.getViewRootImpl(); + + waitForFrameRateCategoryToSettle(mView); + + sInstrumentation.runOnMainSync(() -> { + mViewRootImpl.getView().setVisibility(View.INVISIBLE); + mViewRootImpl.mSurface.release(); + mView.invalidate(); + }); + sInstrumentation.waitForIdleSync(); + + assertEquals(false, mViewRootImpl.mSurface.isValid()); + assertEquals(FRAME_RATE_CATEGORY_DEFAULT, + mViewRootImpl.getLastPreferredFrameRateCategory()); + assertEquals(FRAME_RATE_CATEGORY_DEFAULT, + mViewRootImpl.getPreferredFrameRateCategory()); + assertEquals(0, mViewRootImpl.getLastPreferredFrameRate(), 0.1); + assertEquals(0, mViewRootImpl.getPreferredFrameRate(), 0.1); + } + + @Test @RequiresFlagsEnabled(FLAG_TOOLKIT_FRAME_RATE_VIEW_ENABLING_READ_ONLY) - public void votePreferredFrameRate_velocityVotedAfterOnDraw() throws Throwable { + public void votePreferredFrameRate_reset() throws Throwable { if (!ViewProperties.vrr_enabled().orElse(true)) { return; } diff --git a/core/tests/coretests/src/android/widget/ChronometerActivity.java b/core/tests/coretests/src/android/widget/ChronometerActivity.java new file mode 100644 index 000000000000..aaed4307eda3 --- /dev/null +++ b/core/tests/coretests/src/android/widget/ChronometerActivity.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2008 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 android.widget; + +import android.app.Activity; +import android.os.Bundle; + +import com.android.frameworks.coretests.R; + +/** + * A minimal application for DatePickerFocusTest. + */ +public class ChronometerActivity extends Activity { + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.chronometer_layout); + } +} diff --git a/core/tests/coretests/src/android/widget/ChronometerTest.java b/core/tests/coretests/src/android/widget/ChronometerTest.java new file mode 100644 index 000000000000..3c738372377a --- /dev/null +++ b/core/tests/coretests/src/android/widget/ChronometerTest.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2008 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 android.widget; + +import android.app.Activity; +import android.test.ActivityInstrumentationTestCase2; + +import androidx.test.filters.LargeTest; + +import com.android.frameworks.coretests.R; + +import java.util.ArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * Test {@link DatePicker} focus changes. + */ +@SuppressWarnings("deprecation") +@LargeTest +public class ChronometerTest extends ActivityInstrumentationTestCase2<ChronometerActivity> { + + private Activity mActivity; + private Chronometer mChronometer; + + public ChronometerTest() { + super(ChronometerActivity.class); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + + mActivity = getActivity(); + mChronometer = mActivity.findViewById(R.id.chronometer); + } + + public void testChronometerTicksSequentially() throws Throwable { + final CountDownLatch latch = new CountDownLatch(5); + ArrayList<String> ticks = new ArrayList<>(); + runOnUiThread(() -> { + mChronometer.setOnChronometerTickListener((chronometer) -> { + ticks.add(chronometer.getText().toString()); + latch.countDown(); + try { + Thread.sleep(500); + } catch (InterruptedException e) { + } + }); + mChronometer.start(); + }); + assertTrue(latch.await(6, TimeUnit.SECONDS)); + assertTrue(ticks.size() >= 5); + assertEquals("00:00", ticks.get(0)); + assertEquals("00:01", ticks.get(1)); + assertEquals("00:02", ticks.get(2)); + assertEquals("00:03", ticks.get(3)); + assertEquals("00:04", ticks.get(4)); + } + + private void runOnUiThread(Runnable runnable) throws InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); + mActivity.runOnUiThread(() -> { + runnable.run(); + latch.countDown(); + }); + latch.await(); + } +} diff --git a/core/tests/coretests/src/com/android/internal/jank/CujTest.java b/core/tests/coretests/src/com/android/internal/jank/CujTest.java index bf35ed0a1601..2362a4c925f9 100644 --- a/core/tests/coretests/src/com/android/internal/jank/CujTest.java +++ b/core/tests/coretests/src/com/android/internal/jank/CujTest.java @@ -35,7 +35,6 @@ import org.junit.Test; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.Arrays; -import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -47,26 +46,30 @@ import java.util.stream.Stream; public class CujTest { private static final String ENUM_NAME_PREFIX = "UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__"; - private static final Set<String> DEPRECATED_VALUES = new HashSet<>() { - { - add(ENUM_NAME_PREFIX + "IME_INSETS_ANIMATION"); - } - }; - private static final Map<Integer, String> ENUM_NAME_EXCEPTION_MAP = new HashMap<>() { - { - put(Cuj.CUJ_NOTIFICATION_ADD, getEnumName("SHADE_NOTIFICATION_ADD")); - put(Cuj.CUJ_NOTIFICATION_HEADS_UP_APPEAR, getEnumName("SHADE_HEADS_UP_APPEAR")); - put(Cuj.CUJ_NOTIFICATION_APP_START, getEnumName("SHADE_APP_LAUNCH")); - put(Cuj.CUJ_NOTIFICATION_HEADS_UP_DISAPPEAR, getEnumName("SHADE_HEADS_UP_DISAPPEAR")); - put(Cuj.CUJ_NOTIFICATION_REMOVE, getEnumName("SHADE_NOTIFICATION_REMOVE")); - put(Cuj.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE, getEnumName("NOTIFICATION_SHADE_SWIPE")); - put(Cuj.CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE, getEnumName("SHADE_QS_EXPAND_COLLAPSE")); - put(Cuj.CUJ_NOTIFICATION_SHADE_QS_SCROLL_SWIPE, getEnumName("SHADE_QS_SCROLL_SWIPE")); - put(Cuj.CUJ_NOTIFICATION_SHADE_ROW_EXPAND, getEnumName("SHADE_ROW_EXPAND")); - put(Cuj.CUJ_NOTIFICATION_SHADE_ROW_SWIPE, getEnumName("SHADE_ROW_SWIPE")); - put(Cuj.CUJ_NOTIFICATION_SHADE_SCROLL_FLING, getEnumName("SHADE_SCROLL_FLING")); - } - }; + private static final Set<String> DEPRECATED_VALUES = Set.of( + ENUM_NAME_PREFIX + "IME_INSETS_ANIMATION" + ); + private static final Map<Integer, String> ENUM_NAME_EXCEPTION_MAP = Map.ofEntries( + Map.entry(Cuj.CUJ_NOTIFICATION_ADD, getEnumName("SHADE_NOTIFICATION_ADD")), + Map.entry(Cuj.CUJ_NOTIFICATION_HEADS_UP_APPEAR, getEnumName("SHADE_HEADS_UP_APPEAR")), + Map.entry(Cuj.CUJ_NOTIFICATION_APP_START, getEnumName("SHADE_APP_LAUNCH")), + Map.entry( + Cuj.CUJ_NOTIFICATION_HEADS_UP_DISAPPEAR, + getEnumName("SHADE_HEADS_UP_DISAPPEAR")), + Map.entry(Cuj.CUJ_NOTIFICATION_REMOVE, getEnumName("SHADE_NOTIFICATION_REMOVE")), + Map.entry( + Cuj.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE, + getEnumName("NOTIFICATION_SHADE_SWIPE")), + Map.entry( + Cuj.CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE, + getEnumName("SHADE_QS_EXPAND_COLLAPSE")), + Map.entry( + Cuj.CUJ_NOTIFICATION_SHADE_QS_SCROLL_SWIPE, + getEnumName("SHADE_QS_SCROLL_SWIPE")), + Map.entry(Cuj.CUJ_NOTIFICATION_SHADE_ROW_EXPAND, getEnumName("SHADE_ROW_EXPAND")), + Map.entry(Cuj.CUJ_NOTIFICATION_SHADE_ROW_SWIPE, getEnumName("SHADE_ROW_SWIPE")), + Map.entry(Cuj.CUJ_NOTIFICATION_SHADE_SCROLL_FLING, getEnumName("SHADE_SCROLL_FLING")) + ); @Rule public final Expect mExpect = Expect.create(); diff --git a/graphics/java/android/graphics/Typeface.java b/graphics/java/android/graphics/Typeface.java index fd788167a0d8..889a778556b7 100644 --- a/graphics/java/android/graphics/Typeface.java +++ b/graphics/java/android/graphics/Typeface.java @@ -56,6 +56,7 @@ import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.Preconditions; +import com.android.text.flags.Flags; import dalvik.annotation.optimization.CriticalNative; import dalvik.annotation.optimization.FastNative; @@ -74,6 +75,7 @@ import java.nio.ByteOrder; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Objects; @@ -143,6 +145,23 @@ public class Typeface { private static final LruCache<String, Typeface> sDynamicTypefaceCache = new LruCache<>(16); private static final Object sDynamicCacheLock = new Object(); + private static final LruCache<Long, LruCache<String, Typeface>> sVariableCache = + new LruCache<>(16); + private static final Object sVariableCacheLock = new Object(); + + /** @hide */ + @VisibleForTesting + public static void clearTypefaceCachesForTestingPurpose() { + synchronized (sWeightCacheLock) { + sWeightTypefaceCache.clear(); + } + synchronized (sDynamicCacheLock) { + sDynamicTypefaceCache.evictAll(); + } + synchronized (sVariableCacheLock) { + sVariableCache.evictAll(); + } + } @GuardedBy("SYSTEM_FONT_MAP_LOCK") static Typeface sDefaultTypeface; @@ -195,6 +214,8 @@ public class Typeface { @UnsupportedAppUsage public final long native_instance; + private final Typeface mDerivedFrom; + private final String mSystemFontFamilyName; private final Runnable mCleaner; @@ -274,6 +295,18 @@ public class Typeface { } /** + * Returns the Typeface used for creating this Typeface. + * + * Maybe null if this is not derived from other Typeface. + * TODO(b/357707916): Make this public API. + * @hide + */ + @VisibleForTesting + public final @Nullable Typeface getDerivedFrom() { + return mDerivedFrom; + } + + /** * Returns the system font family name if the typeface was created from a system font family, * otherwise returns null. */ @@ -1021,9 +1054,51 @@ public class Typeface { return typeface; } - /** @hide */ + private static String axesToVarKey(@NonNull List<FontVariationAxis> axes) { + // The given list can be mutated because it is allocated in Paint#setFontVariationSettings. + // Currently, Paint#setFontVariationSettings is the only code path reaches this method. + axes.sort(Comparator.comparingInt(FontVariationAxis::getOpenTypeTagValue)); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < axes.size(); ++i) { + final FontVariationAxis fva = axes.get(i); + sb.append(fva.getTag()); + sb.append(fva.getStyleValue()); + } + return sb.toString(); + } + + /** + * TODO(b/357707916): Make this public API. + * @hide + */ public static Typeface createFromTypefaceWithVariation(@Nullable Typeface family, @NonNull List<FontVariationAxis> axes) { + if (Flags.typefaceCacheForVarSettings()) { + final Typeface target = (family == null) ? Typeface.DEFAULT : family; + final Typeface base = (target.mDerivedFrom == null) ? target : target.mDerivedFrom; + + final String key = axesToVarKey(axes); + + synchronized (sVariableCacheLock) { + LruCache<String, Typeface> innerCache = sVariableCache.get(base.native_instance); + if (innerCache == null) { + // Cache up to 16 var instance per root Typeface + innerCache = new LruCache<>(16); + sVariableCache.put(base.native_instance, innerCache); + } else { + Typeface cached = innerCache.get(key); + if (cached != null) { + return cached; + } + } + Typeface typeface = new Typeface( + nativeCreateFromTypefaceWithVariation(base.native_instance, axes), + base.getSystemFontFamilyName(), base); + innerCache.put(key, typeface); + return typeface; + } + } + final Typeface base = family == null ? Typeface.DEFAULT : family; Typeface typeface = new Typeface( nativeCreateFromTypefaceWithVariation(base.native_instance, axes), @@ -1184,11 +1259,19 @@ public class Typeface { // don't allow clients to call this directly @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) private Typeface(long ni) { - this(ni, null); + this(ni, null, null); } + // don't allow clients to call this directly + // This is kept for robolectric. private Typeface(long ni, @Nullable String systemFontFamilyName) { + this(ni, systemFontFamilyName, null); + } + + // don't allow clients to call this directly + private Typeface(long ni, @Nullable String systemFontFamilyName, + @Nullable Typeface derivedFrom) { if (ni == 0) { throw new RuntimeException("native typeface cannot be made"); } @@ -1198,6 +1281,7 @@ public class Typeface { mStyle = nativeGetStyle(ni); mWeight = nativeGetWeight(ni); mSystemFontFamilyName = systemFontFamilyName; + mDerivedFrom = derivedFrom; } /** diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java index f1e7ef5ce123..99716e7cc69e 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -405,8 +405,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { // Sets the dim area when the two TaskFragments are adjacent. final boolean dimOnTask = !isStacked - && splitAttributes.getWindowAttributes().getDimAreaBehavior() == DIM_AREA_ON_TASK - && Flags.fullscreenDimFlag(); + && splitAttributes.getWindowAttributes().getDimAreaBehavior() == DIM_AREA_ON_TASK; setTaskFragmentDimOnTask(wct, primaryContainer.getTaskFragmentToken(), dimOnTask); setTaskFragmentDimOnTask(wct, secondaryContainer.getTaskFragmentToken(), dimOnTask); @@ -646,7 +645,6 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { container); final boolean isFillParent = relativeBounds.isEmpty(); final boolean dimOnTask = !isFillParent - && Flags.fullscreenDimFlag() && attributes.getWindowAttributes().getDimAreaBehavior() == DIM_AREA_ON_TASK; final IBinder fragmentToken = container.getTaskFragmentToken(); diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java index dc022b4afd3b..9027bf34a58e 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java @@ -25,6 +25,7 @@ import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_KEYGUARD_GOING_AWAY; +import static android.view.WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; @@ -59,7 +60,8 @@ public class TransitionUtil { public static boolean isOpeningType(@WindowManager.TransitionType int type) { return type == TRANSIT_OPEN || type == TRANSIT_TO_FRONT - || type == TRANSIT_KEYGUARD_GOING_AWAY; + || type == TRANSIT_KEYGUARD_GOING_AWAY + || type == TRANSIT_PREPARE_BACK_NAVIGATION; } /** @return true if the transition was triggered by closing something vs opening something */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java index f14f4198c3f2..7275c6494140 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java @@ -16,9 +16,14 @@ package com.android.wm.shell.back; +import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.view.RemoteAnimationTarget.MODE_CLOSING; import static android.view.RemoteAnimationTarget.MODE_OPENING; +import static android.view.WindowManager.TRANSIT_CLOSE_PREPARE_BACK_NAVIGATION; import static android.window.TransitionInfo.FLAG_BACK_GESTURE_ANIMATED; +import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; +import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP; +import static android.window.TransitionInfo.FLAG_SHOW_WALLPAPER; import static com.android.internal.jank.InteractionJankMonitor.CUJ_PREDICTIVE_BACK_HOME; import static com.android.window.flags.Flags.migratePredictiveBackTransition; @@ -31,6 +36,8 @@ import android.annotation.Nullable; import android.annotation.SuppressLint; import android.app.ActivityTaskManager; import android.app.IActivityTaskManager; +import android.app.TaskInfo; +import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; import android.content.res.Configuration; @@ -837,8 +844,9 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont mShellExecutor.executeDelayed(mAnimationTimeoutRunnable, MAX_ANIMATION_DURATION); // The next callback should be {@link #onBackAnimationFinished}. + final boolean migrateBackToTransition = migratePredictiveBackTransition(); if (mCurrentTracker.getTriggerBack()) { - if (migratePredictiveBackTransition()) { + if (migrateBackToTransition) { // notify core gesture is commit if (shouldTriggerCloseTransition()) { mBackTransitionHandler.mCloseTransitionRequested = true; @@ -856,6 +864,10 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont // start post animation dispatchOnBackInvoked(mActiveCallback); } else { + if (migrateBackToTransition + && mBackTransitionHandler.mPrepareOpenTransition != null) { + mBackTransitionHandler.createClosePrepareTransition(); + } tryDispatchOnBackCancelled(mActiveCallback); } } @@ -960,6 +972,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont mShellBackAnimationRegistry.resetDefaultCrossActivity(); cancelLatencyTracking(); mReceivedNullNavigationInfo = false; + mBackTransitionHandler.mLastTrigger = triggerBack; if (mBackNavigationInfo != null) { mPreviousNavigationType = mBackNavigationInfo.getType(); mBackNavigationInfo.onBackNavigationFinished(triggerBack); @@ -1128,12 +1141,18 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont Runnable mOnAnimationFinishCallback; boolean mCloseTransitionRequested; - boolean mOpeningRunning; SurfaceControl.Transaction mFinishOpenTransaction; Transitions.TransitionFinishCallback mFinishOpenTransitionCallback; QueuedTransition mQueuedTransition = null; + boolean mLastTrigger; + // The Transition to make behindActivity become visible + IBinder mPrepareOpenTransition; + // The Transition to make behindActivity become invisible, if prepare open exist and + // animation is canceled, start a close prepare transition to finish the whole transition. + IBinder mClosePrepareTransition; + TransitionInfo mOpenTransitionInfo; void onAnimationFinished() { - if (!mCloseTransitionRequested) { + if (!mCloseTransitionRequested && mClosePrepareTransition == null) { applyFinishOpenTransition(); } if (mOnAnimationFinishCallback != null) { @@ -1158,7 +1177,8 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont mFinishOpenTransitionCallback.onTransitionFinished(null); mFinishOpenTransitionCallback = null; } - mOpeningRunning = false; + mOpenTransitionInfo = null; + mPrepareOpenTransition = null; } private void applyAndFinish(@NonNull SurfaceControl.Transaction st, @@ -1178,21 +1198,42 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont @NonNull Transitions.TransitionFinishCallback finishCallback) { // Both mShellExecutor and Transitions#mMainExecutor are ShellMainThread, so we don't // need to post to ShellExecutor when called. + if (info.getType() == WindowManager.TRANSIT_CLOSE_PREPARE_BACK_NAVIGATION) { + // only consume it if this transition hasn't being processed. + if (mClosePrepareTransition != null) { + mClosePrepareTransition = null; + applyAndFinish(st, ft, finishCallback); + return true; + } + return false; + } + if (info.getType() != WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION && !isGestureBackTransition(info)) { return false; } + + if (shouldCancelAnimation(info)) { + return false; + } + if (mApps == null || mApps.length == 0) { if (mBackNavigationInfo != null && mShellBackAnimationRegistry .isWaitingAnimation(mBackNavigationInfo.getType())) { // Waiting for animation? Queue update to wait for animation start. consumeQueuedTransitionIfNeeded(); mQueuedTransition = new QueuedTransition(info, st, ft, finishCallback); - } else { + return true; + } else if (mLastTrigger) { // animation was done, consume directly applyAndFinish(st, ft, finishCallback); + return true; + } else { + // animation was cancelled but transition haven't happen, we must handle it + if (mClosePrepareTransition == null && mCurrentTracker.isFinished()) { + createClosePrepareTransition(); + } } - return true; } if (handlePrepareTransition(info, st, ft, finishCallback)) { @@ -1201,12 +1242,131 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont return handleCloseTransition(info, st, ft, finishCallback); } + void createClosePrepareTransition() { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.restoreBackNavi(); + mClosePrepareTransition = mTransitions.startTransition( + TRANSIT_CLOSE_PREPARE_BACK_NAVIGATION, wct, mBackTransitionHandler); + } + private void mergePendingTransitions(TransitionInfo info) { + if (mOpenTransitionInfo == null) { + return; + } + // Copy initial changes to final transition + final TransitionInfo init = mOpenTransitionInfo; + // find prepare open target + boolean openShowWallpaper = false; + ComponentName openComponent = null; + int tmpSize; + int openTaskId = INVALID_TASK_ID; + for (int j = init.getChanges().size() - 1; j >= 0; --j) { + final TransitionInfo.Change change = init.getChanges().get(j); + if (change.hasFlags(FLAG_BACK_GESTURE_ANIMATED)) { + openComponent = findComponentName(change); + openTaskId = findTaskId(change); + if (change.hasFlags(FLAG_SHOW_WALLPAPER)) { + openShowWallpaper = true; + } + break; + } + } + if (openComponent == null && openTaskId == INVALID_TASK_ID) { + // shouldn't happen. + return; + } + // find first non-prepare open target + boolean isOpen = false; + tmpSize = info.getChanges().size(); + for (int j = 0; j < tmpSize; ++j) { + final TransitionInfo.Change change = info.getChanges().get(j); + final ComponentName firstNonOpen = findComponentName(change); + final int firstTaskId = findTaskId(change); + if ((firstNonOpen != null && firstNonOpen != openComponent) + || (firstTaskId != INVALID_TASK_ID && firstTaskId != openTaskId)) { + // this is original close target, potential be close, but cannot determine from + // it + if (change.hasFlags(FLAG_BACK_GESTURE_ANIMATED)) { + isOpen = !TransitionUtil.isClosingMode(change.getMode()); + } else { + isOpen = TransitionUtil.isOpeningMode(change.getMode()); + break; + } + } + } + + if (!isOpen) { + // Close transition, the transition info should be: + // init info(open A & wallpaper) + // current info(close B target) + // remove init info(open/change A target & wallpaper) + boolean moveToTop = false; + for (int j = info.getChanges().size() - 1; j >= 0; --j) { + final TransitionInfo.Change change = info.getChanges().get(j); + if (isSameChangeTarget(openComponent, openTaskId, change)) { + moveToTop = change.hasFlags(FLAG_MOVED_TO_TOP); + info.getChanges().remove(j); + } else if ((openShowWallpaper && change.hasFlags(FLAG_IS_WALLPAPER)) + || !change.hasFlags(FLAG_BACK_GESTURE_ANIMATED)) { + info.getChanges().remove(j); + } + } + tmpSize = info.getChanges().size(); + for (int i = 0; i < tmpSize; ++i) { + final TransitionInfo.Change change = init.getChanges().get(i); + if (moveToTop) { + if (isSameChangeTarget(openComponent, openTaskId, change)) { + change.setFlags(change.getFlags() | FLAG_MOVED_TO_TOP); + } + } + info.getChanges().add(i, change); + } + } else { + // Open transition, the transition info should be: + // init info(open A & wallpaper) + // current info(open C target + close B target + close A & wallpaper) + + // If close target isn't back navigated, filter out close A & wallpaper because the + // (open C + close B) pair didn't participant prepare close + boolean nonBackOpen = false; + boolean nonBackClose = false; + tmpSize = info.getChanges().size(); + for (int j = 0; j < tmpSize; ++j) { + final TransitionInfo.Change change = info.getChanges().get(j); + if (!change.hasFlags(FLAG_BACK_GESTURE_ANIMATED) + && canBeTransitionTarget(change)) { + final int mode = change.getMode(); + nonBackOpen |= TransitionUtil.isOpeningMode(mode); + nonBackClose |= TransitionUtil.isClosingMode(mode); + } + } + if (nonBackClose && nonBackOpen) { + for (int j = info.getChanges().size() - 1; j >= 0; --j) { + final TransitionInfo.Change change = info.getChanges().get(j); + if (isSameChangeTarget(openComponent, openTaskId, change)) { + info.getChanges().remove(j); + } else if ((openShowWallpaper && change.hasFlags(FLAG_IS_WALLPAPER))) { + info.getChanges().remove(j); + } + } + } + } + ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Back animation transition, merge pending " + + "transitions result=%s", info); + } + @Override public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback) { - if (!isGestureBackTransition(info)) { - if (mOpeningRunning) { + if (mClosePrepareTransition == transition) { + mClosePrepareTransition = null; + } + // try to handle unexpected transition + mergePendingTransitions(info); + + if (!isGestureBackTransition(info) || shouldCancelAnimation(info) + || !mCloseTransitionRequested) { + if (mPrepareOpenTransition != null) { applyFinishOpenTransition(); } if (mQueuedTransition != null) { @@ -1222,7 +1382,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont // animation was done applyFinishOpenTransition(); mCloseTransitionRequested = false; - } // else, let queued transition to play + } // let queued transition finish. } else { // we are animating, wait until animation finish mOnAnimationFinishCallback = () -> { @@ -1233,6 +1393,56 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } } + // Cancel close animation if something happen unexpected, let another handler to handle + private boolean shouldCancelAnimation(@NonNull TransitionInfo info) { + final boolean noCloseAllowed = + info.getType() == WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION; + boolean unableToHandle = false; + boolean filterTargets = false; + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change c = info.getChanges().get(i); + final boolean backGestureAnimated = c.hasFlags(FLAG_BACK_GESTURE_ANIMATED); + if (!backGestureAnimated && !c.hasFlags(FLAG_IS_WALLPAPER)) { + // something we cannot handle? + unableToHandle = true; + filterTargets = true; + } else if (noCloseAllowed && backGestureAnimated + && TransitionUtil.isClosingMode(c.getMode())) { + // Prepare back navigation shouldn't contain close change, unless top app + // request close. + unableToHandle = true; + } + } + if (!unableToHandle) { + return false; + } + if (!filterTargets) { + return true; + } + if (TransitionUtil.isOpeningType(info.getType()) + || TransitionUtil.isClosingType(info.getType())) { + boolean removeWallpaper = false; + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change c = info.getChanges().get(i); + // filter out opening target, keep original closing target in this transition + if (c.hasFlags(FLAG_BACK_GESTURE_ANIMATED) + && TransitionUtil.isOpeningMode(c.getMode())) { + info.getChanges().remove(i); + removeWallpaper |= c.hasFlags(FLAG_SHOW_WALLPAPER); + } + } + if (removeWallpaper) { + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change c = info.getChanges().get(i); + if (c.hasFlags(FLAG_IS_WALLPAPER)) { + info.getChanges().remove(i); + } + } + } + } + return true; + } + /** * Check whether this transition is prepare for predictive back animation, which could * happen when core make an activity become visible. @@ -1247,9 +1457,11 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } SurfaceControl openingLeash = null; - for (int i = mApps.length - 1; i >= 0; --i) { - if (mApps[i].mode == MODE_OPENING) { - openingLeash = mApps[i].leash; + if (mApps != null) { + for (int i = mApps.length - 1; i >= 0; --i) { + if (mApps[i].mode == MODE_OPENING) { + openingLeash = mApps[i].leash; + } } } if (openingLeash != null) { @@ -1259,13 +1471,14 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont final Point offset = c.getEndRelOffset(); st.setPosition(c.getLeash(), offset.x, offset.y); st.reparent(c.getLeash(), openingLeash); + st.setAlpha(c.getLeash(), 1.0f); } } } st.apply(); mFinishOpenTransaction = ft; mFinishOpenTransitionCallback = finishCallback; - mOpeningRunning = true; + mOpenTransitionInfo = info; return true; } @@ -1288,6 +1501,10 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont @NonNull SurfaceControl.Transaction st, @NonNull SurfaceControl.Transaction ft, @NonNull Transitions.TransitionFinishCallback finishCallback) { + if (info.getType() == WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION + || !mCloseTransitionRequested) { + return false; + } SurfaceControl openingLeash = null; SurfaceControl closingLeash = null; for (int i = mApps.length - 1; i >= 0; --i) { @@ -1325,7 +1542,12 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont public WindowContainerTransaction handleRequest( @NonNull IBinder transition, @NonNull TransitionRequestInfo request) { - if (request.getType() == WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION) { + final int type = request.getType(); + if (type == WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION) { + mPrepareOpenTransition = transition; + return new WindowContainerTransaction(); + } + if (type == WindowManager.TRANSIT_CLOSE_PREPARE_BACK_NAVIGATION) { return new WindowContainerTransaction(); } if (TransitionUtil.isClosingType(request.getType()) && mCloseTransitionRequested) { @@ -1369,4 +1591,36 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } } } + + private static ComponentName findComponentName(TransitionInfo.Change change) { + final ComponentName componentName = change.getActivityComponent(); + if (componentName != null) { + return componentName; + } + final TaskInfo taskInfo = change.getTaskInfo(); + if (taskInfo != null) { + return taskInfo.topActivity; + } + return null; + } + + private static int findTaskId(TransitionInfo.Change change) { + final TaskInfo taskInfo = change.getTaskInfo(); + if (taskInfo != null) { + return taskInfo.taskId; + } + return INVALID_TASK_ID; + } + + private static boolean isSameChangeTarget(ComponentName topActivity, int taskId, + TransitionInfo.Change change) { + final ComponentName openChange = findComponentName(change); + final int firstTaskId = findTaskId(change); + return (openChange != null && openChange == topActivity) + || (firstTaskId != INVALID_TASK_ID && firstTaskId == taskId); + } + + private static boolean canBeTransitionTarget(TransitionInfo.Change change) { + return findComponentName(change) != null || findTaskId(change) != INVALID_TASK_ID; + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java index 7c0455e17cf2..c2ee223b916a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java @@ -186,6 +186,9 @@ public class CompatUIController implements OnDisplaysChangedListener, */ private boolean mIsFirstReachabilityEducationRunning; + @NonNull + private final CompatUIStatusManager mCompatUIStatusManager; + public CompatUIController(@NonNull Context context, @NonNull ShellInit shellInit, @NonNull ShellController shellController, @@ -198,7 +201,8 @@ public class CompatUIController implements OnDisplaysChangedListener, @NonNull DockStateReader dockStateReader, @NonNull CompatUIConfiguration compatUIConfiguration, @NonNull CompatUIShellCommandHandler compatUIShellCommandHandler, - @NonNull AccessibilityManager accessibilityManager) { + @NonNull AccessibilityManager accessibilityManager, + @NonNull CompatUIStatusManager compatUIStatusManager) { mContext = context; mShellController = shellController; mDisplayController = displayController; @@ -213,6 +217,7 @@ public class CompatUIController implements OnDisplaysChangedListener, mCompatUIShellCommandHandler = compatUIShellCommandHandler; mDisappearTimeSupplier = flags -> accessibilityManager.getRecommendedTimeoutMillis( DISAPPEAR_DELAY_MS, flags); + mCompatUIStatusManager = compatUIStatusManager; shellInit.addInitCallback(this::onInit, this); } @@ -520,7 +525,7 @@ public class CompatUIController implements OnDisplaysChangedListener, mSyncQueue, taskListener, mDisplayController.getDisplayLayout(taskInfo.displayId), mTransitionsLazy.get(), stateInfo -> createOrUpdateReachabilityEduLayout(stateInfo.first, stateInfo.second), - mDockStateReader, mCompatUIConfiguration); + mDockStateReader, mCompatUIConfiguration, mCompatUIStatusManager); } private void createOrUpdateRestartDialogLayout(@NonNull TaskInfo taskInfo, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIStatusManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIStatusManager.java new file mode 100644 index 000000000000..915a8a149d54 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIStatusManager.java @@ -0,0 +1,55 @@ +/* + * 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.wm.shell.compatui; + +import android.annotation.NonNull; + +import java.util.function.IntConsumer; +import java.util.function.IntSupplier; + +/** Handle the visibility state of the Compat UI components. */ +public class CompatUIStatusManager { + + public static final int COMPAT_UI_EDUCATION_HIDDEN = 0; + public static final int COMPAT_UI_EDUCATION_VISIBLE = 1; + + @NonNull + private final IntConsumer mWriter; + @NonNull + private final IntSupplier mReader; + + public CompatUIStatusManager(@NonNull IntConsumer writer, @NonNull IntSupplier reader) { + mWriter = writer; + mReader = reader; + } + + public CompatUIStatusManager() { + this(i -> { }, () -> COMPAT_UI_EDUCATION_HIDDEN); + } + + void onEducationShown() { + mWriter.accept(COMPAT_UI_EDUCATION_VISIBLE); + } + + void onEducationHidden() { + mWriter.accept(COMPAT_UI_EDUCATION_HIDDEN); + } + + boolean isEducationVisible() { + return mReader.getAsInt() == COMPAT_UI_EDUCATION_VISIBLE; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/LetterboxEduWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/LetterboxEduWindowManager.java index 234703277c7d..3124a397162f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/LetterboxEduWindowManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/LetterboxEduWindowManager.java @@ -19,6 +19,7 @@ package com.android.wm.shell.compatui; import static android.provider.Settings.Secure.LAUNCHER_TASKBAR_EDUCATION_SHOWING; import static android.window.TaskConstants.TASK_CHILD_LAYER_COMPAT_UI; +import android.annotation.NonNull; import android.annotation.Nullable; import android.app.TaskInfo; import android.content.Context; @@ -76,15 +77,19 @@ class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract { private final DockStateReader mDockStateReader; + @NonNull + private final CompatUIStatusManager mCompatUIStatusManager; + LetterboxEduWindowManager(Context context, TaskInfo taskInfo, SyncTransactionQueue syncQueue, ShellTaskOrganizer.TaskListener taskListener, DisplayLayout displayLayout, Transitions transitions, Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> onDismissCallback, - DockStateReader dockStateReader, CompatUIConfiguration compatUIConfiguration) { + DockStateReader dockStateReader, CompatUIConfiguration compatUIConfiguration, + @NonNull CompatUIStatusManager compatUIStatusManager) { this(context, taskInfo, syncQueue, taskListener, displayLayout, transitions, onDismissCallback, new DialogAnimationController<>(context, /* tag */ "LetterboxEduWindowManager"), - dockStateReader, compatUIConfiguration); + dockStateReader, compatUIConfiguration, compatUIStatusManager); } @VisibleForTesting @@ -93,7 +98,8 @@ class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract { DisplayLayout displayLayout, Transitions transitions, Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> onDismissCallback, DialogAnimationController<LetterboxEduDialogLayout> animationController, - DockStateReader dockStateReader, CompatUIConfiguration compatUIConfiguration) { + DockStateReader dockStateReader, CompatUIConfiguration compatUIConfiguration, + @NonNull CompatUIStatusManager compatUIStatusManager) { super(context, taskInfo, syncQueue, taskListener, displayLayout); mTransitions = transitions; mOnDismissCallback = onDismissCallback; @@ -103,6 +109,7 @@ class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract { R.dimen.letterbox_education_dialog_margin); mDockStateReader = dockStateReader; mCompatUIConfiguration = compatUIConfiguration; + mCompatUIStatusManager = compatUIStatusManager; mEligibleForLetterboxEducation = taskInfo.appCompatTaskInfo.eligibleForLetterboxEducation(); } @@ -139,7 +146,7 @@ class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract { protected View createLayout() { mLayout = inflateLayout(); updateDialogMargins(); - + mCompatUIStatusManager.onEducationShown(); // startEnterAnimation will be called immediately if shell-transitions are disabled. mTransitions.runOnIdle(this::startEnterAnimation); return mLayout; @@ -199,6 +206,7 @@ class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract { @Override public void release() { mAnimationController.cancelAnimation(); + mCompatUIStatusManager.onEducationHidden(); super.release(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/OWNERS new file mode 100644 index 000000000000..1875675296a8 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/OWNERS @@ -0,0 +1,4 @@ +# WM shell sub-module compat ui owners +mariiasand@google.com +gracielawputri@google.com +mcarli@google.com diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUISpec.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUISpec.kt index a520d5e60fe5..022906cf568c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUISpec.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUISpec.kt @@ -16,6 +16,9 @@ package com.android.wm.shell.compatui.api +import com.android.internal.protolog.ProtoLog +import com.android.wm.shell.protolog.ShellProtoLogGroup + /** * Defines the predicates to invoke for understanding if a component can be created or destroyed. */ @@ -39,6 +42,7 @@ class CompatUILifecyclePredicates( * Describes each compat ui component to the framework. */ class CompatUISpec( + val log: (String) -> Unit = { str -> ProtoLog.v(ShellProtoLogGroup.WM_SHELL_COMPAT_UI, str) }, // Unique name for the component. It's used for debug and for generating the // unique component identifier in the system. val name: String, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index f22dcce00907..04cd225ea4a3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -16,6 +16,9 @@ package com.android.wm.shell.dagger; +import static android.provider.Settings.Secure.COMPAT_UI_EDUCATION_SHOWING; + +import static com.android.wm.shell.compatui.CompatUIStatusManager.COMPAT_UI_EDUCATION_HIDDEN; import static com.android.wm.shell.onehanded.OneHandedController.SUPPORT_ONE_HANDED_MODE; import android.annotation.NonNull; @@ -24,6 +27,7 @@ import android.content.Context; import android.content.pm.PackageManager; import android.os.Handler; import android.os.SystemProperties; +import android.provider.Settings; import android.view.IWindowManager; import android.view.accessibility.AccessibilityManager; import android.window.SystemPerformanceHinter; @@ -72,6 +76,7 @@ import com.android.wm.shell.common.pip.SizeSpecSource; import com.android.wm.shell.compatui.CompatUIConfiguration; import com.android.wm.shell.compatui.CompatUIController; import com.android.wm.shell.compatui.CompatUIShellCommandHandler; +import com.android.wm.shell.compatui.CompatUIStatusManager; import com.android.wm.shell.compatui.api.CompatUIComponentIdGenerator; import com.android.wm.shell.compatui.api.CompatUIHandler; import com.android.wm.shell.compatui.api.CompatUIRepository; @@ -254,7 +259,8 @@ public abstract class WMShellBaseModule { Lazy<AccessibilityManager> accessibilityManager, CompatUIRepository compatUIRepository, @NonNull CompatUIState compatUIState, - @NonNull CompatUIComponentIdGenerator componentIdGenerator) { + @NonNull CompatUIComponentIdGenerator componentIdGenerator, + CompatUIStatusManager compatUIStatusManager) { if (!context.getResources().getBoolean(R.bool.config_enableCompatUIController)) { return Optional.empty(); } @@ -276,7 +282,22 @@ public abstract class WMShellBaseModule { dockStateReader.get(), compatUIConfiguration.get(), compatUIShellCommandHandler.get(), - accessibilityManager.get())); + accessibilityManager.get(), + compatUIStatusManager)); + } + + @WMSingleton + @Provides + static CompatUIStatusManager provideCompatUIStatusManager(@NonNull Context context) { + if (Flags.enableCompatUiVisibilityStatus()) { + return new CompatUIStatusManager( + newState -> Settings.Secure.putInt(context.getContentResolver(), + COMPAT_UI_EDUCATION_SHOWING, newState), + () -> Settings.Secure.getInt(context.getContentResolver(), + COMPAT_UI_EDUCATION_SHOWING, COMPAT_UI_EDUCATION_HIDDEN)); + } else { + return new CompatUIStatusManager(); + } } @WMSingleton diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt index 3e7b4fe89b45..6c03dc333515 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt @@ -54,6 +54,12 @@ fun calculateInitialBounds( // Instead default to the desired initial bounds. val stableBounds = Rect() displayLayout.getStableBoundsForDesktopMode(stableBounds) + if (hasFullscreenOverride(taskInfo)) { + // If the activity has a fullscreen override applied, it should be treated as + // resizeable and match the device orientation. Thus the ideal size can be + // applied. + return positionInScreen(idealSize, stableBounds) + } val topActivityInfo = taskInfo.topActivityInfo ?: return positionInScreen(idealSize, stableBounds) @@ -62,13 +68,17 @@ fun calculateInitialBounds( ORIENTATION_LANDSCAPE -> { if (taskInfo.isResizeable) { if (isFixedOrientationPortrait(topActivityInfo.screenOrientation)) { - // Respect apps fullscreen width + // For portrait resizeable activities, respect apps fullscreen width but + // apply ideal size height. Size(taskInfo.appCompatTaskInfo.topActivityLetterboxAppWidth, idealSize.height) } else { + // For landscape resizeable activities, simply apply ideal size. idealSize } } else { + // If activity is unresizeable, regardless of orientation, calculate maximum + // size (within the ideal size) maintaining original aspect ratio. maximizeSizeGivenAspectRatio(taskInfo, idealSize, appAspectRatio) } } @@ -77,23 +87,29 @@ fun calculateInitialBounds( screenBounds.width() - (DESKTOP_MODE_LANDSCAPE_APP_PADDING * 2) if (taskInfo.isResizeable) { if (isFixedOrientationLandscape(topActivityInfo.screenOrientation)) { - // Respect apps fullscreen height and apply custom app width + // For landscape resizeable activities, respect apps fullscreen height and + // apply custom app width. Size( customPortraitWidthForLandscapeApp, taskInfo.appCompatTaskInfo.topActivityLetterboxAppHeight ) } else { + // For portrait resizeable activities, simply apply ideal size. idealSize } } else { if (isFixedOrientationLandscape(topActivityInfo.screenOrientation)) { - // Apply custom app width and calculate maximum size + // For landscape unresizeable activities, apply custom app width to ideal + // size and calculate maximum size with this area while maintaining original + // aspect ratio. maximizeSizeGivenAspectRatio( taskInfo, Size(customPortraitWidthForLandscapeApp, idealSize.height), appAspectRatio ) } else { + // For portrait unresizeable activities, calculate maximum size (within the + // ideal size) maintaining original aspect ratio. maximizeSizeGivenAspectRatio(taskInfo, idealSize, appAspectRatio) } } @@ -209,3 +225,8 @@ fun TaskInfo.hasPortraitTopActivity(): Boolean { else -> isFixedOrientationPortrait(configuration.orientation) } } + +private fun hasFullscreenOverride(taskInfo: RunningTaskInfo): Boolean { + return taskInfo.appCompatTaskInfo.isUserFullscreenOverrideEnabled + || taskInfo.appCompatTaskInfo.isSystemFullscreenOverrideEnabled +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskPosition.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskPosition.kt index 97abda81d12d..65f12cf4a196 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskPosition.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskPosition.kt @@ -116,10 +116,10 @@ fun canChangeTaskPosition(taskInfo: TaskInfo): Boolean { @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) fun Rect.getDesktopTaskPosition(bounds: Rect): DesktopTaskPosition { return when { - top == bounds.top && left == bounds.left -> TopLeft - top == bounds.top && right == bounds.right -> TopRight - bottom == bounds.bottom && left == bounds.left -> BottomLeft - bottom == bounds.bottom && right == bounds.right -> BottomRight + top == bounds.top && left == bounds.left && bottom != bounds.bottom -> TopLeft + top == bounds.top && right == bounds.right && bottom != bounds.bottom -> TopRight + bottom == bounds.bottom && left == bounds.left && top != bounds.top -> BottomLeft + bottom == bounds.bottom && right == bounds.right && top != bounds.top -> BottomRight else -> Center } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index 78d41b211abe..f54b44b29683 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -1070,6 +1070,11 @@ class DesktopTasksController( // In some launches home task is moved behind new task being launched. Make sure // that's not the case for launches in desktop. moveHomeTask(wct, toTop = false) + // Move existing minimized tasks behind Home + taskRepository.getFreeformTasksInZOrder(task.displayId) + .filter { taskId -> taskRepository.isMinimizedTask(taskId) } + .mapNotNull { taskId -> shellTaskOrganizer.getRunningTaskInfo(taskId) } + .forEach { taskInfo -> wct.reorder(taskInfo.token, /* onTop= */ false) } // Desktop Mode is already showing and we're launching a new Task - we might need to // minimize another Task. val taskToMinimize = addAndGetMinimizeChangesIfNeeded(task.displayId, wct, task) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java index c18964240f98..0d7f7f66032a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java @@ -34,11 +34,13 @@ import android.animation.ValueAnimator; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.ActivityManager; import android.app.PendingIntent; import android.app.RemoteAction; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; import android.graphics.Color; import android.graphics.Rect; import android.graphics.drawable.Drawable; @@ -151,6 +153,10 @@ public class PipMenuView extends FrameLayout { // How long the shell will wait for the app to close the PiP if a custom action is set. private final int mPipForceCloseDelay; + // Context for the currently active user. This may differ from the regular systemui Context + // in cases such as secondary users or HSUM. + private Context mContextForUser; + public PipMenuView(Context context, PhonePipMenuController controller, ShellExecutor mainExecutor, Handler mainHandler, PipUiEventLogger pipUiEventLogger) { @@ -202,6 +208,7 @@ public class PipMenuView extends FrameLayout { .getInteger(R.integer.config_pipExitAnimationDuration); initAccessibility(); + setContextForUser(); } private void initAccessibility() { @@ -476,7 +483,7 @@ public class PipMenuView extends FrameLayout { actionView.setImageDrawable(null); } else { // TODO: Check if the action drawable has changed before we reload it - action.getIcon().loadDrawableAsync(mContext, d -> { + action.getIcon().loadDrawableAsync(mContextForUser, d -> { if (d != null) { d.setTint(Color.WHITE); actionView.setImageDrawable(d); @@ -510,6 +517,33 @@ public class PipMenuView extends FrameLayout { expandContainer.requestLayout(); } + /** + * Sets the Context for the current user. If the user is the same as systemui, then simply + * use systemui Context. + */ + private void setContextForUser() { + int userId = ActivityManager.getCurrentUser(); + + if (mContext.getUserId() != userId) { + try { + mContextForUser = mContext.createPackageContextAsUser(mContext.getPackageName(), + Context.CONTEXT_RESTRICTED, new UserHandle(userId)); + } catch (PackageManager.NameNotFoundException e) { + // Shouldn't happen, use systemui context as backup + ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Failed to get context for user. Sysui userid=%d," + + " current userid=%d, error=%s", + TAG, + mContext.getUserId(), + userId, + e); + mContextForUser = mContext; + } + } else { + mContextForUser = mContext; + } + } + private void notifyMenuStateChangeStart(int menuState, boolean resize, Runnable callback) { mController.onMenuStateChangeStart(menuState, resize, callback); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java index 497c3f704c82..f739d65e63c3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java @@ -61,6 +61,8 @@ public enum ShellProtoLogGroup implements IProtoLogGroup { Consts.TAG_WM_SHELL), WM_SHELL_BUBBLES(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, "Bubbles"), + WM_SHELL_COMPAT_UI(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, + Consts.TAG_WM_COMPAT_UI), TEST_GROUP(true, true, false, "WindowManagerShellProtoLogTest"); private final boolean mEnabled; @@ -128,6 +130,7 @@ public enum ShellProtoLogGroup implements IProtoLogGroup { private static final String TAG_WM_STARTING_WINDOW = "ShellStartingWindow"; private static final String TAG_WM_SPLIT_SCREEN = "ShellSplitScreen"; private static final String TAG_WM_DESKTOP_MODE = "ShellDesktopMode"; + private static final String TAG_WM_COMPAT_UI = "CompatUi"; private static final boolean ENABLE_DEBUG = true; private static final boolean ENABLE_LOG_TO_PROTO_DEBUG = true; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index 87dc16a79766..9bf515933b22 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -2243,6 +2243,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, final @WindowManager.TransitionType int type = request.getType(); final boolean isOpening = isOpeningType(type); final boolean inFullscreen = triggerTask.getWindowingMode() == WINDOWING_MODE_FULLSCREEN; + final StageTaskListener stage = getStageOfTask(triggerTask); if (isOpening && inFullscreen) { // One task is opening into fullscreen mode, remove the corresponding split record. @@ -2258,7 +2259,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, + " sideChildren=%d", triggerTask.taskId, transitTypeToString(type), mMainStage.getChildCount(), mSideStage.getChildCount()); out = new WindowContainerTransaction(); - final StageTaskListener stage = getStageOfTask(triggerTask); if (stage != null) { if (isClosingType(type) && stage.getChildCount() == 1) { // Dismiss split if the last task in one of the stages is going away @@ -2331,16 +2331,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // Don't intercept the transition if we are not handling it as a part of one of the // cases above and it is not already visible return null; - } else { - if (triggerTask.parentTaskId == mMainStage.mRootTaskInfo.taskId - || triggerTask.parentTaskId == mSideStage.mRootTaskInfo.taskId) { - ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "handleRequest: transition=%d " - + "restoring to split", request.getDebugId()); - out = new WindowContainerTransaction(); - mSplitTransitions.setEnterTransition(transition, request.getRemoteTransition(), - TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE, false /* resizeAnim */); - } - if (isOpening && getStageOfTask(triggerTask) != null) { + } else if (stage != null) { + if (isOpening) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "handleRequest: transition=%d enter split", request.getDebugId()); // One task is appearing into split, prepare to enter split screen. @@ -2348,9 +2340,15 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, prepareEnterSplitScreen(out); mSplitTransitions.setEnterTransition(transition, request.getRemoteTransition(), TRANSIT_SPLIT_SCREEN_PAIR_OPEN, !mIsDropEntering); + return out; } - return out; + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "handleRequest: transition=%d " + + "restoring to split", request.getDebugId()); + out = new WindowContainerTransaction(); + mSplitTransitions.setEnterTransition(transition, request.getRemoteTransition(), + TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE, false /* resizeAnim */); } + return out; } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java index 75e7ddf53f9f..a27c14bda15a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java @@ -19,7 +19,9 @@ package com.android.wm.shell.transition; import static android.app.ActivityOptions.ANIM_FROM_STYLE; import static android.app.ActivityOptions.ANIM_NONE; import static android.view.WindowManager.TRANSIT_CLOSE; +import static android.view.WindowManager.TRANSIT_CLOSE_PREPARE_BACK_NAVIGATION; import static android.view.WindowManager.TRANSIT_OPEN; +import static android.view.WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; import static android.view.WindowManager.transitTypeToString; @@ -221,6 +223,15 @@ public class TransitionAnimationHelper { */ public static int getTransitionTypeFromInfo(@NonNull TransitionInfo info) { final int type = info.getType(); + // This back navigation is canceled, check whether the transition should be open or close + if (type == TRANSIT_PREPARE_BACK_NAVIGATION + || type == TRANSIT_CLOSE_PREPARE_BACK_NAVIGATION) { + if (!info.getChanges().isEmpty()) { + final TransitionInfo.Change change = info.getChanges().get(0); + return TransitionUtil.isOpeningMode(change.getMode()) + ? TRANSIT_OPEN : TRANSIT_CLOSE; + } + } // If the info transition type is opening transition, iterate its changes to see if it // has any opening change, if none, returns TRANSIT_CLOSE type for closing animation. if (type == TRANSIT_OPEN) { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java index de1659b1a163..b39cf19a155a 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java @@ -43,6 +43,7 @@ import android.view.InsetsSource; import android.view.InsetsState; import android.view.accessibility.AccessibilityManager; +import androidx.annotation.NonNull; import androidx.test.filters.SmallTest; import com.android.window.flags.Flags; @@ -128,6 +129,9 @@ public class CompatUIControllerTest extends ShellTestCase { @Captor ArgumentCaptor<OnInsetsChangedListener> mOnInsetsChangedListenerCaptor; + @NonNull + private CompatUIStatusManager mCompatUIStatusManager; + @Before public void setUp() { MockitoAnnotations.initMocks(this); @@ -147,11 +151,13 @@ public class CompatUIControllerTest extends ShellTestCase { doReturn(true).when(mMockRestartDialogLayout).createLayout(anyBoolean()); doReturn(true).when(mMockRestartDialogLayout).updateCompatInfo(any(), any(), anyBoolean()); + mCompatUIStatusManager = new CompatUIStatusManager(); mShellInit = spy(new ShellInit(mMockExecutor)); mController = new CompatUIController(mContext, mShellInit, mMockShellController, mMockDisplayController, mMockDisplayInsetsController, mMockImeController, mMockSyncQueue, mMockExecutor, mMockTransitionsLazy, mDockStateReader, - mCompatUIConfiguration, mCompatUIShellCommandHandler, mAccessibilityManager) { + mCompatUIConfiguration, mCompatUIShellCommandHandler, mAccessibilityManager, + mCompatUIStatusManager) { @Override CompatUIWindowManager createCompatUiWindowManager(Context context, TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener) { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIStatusManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIStatusManagerTest.java new file mode 100644 index 000000000000..d6059a88e9c7 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIStatusManagerTest.java @@ -0,0 +1,75 @@ +/* + * 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.wm.shell.compatui; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.testing.AndroidTestingRunner; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.ShellTestCase; + + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.function.IntConsumer; +import java.util.function.IntSupplier; + +/** + * Tests for {@link CompatUILayout}. + * + * Build/Install/Run: + * atest WMShellUnitTests:CompatUIStatusManagerTest + */ +@RunWith(AndroidTestingRunner.class) +@SmallTest +public class CompatUIStatusManagerTest extends ShellTestCase { + + private FakeCompatUIStatusManagerTest mTestState; + private CompatUIStatusManager mStatusManager; + + @Before + public void setUp() { + mTestState = new FakeCompatUIStatusManagerTest(); + mStatusManager = new CompatUIStatusManager(mTestState.mWriter, mTestState.mReader); + } + + @Test + public void isEducationShown() { + assertFalse(mStatusManager.isEducationVisible()); + + mStatusManager.onEducationShown(); + assertTrue(mStatusManager.isEducationVisible()); + + mStatusManager.onEducationHidden(); + assertFalse(mStatusManager.isEducationVisible()); + } + + static class FakeCompatUIStatusManagerTest { + + int mCurrentStatus = 0; + + final IntSupplier mReader = () -> mCurrentStatus; + + final IntConsumer mWriter = newStatus -> mCurrentStatus = newStatus; + + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduWindowManagerTest.java index 7617269cf5d3..94dbd112bb75 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduWindowManagerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduWindowManagerTest.java @@ -20,9 +20,13 @@ import static android.content.res.Configuration.UI_MODE_NIGHT_YES; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.window.flags.Flags.FLAG_APP_COMPAT_UI_FRAMEWORK; +import static com.android.wm.shell.compatui.CompatUIStatusManager.COMPAT_UI_EDUCATION_HIDDEN; +import static com.android.wm.shell.compatui.CompatUIStatusManager.COMPAT_UI_EDUCATION_VISIBLE; import static com.google.common.truth.Truth.assertThat; +import static junit.framework.Assert.assertEquals; + import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; @@ -38,6 +42,7 @@ import android.app.ActivityManager; import android.app.TaskInfo; import android.graphics.Insets; import android.graphics.Rect; +import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.RequiresFlagsDisabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; @@ -54,6 +59,7 @@ import android.view.accessibility.AccessibilityEvent; import androidx.test.filters.SmallTest; +import com.android.window.flags.Flags; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; @@ -61,6 +67,7 @@ import com.android.wm.shell.TestShellExecutor; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.DockStateReader; import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.compatui.CompatUIStatusManagerTest.FakeCompatUIStatusManagerTest; import com.android.wm.shell.transition.Transitions; import org.junit.After; @@ -120,6 +127,8 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { private CompatUIConfiguration mCompatUIConfiguration; private TestShellExecutor mExecutor; + private FakeCompatUIStatusManagerTest mCompatUIStatus; + private CompatUIStatusManager mCompatUIStatusManager; @Rule public final CheckFlagsRule mCheckFlagsRule = @@ -129,6 +138,9 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { public void setUp() { MockitoAnnotations.initMocks(this); mExecutor = new TestShellExecutor(); + mCompatUIStatus = new FakeCompatUIStatusManagerTest(); + mCompatUIStatusManager = new CompatUIStatusManager(mCompatUIStatus.mWriter, + mCompatUIStatus.mReader); mCompatUIConfiguration = new CompatUIConfiguration(mContext, mExecutor) { final Set<Integer> mHasSeenSet = new HashSet<>(); @@ -414,6 +426,21 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { assertFalse(windowManager.needsToBeRecreated(newTaskInfo, mTaskListener)); } + @Test + @EnableFlags(Flags.FLAG_ENABLE_COMPAT_UI_VISIBILITY_STATUS) + public void testCompatUIStatus_dialogIsShown() { + // We display the dialog + LetterboxEduWindowManager windowManager = createWindowManager(/* eligible= */ true, + USER_ID_1, /* isTaskbarEduShowing= */ false); + assertTrue(windowManager.createLayout(/* canShow= */ true)); + assertNotNull(windowManager.mLayout); + assertEquals(/* expected= */ COMPAT_UI_EDUCATION_VISIBLE, mCompatUIStatus.mCurrentStatus); + + // We dismiss + windowManager.release(); + assertEquals(/* expected= */ COMPAT_UI_EDUCATION_HIDDEN, mCompatUIStatus.mCurrentStatus); + } + private void verifyLayout(LetterboxEduDialogLayout layout, ViewGroup.LayoutParams params, int expectedWidth, int expectedHeight, int expectedExtraTopMargin, int expectedExtraBottomMargin) { @@ -464,7 +491,7 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { windowManager = new LetterboxEduWindowManager(mContext, createTaskInfo(eligible, userId), mSyncTransactionQueue, mTaskListener, createDisplayLayout(), mTransitions, mOnDismissCallback, mAnimationController, - mDockStateReader, mCompatUIConfiguration); + mDockStateReader, mCompatUIConfiguration, mCompatUIStatusManager); spyOn(windowManager); doReturn(mViewHost).when(windowManager).createSurfaceViewHost(); doReturn(isTaskbarEduShowing).when(windowManager).isTaskbarEduShowing(); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index 92f705097c33..7bb54498b877 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -731,6 +731,64 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS) + fun addMoveToDesktopChanges_lastWindowSnapLeft_positionResetsToCenter() { + setUpLandscapeDisplay() + val stableBounds = Rect() + displayLayout.getStableBoundsForDesktopMode(stableBounds) + + // Add freeform task with half display size snap bounds at left side. + setUpFreeformTask(bounds = Rect(stableBounds.left, stableBounds.top, 500, stableBounds.bottom)) + + val task = setUpFullscreenTask() + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) + + val finalBounds = findBoundsChange(wct, task) + assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!)) + .isEqualTo(DesktopTaskPosition.Center) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS) + fun addMoveToDesktopChanges_lastWindowSnapRight_positionResetsToCenter() { + setUpLandscapeDisplay() + val stableBounds = Rect() + displayLayout.getStableBoundsForDesktopMode(stableBounds) + + // Add freeform task with half display size snap bounds at right side. + setUpFreeformTask(bounds = Rect( + stableBounds.right - 500, stableBounds.top, stableBounds.right, stableBounds.bottom)) + + val task = setUpFullscreenTask() + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) + + val finalBounds = findBoundsChange(wct, task) + assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!)) + .isEqualTo(DesktopTaskPosition.Center) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS) + fun addMoveToDesktopChanges_lastWindowMaximised_positionResetsToCenter() { + setUpLandscapeDisplay() + val stableBounds = Rect() + displayLayout.getStableBoundsForDesktopMode(stableBounds) + + // Add maximised freeform task. + setUpFreeformTask(bounds = Rect(stableBounds)) + + val task = setUpFullscreenTask() + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) + + val finalBounds = findBoundsChange(wct, task) + assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!)) + .isEqualTo(DesktopTaskPosition.Center) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS) fun addMoveToDesktopChanges_defaultToCenterIfFree() { setUpLandscapeDisplay() val stableBounds = Rect() @@ -751,6 +809,50 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun addMoveToDesktopChanges_landscapeDevice_userFullscreenOverride_defaultPortraitBounds() { + setUpLandscapeDisplay() + val task = setUpFullscreenTask(enableUserFullscreenOverride = true) + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) + + assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun addMoveToDesktopChanges_landscapeDevice_systemFullscreenOverride_defaultPortraitBounds() { + setUpLandscapeDisplay() + val task = setUpFullscreenTask(enableSystemFullscreenOverride = true) + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) + + assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun addMoveToDesktopChanges_portraitDevice_userFullscreenOverride_defaultPortraitBounds() { + setUpPortraitDisplay() + val task = setUpFullscreenTask(enableUserFullscreenOverride = true) + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) + + assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun addMoveToDesktopChanges_portraitDevice_systemFullscreenOverride_defaultPortraitBounds() { + setUpPortraitDisplay() + val task = setUpFullscreenTask(enableSystemFullscreenOverride = true) + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) + + assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS) + } + + @Test fun moveToDesktop_tdaFullscreen_windowingModeSetToFreeform() { val task = setUpFullscreenTask() val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! @@ -1305,13 +1407,36 @@ class DesktopTasksControllerTest : ShellTestCase() { val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() } freeformTasks.forEach { markTaskVisible(it) } val fullscreenTask = createFullscreenTask() + val homeTask = setUpHomeTask(DEFAULT_DISPLAY) val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask)) // Make sure we reorder the new task to top, and the back task to the bottom - assertThat(wct!!.hierarchyOps.size).isEqualTo(2) + assertThat(wct!!.hierarchyOps.size).isEqualTo(3) wct.assertReorderAt(0, fullscreenTask, toTop = true) - wct.assertReorderAt(1, freeformTasks[0], toTop = false) + wct.assertReorderAt(1, homeTask, toTop = false) + wct.assertReorderAt(2, freeformTasks[0], toTop = false) + } + + @Test + fun handleRequest_fullscreenTaskToFreeform_alreadyBeyondLimit_existingAndNewTasksAreMinimized() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val minimizedTask = setUpFreeformTask() + taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = minimizedTask.taskId) + val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() } + freeformTasks.forEach { markTaskVisible(it) } + val homeTask = setUpHomeTask() + val fullscreenTask = createFullscreenTask() + + val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask)) + + assertThat(wct!!.hierarchyOps.size).isEqualTo(4) + wct.assertReorderAt(0, fullscreenTask, toTop = true) + // Make sure we reorder the home task to the bottom, and minimized tasks below the home task. + wct.assertReorderAt(1, homeTask, toTop = false) + wct.assertReorderAt(2, minimizedTask, toTop = false) + wct.assertReorderAt(3, freeformTasks[0], toTop = false) } @Test @@ -2712,13 +2837,15 @@ class DesktopTasksControllerTest : ShellTestCase() { } private fun setUpFullscreenTask( - displayId: Int = DEFAULT_DISPLAY, - isResizable: Boolean = true, - windowingMode: Int = WINDOWING_MODE_FULLSCREEN, - deviceOrientation: Int = ORIENTATION_LANDSCAPE, - screenOrientation: Int = SCREEN_ORIENTATION_UNSPECIFIED, - shouldLetterbox: Boolean = false, - gravity: Int = Gravity.NO_GRAVITY + displayId: Int = DEFAULT_DISPLAY, + isResizable: Boolean = true, + windowingMode: Int = WINDOWING_MODE_FULLSCREEN, + deviceOrientation: Int = ORIENTATION_LANDSCAPE, + screenOrientation: Int = SCREEN_ORIENTATION_UNSPECIFIED, + shouldLetterbox: Boolean = false, + gravity: Int = Gravity.NO_GRAVITY, + enableUserFullscreenOverride: Boolean = false, + enableSystemFullscreenOverride: Boolean = false ): RunningTaskInfo { val task = createFullscreenTask(displayId) val activityInfo = ActivityInfo() @@ -2729,6 +2856,8 @@ class DesktopTasksControllerTest : ShellTestCase() { isResizeable = isResizable configuration.orientation = deviceOrientation configuration.windowConfiguration.windowingMode = windowingMode + appCompatTaskInfo.isUserFullscreenOverrideEnabled = enableUserFullscreenOverride + appCompatTaskInfo.isSystemFullscreenOverrideEnabled = enableSystemFullscreenOverride if (shouldLetterbox) { if (deviceOrientation == ORIENTATION_LANDSCAPE && diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlagsTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlagsTest.kt index dd19d76d88cf..571bdd4ea32f 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlagsTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlagsTest.kt @@ -33,7 +33,7 @@ import com.android.wm.shell.shared.desktopmode.DesktopModeFlags.ToggleOverride.O import com.android.wm.shell.shared.desktopmode.DesktopModeFlags.ToggleOverride.OVERRIDE_UNSET import com.android.wm.shell.shared.desktopmode.DesktopModeFlags.WALLPAPER_ACTIVITY import com.google.common.truth.Truth.assertThat -import org.junit.Before +import org.junit.After import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -49,9 +49,9 @@ class DesktopModeFlagsTest : ShellTestCase() { @JvmField @Rule val setFlagsRule = SetFlagsRule() - @Before - fun setUp() { - resetCache() + @After + fun tearDown() { + resetToggleOverrideCache() } // TODO(b/348193756): Add tests @@ -338,7 +338,7 @@ class DesktopModeFlagsTest : ShellTestCase() { } } - private fun resetCache() { + private fun resetToggleOverrideCache() { val cachedToggleOverride = DesktopModeFlags::class.java.getDeclaredField("cachedToggleOverride") cachedToggleOverride.isAccessible = true diff --git a/nfc/java/android/nfc/flags.aconfig b/nfc/java/android/nfc/flags.aconfig index 95945d77730d..f16fa801a3e6 100644 --- a/nfc/java/android/nfc/flags.aconfig +++ b/nfc/java/android/nfc/flags.aconfig @@ -118,3 +118,10 @@ flag { bug: "321310044" } +flag { + name: "nfc_action_manage_services_settings" + is_exported: true + namespace: "nfc" + description: "Add Settings.ACTION_MANAGE_OTHER_NFC_SERVICES_SETTINGS" + bug: "358129872" +} diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java index 0fec61c5affe..92da2be60d1e 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java @@ -1026,21 +1026,29 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> return mDevice.getBluetoothClass(); } + /** + * Returns a list of {@link LocalBluetoothProfile} supported by the device. + */ public List<LocalBluetoothProfile> getProfiles() { return new ArrayList<>(mProfiles); } - public List<LocalBluetoothProfile> getConnectableProfiles() { - List<LocalBluetoothProfile> connectableProfiles = - new ArrayList<LocalBluetoothProfile>(); + /** + * Returns a list of {@link LocalBluetoothProfile} that are user-accessible from UI to + * initiate a connection. + * + * Note: Use {@link #getProfiles()} to retrieve all supported profiles on the device. + */ + public List<LocalBluetoothProfile> getUiAccessibleProfiles() { + List<LocalBluetoothProfile> accessibleProfiles = new ArrayList<>(); synchronized (mProfileLock) { for (LocalBluetoothProfile profile : mProfiles) { if (profile.accessProfileEnabled()) { - connectableProfiles.add(profile); + accessibleProfiles.add(profile); } } } - return connectableProfiles; + return accessibleProfiles; } public List<LocalBluetoothProfile> getRemovedProfiles() { diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java index a49314aae1b3..7124ed2d96b8 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java @@ -261,9 +261,9 @@ public class CsipDeviceManager { } CachedBluetoothDevice dualModeDevice = groupDevicesList.stream() - .filter(cachedDevice -> cachedDevice.getConnectableProfiles().stream() + .filter(cachedDevice -> cachedDevice.getUiAccessibleProfiles().stream() .anyMatch(profile -> profile instanceof LeAudioProfile)) - .filter(cachedDevice -> cachedDevice.getConnectableProfiles().stream() + .filter(cachedDevice -> cachedDevice.getUiAccessibleProfiles().stream() .anyMatch(profile -> profile instanceof A2dpProfile || profile instanceof HeadsetProfile)) .findFirst().orElse(null); diff --git a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java index 72a60fbc9fea..fe6659d1dc4f 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java @@ -634,7 +634,7 @@ public class LocalMediaManager implements BluetoothCallback { } private boolean isMediaDevice(CachedBluetoothDevice device) { - for (LocalBluetoothProfile profile : device.getConnectableProfiles()) { + for (LocalBluetoothProfile profile : device.getUiAccessibleProfiles()) { if (profile instanceof A2dpProfile || profile instanceof HearingAidProfile || profile instanceof LeAudioProfile) { return true; diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java index 22e6133a3019..f7492cfc9a72 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java +++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java @@ -40,13 +40,17 @@ public class TestModeBuilder { private ZenModeConfig.ZenRule mConfigZenRule; public static final ZenMode EXAMPLE = new TestModeBuilder().build(); - public static final ZenMode MANUAL_DND = ZenMode.manualDndMode( - new AutomaticZenRule.Builder("Manual DND", Uri.parse("rule://dnd")) - .setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_PRIORITY) - .setZenPolicy(new ZenPolicy.Builder().disallowAllSounds().build()) - .build(), - true /* isActive */ - ); + public static final ZenMode MANUAL_DND_ACTIVE = manualDnd(Uri.EMPTY, true); + public static final ZenMode MANUAL_DND_INACTIVE = manualDnd(Uri.EMPTY, false); + + public static ZenMode manualDnd(Uri conditionId, boolean isActive) { + return ZenMode.manualDndMode( + new AutomaticZenRule.Builder("Do Not Disturb", conditionId) + .setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().disallowAllSounds().build()) + .build(), + isActive); + } public TestModeBuilder() { // Reasonable defaults diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java index 88497a393ce8..2f4b2efeec7f 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java +++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java @@ -22,6 +22,7 @@ import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL; import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY; import static android.service.notification.SystemZenRules.getTriggerDescriptionForScheduleEvent; import static android.service.notification.SystemZenRules.getTriggerDescriptionForScheduleTime; +import static android.service.notification.ZenModeConfig.tryParseCountdownConditionId; import static android.service.notification.ZenModeConfig.tryParseEventConditionId; import static android.service.notification.ZenModeConfig.tryParseScheduleConditionId; @@ -188,11 +189,37 @@ public class ZenMode implements Parcelable { return mRule.getType(); } + /** Returns the trigger description of the mode. */ @Nullable public String getTriggerDescription() { return mRule.getTriggerDescription(); } + /** + * Returns a "dynamic" trigger description. For some modes (such as manual Do Not Disturb) + * when activated, we know when (and if) the mode is expected to end on its own; this dynamic + * description reflects that. In other cases, returns {@link #getTriggerDescription}. + */ + @Nullable + public String getDynamicDescription(Context context) { + if (isManualDnd() && isActive()) { + long countdownEndTime = tryParseCountdownConditionId(mRule.getConditionId()); + if (countdownEndTime > 0) { + CharSequence formattedTime = ZenModeConfig.getFormattedTime(context, + countdownEndTime, ZenModeConfig.isToday(countdownEndTime), + context.getUserId()); + return context.getString(com.android.internal.R.string.zen_mode_until, + formattedTime); + } + } + // TODO: b/333527800 - For TYPE_SCHEDULE_TIME rules we could do the same; however + // according to the snoozing discussions the mode may or may not end at the scheduled + // time if manually activated. When we resolve that point, we could calculate end time + // for these modes as well. + + return getTriggerDescription(); + } + @NonNull public ListenableFuture<Drawable> getIcon(@NonNull Context context, @NonNull ZenIconLoader iconLoader) { diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenModesBackend.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenModesBackend.java index f533c951d7f8..492828d701b9 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenModesBackend.java +++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenModesBackend.java @@ -116,7 +116,6 @@ public class ZenModesBackend { private ZenMode getManualDndMode(ZenModeConfig config) { ZenModeConfig.ZenRule manualRule = config.manualRule; - // TODO: b/333682392 - Replace with final strings for name & trigger description AutomaticZenRule manualDndRule = new AutomaticZenRule.Builder( mContext.getString(R.string.zen_mode_settings_title), manualRule.conditionId) .setType(manualRule.type) @@ -127,7 +126,7 @@ public class ZenModesBackend { .setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_PRIORITY) .build(); - return ZenMode.manualDndMode(manualDndRule, config != null && config.isManualActive()); + return ZenMode.manualDndMode(manualDndRule, config.isManualActive()); } public void updateMode(ZenMode mode) { diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java index 3f59da4bf24e..f94f21fe5d45 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java @@ -145,18 +145,18 @@ public class CsipDeviceManagerTest { profiles.add(mHfpProfile); profiles.add(mA2dpProfile); profiles.add(mLeAudioProfile); - when(mCachedDevice1.getConnectableProfiles()).thenReturn(profiles); + when(mCachedDevice1.getUiAccessibleProfiles()).thenReturn(profiles); when(mCachedDevice1.isConnected()).thenReturn(true); profiles.clear(); profiles.add(mLeAudioProfile); - when(mCachedDevice2.getConnectableProfiles()).thenReturn(profiles); + when(mCachedDevice2.getUiAccessibleProfiles()).thenReturn(profiles); when(mCachedDevice2.isConnected()).thenReturn(true); profiles.clear(); profiles.add(mHfpProfile); profiles.add(mA2dpProfile); - when(mCachedDevice3.getConnectableProfiles()).thenReturn(profiles); + when(mCachedDevice3.getUiAccessibleProfiles()).thenReturn(profiles); when(mCachedDevice3.isConnected()).thenReturn(true); } @@ -253,7 +253,7 @@ public class CsipDeviceManagerTest { when(mDevice2.isConnected()).thenReturn(false); List<LocalBluetoothProfile> profiles = new ArrayList<LocalBluetoothProfile>(); profiles.add(mLeAudioProfile); - when(mCachedDevice1.getConnectableProfiles()).thenReturn(profiles); + when(mCachedDevice1.getUiAccessibleProfiles()).thenReturn(profiles); CachedBluetoothDevice expectedDevice = mCachedDevice1; assertThat( diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java index a30d6a787971..3e8457b427fc 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java @@ -470,7 +470,7 @@ public class LocalMediaManagerTest { when(cachedManager.findDevice(bluetoothDevice)).thenReturn(cachedDevice); when(cachedDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED); when(cachedDevice.isConnected()).thenReturn(false); - when(cachedDevice.getConnectableProfiles()).thenReturn(profiles); + when(cachedDevice.getUiAccessibleProfiles()).thenReturn(profiles); when(cachedDevice.getDevice()).thenReturn(bluetoothDevice); when(cachedDevice.getAddress()).thenReturn(TEST_ADDRESS); when(mA2dpProfile.getActiveDevice()).thenReturn(bluetoothDevice); diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java index 03c2a83519d8..65937ea067c6 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java @@ -445,7 +445,6 @@ public class GlobalSettingsValidators { String.valueOf(Global.Wearable.TETHERED_CONFIG_TETHERED), String.valueOf(Global.Wearable.TETHERED_CONFIG_RESTRICTED) })); - VALIDATORS.put(Global.Wearable.PHONE_SWITCHING_SUPPORTED, BOOLEAN_VALIDATOR); VALIDATORS.put(Global.Wearable.WEAR_LAUNCHER_UI_MODE, ANY_INTEGER_VALIDATOR); VALIDATORS.put(Global.Wearable.WEAR_POWER_ANOMALY_SERVICE_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Global.Wearable.CONNECTIVITY_KEEP_DATA_ON, BOOLEAN_VALIDATOR); @@ -457,5 +456,10 @@ public class GlobalSettingsValidators { VALIDATORS.put(Global.ADD_USERS_WHEN_LOCKED, BOOLEAN_VALIDATOR); VALIDATORS.put(Global.REMOVE_GUEST_ON_EXIT, BOOLEAN_VALIDATOR); VALIDATORS.put(Global.USER_SWITCHER_ENABLED, BOOLEAN_VALIDATOR); + VALIDATORS.put(Global.Wearable.PHONE_SWITCHING_REQUEST_SOURCE, + new InclusiveIntegerRangeValidator( + Global.Wearable.PHONE_SWITCHING_REQUEST_SOURCE_NONE, + Global.Wearable.PHONE_SWITCHING_REQUEST_SOURCE_COMPANION + )); } } diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java index 8c9648437b17..d39b5645109d 100644 --- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java +++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java @@ -629,11 +629,11 @@ public class SettingsBackupTest { Settings.Global.Wearable.CUSTOM_COLOR_BACKGROUND, Settings.Global.Wearable.PHONE_SWITCHING_STATUS, Settings.Global.Wearable.TETHER_CONFIG_STATE, - Settings.Global.Wearable.PHONE_SWITCHING_SUPPORTED, Settings.Global.Wearable.WEAR_MEDIA_CONTROLS_PACKAGE, Settings.Global.Wearable.WEAR_MEDIA_SESSIONS_PACKAGE, Settings.Global.Wearable.WEAR_POWER_ANOMALY_SERVICE_ENABLED, - Settings.Global.Wearable.CONNECTIVITY_KEEP_DATA_ON); + Settings.Global.Wearable.CONNECTIVITY_KEEP_DATA_ON, + Settings.Global.Wearable.PHONE_SWITCHING_REQUEST_SOURCE); private static final Set<String> BACKUP_DENY_LIST_SECURE_SETTINGS = newHashSet( @@ -677,6 +677,7 @@ public class SettingsBackupTest { Settings.Secure.CAMERA_LIFT_TRIGGER_ENABLED, // Candidate for backup? Settings.Secure.CARRIER_APPS_HANDLED, Settings.Secure.CMAS_ADDITIONAL_BROADCAST_PKG, + Settings.Secure.COMPAT_UI_EDUCATION_SHOWING, Settings.Secure.COMPLETED_CATEGORY_PREFIX, Settings.Secure.CONNECTIVITY_RELEASE_PENDING_INTENT_DELAY_MS, Settings.Secure.CONTENT_CAPTURE_ENABLED, diff --git a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/model/A11yMenuShortcut.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/model/A11yMenuShortcut.java index 66a2fae0c4c3..c698d18bfde8 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/model/A11yMenuShortcut.java +++ b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/model/A11yMenuShortcut.java @@ -19,7 +19,6 @@ import android.util.Log; import com.android.systemui.accessibility.accessibilitymenu.R; -import java.util.HashMap; import java.util.Map; /** @@ -52,80 +51,80 @@ public class A11yMenuShortcut { private static final int LABEL_TEXT_INDEX = 3; /** Map stores all shortcut resource IDs that is in matching order of defined shortcut. */ - private static final Map<ShortcutId, int[]> sShortcutResource = new HashMap<>() {{ - put(ShortcutId.ID_ASSISTANT_VALUE, new int[] { + private static final Map<ShortcutId, int[]> sShortcutResource = Map.ofEntries( + Map.entry(ShortcutId.ID_ASSISTANT_VALUE, new int[] { R.drawable.ic_logo_a11y_assistant_24dp, R.color.assistant_color, R.string.assistant_utterance, R.string.assistant_label, - }); - put(ShortcutId.ID_A11YSETTING_VALUE, new int[] { + }), + Map.entry(ShortcutId.ID_A11YSETTING_VALUE, new int[] { R.drawable.ic_logo_a11y_settings_24dp, R.color.a11y_settings_color, R.string.a11y_settings_label, R.string.a11y_settings_label, - }); - put(ShortcutId.ID_POWER_VALUE, new int[] { + }), + Map.entry(ShortcutId.ID_POWER_VALUE, new int[] { R.drawable.ic_logo_a11y_power_24dp, R.color.power_color, R.string.power_utterance, R.string.power_label, - }); - put(ShortcutId.ID_RECENT_VALUE, new int[] { + }), + Map.entry(ShortcutId.ID_RECENT_VALUE, new int[] { R.drawable.ic_logo_a11y_recent_apps_24dp, R.color.recent_apps_color, R.string.recent_apps_label, R.string.recent_apps_label, - }); - put(ShortcutId.ID_LOCKSCREEN_VALUE, new int[] { + }), + Map.entry(ShortcutId.ID_LOCKSCREEN_VALUE, new int[] { R.drawable.ic_logo_a11y_lock_24dp, R.color.lockscreen_color, R.string.lockscreen_label, R.string.lockscreen_label, - }); - put(ShortcutId.ID_QUICKSETTING_VALUE, new int[] { + }), + Map.entry(ShortcutId.ID_QUICKSETTING_VALUE, new int[] { R.drawable.ic_logo_a11y_quick_settings_24dp, R.color.quick_settings_color, R.string.quick_settings_label, R.string.quick_settings_label, - }); - put(ShortcutId.ID_NOTIFICATION_VALUE, new int[] { + }), + Map.entry(ShortcutId.ID_NOTIFICATION_VALUE, new int[] { R.drawable.ic_logo_a11y_notifications_24dp, R.color.notifications_color, R.string.notifications_label, R.string.notifications_label, - }); - put(ShortcutId.ID_SCREENSHOT_VALUE, new int[] { + }), + Map.entry(ShortcutId.ID_SCREENSHOT_VALUE, new int[] { R.drawable.ic_logo_a11y_screenshot_24dp, R.color.screenshot_color, R.string.screenshot_utterance, R.string.screenshot_label, - }); - put(ShortcutId.ID_BRIGHTNESS_UP_VALUE, new int[] { + }), + Map.entry(ShortcutId.ID_BRIGHTNESS_UP_VALUE, new int[] { R.drawable.ic_logo_a11y_brightness_up_24dp, R.color.brightness_color, R.string.brightness_up_label, R.string.brightness_up_label, - }); - put(ShortcutId.ID_BRIGHTNESS_DOWN_VALUE, new int[] { + }), + Map.entry(ShortcutId.ID_BRIGHTNESS_DOWN_VALUE, new int[] { R.drawable.ic_logo_a11y_brightness_down_24dp, R.color.brightness_color, R.string.brightness_down_label, R.string.brightness_down_label, - }); - put(ShortcutId.ID_VOLUME_UP_VALUE, new int[] { + }), + Map.entry(ShortcutId.ID_VOLUME_UP_VALUE, new int[] { R.drawable.ic_logo_a11y_volume_up_24dp, R.color.volume_color, R.string.volume_up_label, R.string.volume_up_label, - }); - put(ShortcutId.ID_VOLUME_DOWN_VALUE, new int[] { + }), + Map.entry(ShortcutId.ID_VOLUME_DOWN_VALUE, new int[] { R.drawable.ic_logo_a11y_volume_down_24dp, R.color.volume_color, R.string.volume_down_label, R.string.volume_down_label, - }); - }}; + }) + ); /** Shortcut id used to identify. */ private int mShortcutId = ShortcutId.UNSPECIFIED_ID_VALUE.ordinal(); diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 19dced526692..e6fae7b588ce 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -1197,6 +1197,17 @@ flag { } flag { + name: "hubmode_fullscreen_vertical_swipe_fix" + namespace: "systemui" + description: "Bug fix that enables fullscreen vertical swiping in hub mode to bring up and down the bouncer and shade" + bug: "340177049" + metadata { + purpose: PURPOSE_BUGFIX + } +} + + +flag { namespace: "systemui" name: "remove_update_listener_in_qs_icon_view_impl" description: "Remove update listeners in QsIconViewImpl class to avoid memory leak." diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterContentObserverSyncViaSettingsProxyDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterContentObserverSyncViaSettingsProxyDetector.kt new file mode 100644 index 000000000000..d8c7c06c8c5b --- /dev/null +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterContentObserverSyncViaSettingsProxyDetector.kt @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2022 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.internal.systemui.lint + +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Implementation +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.Scope +import com.android.tools.lint.detector.api.Severity +import com.android.tools.lint.detector.api.SourceCodeScanner +import com.intellij.psi.PsiMethod +import org.jetbrains.uast.UCallExpression + +/** + * Checks if the synchronous APIs like registerContentObserverSync/unregisterContentObserverSync are + * invoked for SettingsProxy or it's sub-classes, and raise a warning notifying the caller to use + * the asynchronous/suspend APIs instead. + */ +@Suppress("UnstableApiUsage") +class RegisterContentObserverSyncViaSettingsProxyDetector : Detector(), SourceCodeScanner { + + override fun getApplicableMethodNames(): List<String> { + return SYNC_METHOD_LIST + } + + override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) { + + val evaluator = context.evaluator + if (evaluator.isMemberInSubClassOf(method, SETTINGS_PROXY_CLASS)) { + context.report( + issue = SYNC_WARNING, + location = context.getNameLocation(node), + message = + "`Avoid using ${method.name}()` if calling the API is not " + + "required on the main thread. Instead use an appropriate async interface " + + "API call for eg. `registerContentObserver()` or " + + "`registerContentObserverAsync()`." + ) + } + } + + companion object { + val SYNC_WARNING: Issue = + Issue.create( + id = "RegisterContentObserverSyncWarning", + briefDescription = + "Synchronous content observer registration API called " + + "instead of the async APIs.`", + // lint trims indents and converts \ to line continuations + explanation = + """ + ContentObserver registration/de-registration done via \ + `SettingsProxy.registerContentObserverSync` will block the main thread \ + and may cause missed frames. Instead, use \ + `SettingsProxy.registerContentObserver()` or \ + `SettingsProxy.registerContentObserverAsync()`. These APIs will ensure \ + that the registrations/de-registrations happen sequentially on a + background worker thread.""", + category = Category.PERFORMANCE, + priority = 8, + severity = Severity.WARNING, + implementation = + Implementation( + RegisterContentObserverSyncViaSettingsProxyDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) + ) + + private val SYNC_METHOD_LIST = + listOf( + "registerContentObserverSync", + "unregisterContentObserverSync", + "registerContentObserverForUserSync" + ) + + private val SETTINGS_PROXY_CLASS = "com.android.systemui.util.settings.SettingsProxy" + } +} diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt index 73ac6ccf8f76..5206b05a3f4e 100644 --- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt @@ -46,10 +46,12 @@ class SystemUIIssueRegistry : IssueRegistry() { DemotingTestWithoutBugDetector.ISSUE, TestFunctionNameViolationDetector.ISSUE, MissingApacheLicenseDetector.ISSUE, + RegisterContentObserverSyncViaSettingsProxyDetector.SYNC_WARNING ) override val api: Int get() = CURRENT_API + override val minApi: Int get() = 8 diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterContentObserverSyncViaSettingsProxyDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterContentObserverSyncViaSettingsProxyDetectorTest.kt new file mode 100644 index 000000000000..57347d351543 --- /dev/null +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterContentObserverSyncViaSettingsProxyDetectorTest.kt @@ -0,0 +1,211 @@ +/* + * 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.internal.systemui.lint + +import com.android.tools.lint.checks.infrastructure.TestFiles +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Issue +import org.junit.Test + +/** Test class for [RegisterContentObserverSyncViaSettingsProxyDetector]. */ +class RegisterContentObserverSyncViaSettingsProxyDetectorTest : SystemUILintDetectorTest() { + override fun getDetector(): Detector = RegisterContentObserverSyncViaSettingsProxyDetector() + + override fun getIssues(): List<Issue> = + listOf(RegisterContentObserverSyncViaSettingsProxyDetector.SYNC_WARNING) + + @Test + fun testRegisterContentObserverSync_throwError() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import com.android.systemui.util.settings.SecureSettings; + public class TestClass { + public void register(SecureSettings secureSettings) { + secureSettings. + registerContentObserverSync(Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED), + false, mSettingObserver); + } + } + """ + ) + .indented(), + *stubs + ) + .issues(RegisterContentObserverSyncViaSettingsProxyDetector.SYNC_WARNING) + .run() + .expect( + """ + src/test/pkg/TestClass.java:6: Warning: Avoid using registerContentObserverSync() if calling the API is not required on the main thread. Instead use an appropriate async interface API call for eg. registerContentObserver() or registerContentObserverAsync(). [RegisterContentObserverSyncWarning] + registerContentObserverSync(Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~ +0 errors, 1 warnings + """ + .trimIndent() + ) + } + + @Test + fun testRegisterContentObserverForUserSync_throwError() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import com.android.systemui.util.settings.SecureSettings; + public class TestClass { + public void register(SecureSettings secureSettings) { + secureSettings. + registerContentObserverForUserSync(Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED), + false, mSettingObserver); + } + } + """ + ) + .indented(), + *stubs + ) + .issues(RegisterContentObserverSyncViaSettingsProxyDetector.SYNC_WARNING) + .run() + .expect( + """ + src/test/pkg/TestClass.java:6: Warning: Avoid using registerContentObserverForUserSync() if calling the API is not required on the main thread. Instead use an appropriate async interface API call for eg. registerContentObserver() or registerContentObserverAsync(). [RegisterContentObserverSyncWarning] + registerContentObserverForUserSync(Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +0 errors, 1 warnings + """ + .trimIndent() + ) + } + + @Test + fun testSuppressRegisterContentObserverSync() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import com.android.systemui.util.settings.SecureSettings; + public class TestClass { + @SuppressWarnings("RegisterContentObserverSyncWarning") + public void register(SecureSettings secureSettings) { + secureSettings. + registerContentObserverForUserSync(Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED), + false, mSettingObserver); + } + } + """ + ) + .indented(), + *stubs + ) + .issues(RegisterContentObserverSyncViaSettingsProxyDetector.SYNC_WARNING) + .run() + .expectClean() + } + + @Test + fun testNoopIfNoCall() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import com.android.systemui.util.settings.SecureSettings; + public class TestClass { + public void register(SecureSettings secureSettings) { + } + } + """ + ) + .indented(), + *stubs + ) + .issues(RegisterContentObserverSyncViaSettingsProxyDetector.SYNC_WARNING) + .run() + .expectClean() + } + + @Test + fun testUnRegisterContentObserverSync_throwError() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import com.android.systemui.util.settings.SecureSettings; + public class TestClass { + public void register(SecureSettings secureSettings) { + secureSettings. + unregisterContentObserverSync(mSettingObserver); + } + } + """ + ) + .indented(), + *stubs + ) + .issues(RegisterContentObserverSyncViaSettingsProxyDetector.SYNC_WARNING) + .run() + .expect( + """ + src/test/pkg/TestClass.java:6: Warning: Avoid using unregisterContentObserverSync() if calling the API is not required on the main thread. Instead use an appropriate async interface API call for eg. registerContentObserver() or registerContentObserverAsync(). [RegisterContentObserverSyncWarning] + unregisterContentObserverSync(mSettingObserver); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +0 errors, 1 warnings + """ + .trimIndent() + ) + } + + private companion object { + private val SETTINGS_PROXY_STUB = + kotlin( + """ + package com.android.systemui.util.settings + interface SettingsProxy { + fun registerContentObserverSync() {} + fun unregisterContentObserverSync() {} + } + """ + ) + .indented() + + private val USER_SETTINGS_PROXY_STUB = + kotlin( + """ + package com.android.systemui.util.settings + interface UserSettingsProxy : SettingsProxy { + fun registerContentObserverForUserSync() {} + } + """ + ) + .indented() + + private val SECURE_SETTINGS_STUB = + kotlin( + """ + package com.android.systemui.util.settings + interface SecureSettings : UserSettingsProxy {} + """ + ) + .indented() + } + + private val stubs = arrayOf(SETTINGS_PROXY_STUB, USER_SETTINGS_PROXY_STUB, SECURE_SETTINGS_STUB) +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt index 69f117431663..b65b47123eaa 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt @@ -160,6 +160,7 @@ import com.android.compose.modifiers.thenIf import com.android.compose.theme.LocalAndroidColorScheme import com.android.compose.ui.graphics.painter.rememberDrawablePainter import com.android.internal.R.dimen.system_app_widget_background_radius +import com.android.systemui.Flags import com.android.systemui.Flags.communalTimerFlickerFix import com.android.systemui.communal.domain.model.CommunalContentModel import com.android.systemui.communal.shared.model.CommunalContentSize @@ -269,7 +270,7 @@ fun CommunalHub( } } // Nested scroll for full screen swipe to get to shade and bouncer - .thenIf(!viewModel.isEditMode) { + .thenIf(!viewModel.isEditMode && Flags.hubmodeFullscreenVerticalSwipeFix()) { Modifier.nestedScroll(nestedScrollConnection).pointerInput(viewModel) { awaitPointerEventScope { while (true) { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenSceneBlueprintModule.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenSceneBlueprintModule.kt index 9afb4d5b7523..a78c038595f1 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenSceneBlueprintModule.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenSceneBlueprintModule.kt @@ -17,7 +17,6 @@ package com.android.systemui.keyguard.ui.composable import com.android.systemui.keyguard.ui.composable.blueprint.CommunalBlueprintModule -import com.android.systemui.keyguard.ui.composable.blueprint.ShortcutsBesideUdfpsBlueprintModule import com.android.systemui.keyguard.ui.composable.section.OptionalSectionModule import dagger.Module @@ -26,7 +25,6 @@ import dagger.Module [ CommunalBlueprintModule::class, OptionalSectionModule::class, - ShortcutsBesideUdfpsBlueprintModule::class, ], ) interface LockscreenSceneBlueprintModule diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt deleted file mode 100644 index a5e120c6f04e..000000000000 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt +++ /dev/null @@ -1,259 +0,0 @@ -/* - * Copyright (C) 2023 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.keyguard.ui.composable.blueprint - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.layout.Layout -import androidx.compose.ui.unit.IntRect -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.android.compose.animation.scene.SceneScope -import com.android.compose.modifiers.padding -import com.android.systemui.keyguard.ui.composable.LockscreenLongPress -import com.android.systemui.keyguard.ui.composable.section.AmbientIndicationSection -import com.android.systemui.keyguard.ui.composable.section.BottomAreaSection -import com.android.systemui.keyguard.ui.composable.section.LockSection -import com.android.systemui.keyguard.ui.composable.section.NotificationSection -import com.android.systemui.keyguard.ui.composable.section.SettingsMenuSection -import com.android.systemui.keyguard.ui.composable.section.StatusBarSection -import com.android.systemui.keyguard.ui.composable.section.TopAreaSection -import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel -import dagger.Binds -import dagger.Module -import dagger.multibindings.IntoSet -import java.util.Optional -import javax.inject.Inject -import kotlin.math.roundToInt - -/** - * Renders the lockscreen scene when showing with the default layout (e.g. vertical phone form - * factor). - */ -class ShortcutsBesideUdfpsBlueprint -@Inject -constructor( - private val statusBarSection: StatusBarSection, - private val lockSection: LockSection, - private val ambientIndicationSectionOptional: Optional<AmbientIndicationSection>, - private val bottomAreaSection: BottomAreaSection, - private val settingsMenuSection: SettingsMenuSection, - private val topAreaSection: TopAreaSection, - private val notificationSection: NotificationSection, -) : ComposableLockscreenSceneBlueprint { - - override val id: String = "shortcuts-besides-udfps" - - @Composable - override fun SceneScope.Content( - viewModel: LockscreenContentViewModel, - modifier: Modifier, - ) { - val isUdfpsVisible = viewModel.isUdfpsVisible - val isShadeLayoutWide by viewModel.isShadeLayoutWide.collectAsStateWithLifecycle() - val unfoldTranslations by viewModel.unfoldTranslations.collectAsStateWithLifecycle() - val areNotificationsVisible by - viewModel - .areNotificationsVisible(contentKey) - .collectAsStateWithLifecycle(initialValue = false) - - LockscreenLongPress( - viewModel = viewModel.touchHandling, - modifier = modifier, - ) { onSettingsMenuPlaced -> - Layout( - content = { - // Constrained to above the lock icon. - Column( - modifier = Modifier.fillMaxSize(), - ) { - with(statusBarSection) { - StatusBar( - modifier = - Modifier.fillMaxWidth() - .padding( - horizontal = { unfoldTranslations.start.roundToInt() }, - ) - ) - } - - Box { - with(topAreaSection) { - DefaultClockLayout( - smartSpacePaddingTop = viewModel::getSmartSpacePaddingTop, - modifier = - Modifier.graphicsLayer { - translationX = unfoldTranslations.start - }, - ) - } - if (isShadeLayoutWide) { - with(notificationSection) { - Notifications( - areNotificationsVisible = areNotificationsVisible, - isShadeLayoutWide = isShadeLayoutWide, - burnInParams = null, - modifier = - Modifier.fillMaxWidth(0.5f) - .fillMaxHeight() - .align(alignment = Alignment.TopEnd) - ) - } - } - } - if (!isShadeLayoutWide) { - with(notificationSection) { - Notifications( - areNotificationsVisible = areNotificationsVisible, - isShadeLayoutWide = isShadeLayoutWide, - burnInParams = null, - modifier = Modifier.weight(weight = 1f) - ) - } - } - if (!isUdfpsVisible && ambientIndicationSectionOptional.isPresent) { - with(ambientIndicationSectionOptional.get()) { - AmbientIndication(modifier = Modifier.fillMaxWidth()) - } - } - } - - // Constrained to the left of the lock icon (in left-to-right layouts). - with(bottomAreaSection) { - Shortcut( - isStart = true, - applyPadding = false, - modifier = - Modifier.graphicsLayer { translationX = unfoldTranslations.start }, - ) - } - - with(lockSection) { LockIcon() } - - // Constrained to the right of the lock icon (in left-to-right layouts). - with(bottomAreaSection) { - Shortcut( - isStart = false, - applyPadding = false, - modifier = - Modifier.graphicsLayer { translationX = unfoldTranslations.end }, - ) - } - - // Aligned to bottom and constrained to below the lock icon. - Column(modifier = Modifier.fillMaxWidth()) { - if (isUdfpsVisible && ambientIndicationSectionOptional.isPresent) { - with(ambientIndicationSectionOptional.get()) { - AmbientIndication(modifier = Modifier.fillMaxWidth()) - } - } - - with(bottomAreaSection) { - IndicationArea(modifier = Modifier.fillMaxWidth()) - } - } - - // Aligned to bottom and NOT constrained by the lock icon. - with(settingsMenuSection) { SettingsMenu(onSettingsMenuPlaced) } - }, - modifier = Modifier.fillMaxSize(), - ) { measurables, constraints -> - check(measurables.size == 6) - val aboveLockIconMeasurable = measurables[0] - val startSideShortcutMeasurable = measurables[1] - val lockIconMeasurable = measurables[2] - val endSideShortcutMeasurable = measurables[3] - val belowLockIconMeasurable = measurables[4] - val settingsMenuMeasurable = measurables[5] - - val noMinConstraints = - constraints.copy( - minWidth = 0, - minHeight = 0, - ) - - val lockIconPlaceable = lockIconMeasurable.measure(noMinConstraints) - val lockIconBounds = - IntRect( - left = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Left], - top = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Top], - right = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Right], - bottom = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Bottom], - ) - - val aboveLockIconPlaceable = - aboveLockIconMeasurable.measure( - noMinConstraints.copy(maxHeight = lockIconBounds.top) - ) - val startSideShortcutPlaceable = - startSideShortcutMeasurable.measure(noMinConstraints) - val endSideShortcutPlaceable = endSideShortcutMeasurable.measure(noMinConstraints) - val belowLockIconPlaceable = - belowLockIconMeasurable.measure( - noMinConstraints.copy( - maxHeight = constraints.maxHeight - lockIconBounds.bottom - ) - ) - val settingsMenuPlaceable = settingsMenuMeasurable.measure(noMinConstraints) - - layout(constraints.maxWidth, constraints.maxHeight) { - aboveLockIconPlaceable.place( - x = 0, - y = 0, - ) - startSideShortcutPlaceable.placeRelative( - x = lockIconBounds.left / 2 - startSideShortcutPlaceable.width / 2, - y = lockIconBounds.center.y - startSideShortcutPlaceable.height / 2, - ) - lockIconPlaceable.place( - x = lockIconBounds.left, - y = lockIconBounds.top, - ) - endSideShortcutPlaceable.placeRelative( - x = - lockIconBounds.right + - (constraints.maxWidth - lockIconBounds.right) / 2 - - endSideShortcutPlaceable.width / 2, - y = lockIconBounds.center.y - endSideShortcutPlaceable.height / 2, - ) - belowLockIconPlaceable.place( - x = 0, - y = constraints.maxHeight - belowLockIconPlaceable.height, - ) - settingsMenuPlaceable.place( - x = (constraints.maxWidth - settingsMenuPlaceable.width) / 2, - y = constraints.maxHeight - settingsMenuPlaceable.height, - ) - } - } - } - } -} - -@Module -interface ShortcutsBesideUdfpsBlueprintModule { - @Binds - @IntoSet - fun blueprint(blueprint: ShortcutsBesideUdfpsBlueprint): ComposableLockscreenSceneBlueprint -} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/BottomAreaSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/BottomAreaSection.kt index 9c72d933da32..364adcaffd77 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/BottomAreaSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/BottomAreaSection.kt @@ -32,7 +32,6 @@ import androidx.compose.ui.viewinterop.AndroidView import androidx.core.content.res.ResourcesCompat import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.SceneScope -import com.android.keyguard.logging.KeyguardQuickAffordancesLogger import com.android.systemui.animation.view.LaunchableImageView import com.android.systemui.keyguard.ui.binder.KeyguardIndicationAreaBinder import com.android.systemui.keyguard.ui.binder.KeyguardQuickAffordanceViewBinder @@ -40,10 +39,8 @@ import com.android.systemui.keyguard.ui.view.KeyguardIndicationArea import com.android.systemui.keyguard.ui.viewmodel.KeyguardIndicationAreaViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordancesCombinedViewModel -import com.android.systemui.plugins.FalsingManager import com.android.systemui.res.R import com.android.systemui.statusbar.KeyguardIndicationController -import com.android.systemui.statusbar.VibratorHelper import javax.inject.Inject import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.flow.Flow @@ -52,11 +49,9 @@ class BottomAreaSection @Inject constructor( private val viewModel: KeyguardQuickAffordancesCombinedViewModel, - private val falsingManager: FalsingManager, - private val vibratorHelper: VibratorHelper, private val indicationController: KeyguardIndicationController, private val indicationAreaViewModel: KeyguardIndicationAreaViewModel, - private val shortcutsLogger: KeyguardQuickAffordancesLogger, + private val keyguardQuickAffordanceViewBinder: KeyguardQuickAffordanceViewBinder, ) { /** * Renders a single lockscreen shortcut. @@ -80,9 +75,8 @@ constructor( viewId = if (isStart) R.id.start_button else R.id.end_button, viewModel = if (isStart) viewModel.startButton else viewModel.endButton, transitionAlpha = viewModel.transitionAlpha, - falsingManager = falsingManager, - vibratorHelper = vibratorHelper, indicationController = indicationController, + binder = keyguardQuickAffordanceViewBinder, modifier = if (applyPadding) { Modifier.shortcutPadding() @@ -124,9 +118,8 @@ constructor( @IdRes viewId: Int, viewModel: Flow<KeyguardQuickAffordanceViewModel>, transitionAlpha: Flow<Float>, - falsingManager: FalsingManager, - vibratorHelper: VibratorHelper, indicationController: KeyguardIndicationController, + binder: KeyguardQuickAffordanceViewBinder, modifier: Modifier = Modifier, ) { val (binding, setBinding) = mutableStateOf<KeyguardQuickAffordanceViewBinder.Binding?>(null) @@ -158,13 +151,10 @@ constructor( } setBinding( - KeyguardQuickAffordanceViewBinder.bind( + binder.bind( view, viewModel, transitionAlpha, - falsingManager, - vibratorHelper, - shortcutsLogger, ) { indicationController.showTransientIndication(it) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt index 84782fdfc0af..0bef05dc00ba 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt @@ -276,6 +276,7 @@ fun SceneScope.NotificationScrollingStack( shouldReserveSpaceForNavBar: Boolean = true, shouldIncludeHeadsUpSpace: Boolean = true, shadeMode: ShadeMode, + onEmptySpaceClick: (() -> Unit)? = null, modifier: Modifier = Modifier, ) { val coroutineScope = rememberCoroutineScope() @@ -328,8 +329,6 @@ fun SceneScope.NotificationScrollingStack( // The height of the scrim visible on screen when it is in its resting (collapsed) state. val minVisibleScrimHeight: () -> Float = { screenHeight - maxScrimTop() } - val isClickable by viewModel.isClickable.collectAsStateWithLifecycle() - // we are not scrolled to the top unless the scrim is at its maximum offset. LaunchedEffect(viewModel, scrimOffset) { snapshotFlow { scrimOffset.value >= 0f } @@ -437,8 +436,8 @@ fun SceneScope.NotificationScrollingStack( ) ) } - .thenIf(isClickable) { - Modifier.clickable(onClick = { viewModel.onEmptySpaceClicked() }) + .thenIf(onEmptySpaceClick != null) { + Modifier.clickable(onClick = { onEmptySpaceClick?.invoke() }) } ) { // Creates a cutout in the background scrim in the shape of the notifications scrim. diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt index 7159def8d60a..66be7bc83c64 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt @@ -20,15 +20,19 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.compose.animation.scene.SceneScope import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.systemui.battery.BatteryMeterViewController import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.ui.composable.LockscreenContent -import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeSceneViewModel +import com.android.systemui.lifecycle.rememberViewModel +import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeSceneActionsViewModel +import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeSceneContentViewModel import com.android.systemui.scene.session.ui.composable.SaveableSession import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.ui.composable.ComposableScene @@ -50,9 +54,10 @@ import kotlinx.coroutines.flow.Flow class NotificationsShadeScene @Inject constructor( - sceneViewModel: NotificationsShadeSceneViewModel, - private val overlayShadeViewModel: OverlayShadeViewModel, - private val shadeHeaderViewModel: ShadeHeaderViewModel, + private val contentViewModelFactory: NotificationsShadeSceneContentViewModel.Factory, + private val actionsViewModelFactory: NotificationsShadeSceneActionsViewModel.Factory, + private val overlayShadeViewModelFactory: OverlayShadeViewModel.Factory, + private val shadeHeaderViewModelFactory: ShadeHeaderViewModel.Factory, private val notificationsPlaceholderViewModel: NotificationsPlaceholderViewModel, private val tintedIconManagerFactory: TintedIconManager.Factory, private val batteryMeterViewControllerFactory: BatteryMeterViewController.Factory, @@ -64,21 +69,32 @@ constructor( override val key = Scenes.NotificationsShade + private val actionsViewModel: NotificationsShadeSceneActionsViewModel by lazy { + actionsViewModelFactory.create() + } + override val destinationScenes: Flow<Map<UserAction, UserActionResult>> = - sceneViewModel.destinationScenes + actionsViewModel.actions + + override suspend fun activate() { + actionsViewModel.activate() + } @Composable override fun SceneScope.Content( modifier: Modifier, ) { + val viewModel = rememberViewModel { contentViewModelFactory.create() } + val isEmptySpaceClickable by viewModel.isEmptySpaceClickable.collectAsStateWithLifecycle() + OverlayShade( modifier = modifier, - viewModel = overlayShadeViewModel, + viewModelFactory = overlayShadeViewModelFactory, lockscreenContent = lockscreenContent, ) { Column { ExpandedShadeHeader( - viewModel = shadeHeaderViewModel, + viewModelFactory = shadeHeaderViewModelFactory, createTintedIconManager = tintedIconManagerFactory::create, createBatteryMeterViewController = batteryMeterViewControllerFactory::create, statusBarIconController = statusBarIconController, @@ -94,6 +110,8 @@ constructor( shouldFillMaxSize = false, shouldReserveSpaceForNavBar = false, shadeMode = ShadeMode.Dual, + onEmptySpaceClick = + viewModel::onEmptySpaceClicked.takeIf { isEmptySpaceClickable }, modifier = Modifier.fillMaxWidth(), ) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt index cdcd840b2f34..8bba0f4f3651 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt @@ -81,6 +81,7 @@ import com.android.systemui.common.ui.compose.windowinsets.LocalDisplayCutout import com.android.systemui.common.ui.compose.windowinsets.LocalRawScreenHeight import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.media.controls.ui.composable.MediaCarousel import com.android.systemui.media.controls.ui.controller.MediaCarouselController import com.android.systemui.media.controls.ui.view.MediaHost @@ -166,19 +167,23 @@ private fun SceneScope.QuickSettingsScene( ) { val cutoutLocation = LocalDisplayCutout.current.location - val brightnessMirrorShowing by - viewModel.brightnessMirrorViewModel.isShowing.collectAsStateWithLifecycle() + val brightnessMirrorViewModel = rememberViewModel { + viewModel.brightnessMirrorViewModelFactory.create() + } + val brightnessMirrorShowing by brightnessMirrorViewModel.isShowing.collectAsStateWithLifecycle() val contentAlpha by animateFloatAsState( targetValue = if (brightnessMirrorShowing) 0f else 1f, label = "alphaAnimationBrightnessMirrorContentHiding", ) - viewModel.notifications.setAlphaForBrightnessMirror(contentAlpha) - DisposableEffect(Unit) { onDispose { viewModel.notifications.setAlphaForBrightnessMirror(1f) } } + notificationsPlaceholderViewModel.setAlphaForBrightnessMirror(contentAlpha) + DisposableEffect(Unit) { + onDispose { notificationsPlaceholderViewModel.setAlphaForBrightnessMirror(1f) } + } BrightnessMirror( - viewModel = viewModel.brightnessMirrorViewModel, + viewModel = brightnessMirrorViewModel, qsSceneAdapter = viewModel.qsSceneAdapter, modifier = Modifier.thenIf(cutoutLocation != CutoutLocation.CENTER) { @@ -337,7 +342,7 @@ private fun SceneScope.QuickSettingsScene( fadeOut(tween(customizingAnimationDuration)), ) { ExpandedShadeHeader( - viewModel = viewModel.shadeHeaderViewModel, + viewModelFactory = viewModel.shadeHeaderViewModelFactory, createTintedIconManager = createTintedIconManager, createBatteryMeterViewController = createBatteryMeterViewController, @@ -347,7 +352,7 @@ private fun SceneScope.QuickSettingsScene( } else -> CollapsedShadeHeader( - viewModel = viewModel.shadeHeaderViewModel, + viewModelFactory = viewModel.shadeHeaderViewModelFactory, createTintedIconManager = createTintedIconManager, createBatteryMeterViewController = createBatteryMeterViewController, statusBarIconController = statusBarIconController, @@ -417,7 +422,7 @@ private fun SceneScope.QuickSettingsScene( ) NotificationStackCutoffGuideline( stackScrollView = notificationStackScrollView, - viewModel = viewModel.notifications, + viewModel = notificationsPlaceholderViewModel, modifier = Modifier.align(Alignment.BottomCenter).navigationBarsPadding().offset { IntOffset(x = 0, y = screenHeight.roundToInt()) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt index f6d1283e1f29..eea00c4f2935 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt @@ -44,12 +44,14 @@ import com.android.systemui.battery.BatteryMeterViewController import com.android.systemui.brightness.ui.compose.BrightnessSliderContainer import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.ui.composable.LockscreenContent +import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.qs.panels.ui.compose.EditMode import com.android.systemui.qs.panels.ui.compose.TileGrid import com.android.systemui.qs.ui.composable.QuickSettingsShade.Transitions.QuickSettingsLayoutEnter import com.android.systemui.qs.ui.composable.QuickSettingsShade.Transitions.QuickSettingsLayoutExit import com.android.systemui.qs.ui.viewmodel.QuickSettingsContainerViewModel -import com.android.systemui.qs.ui.viewmodel.QuickSettingsShadeSceneViewModel +import com.android.systemui.qs.ui.viewmodel.QuickSettingsShadeSceneActionsViewModel +import com.android.systemui.qs.ui.viewmodel.QuickSettingsShadeSceneContentViewModel import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.ui.composable.ComposableScene import com.android.systemui.shade.ui.composable.ExpandedShadeHeader @@ -66,9 +68,10 @@ import kotlinx.coroutines.flow.Flow class QuickSettingsShadeScene @Inject constructor( - private val viewModel: QuickSettingsShadeSceneViewModel, + private val actionsViewModelFactory: QuickSettingsShadeSceneActionsViewModel.Factory, + private val contentViewModelFactory: QuickSettingsShadeSceneContentViewModel.Factory, private val lockscreenContent: Lazy<Optional<LockscreenContent>>, - private val shadeHeaderViewModel: ShadeHeaderViewModel, + private val shadeHeaderViewModelFactory: ShadeHeaderViewModel.Factory, private val tintedIconManagerFactory: TintedIconManager.Factory, private val batteryMeterViewControllerFactory: BatteryMeterViewController.Factory, private val statusBarIconController: StatusBarIconController, @@ -76,21 +79,26 @@ constructor( override val key = Scenes.QuickSettingsShade + private val actionsViewModel: QuickSettingsShadeSceneActionsViewModel by lazy { + actionsViewModelFactory.create() + } + override val destinationScenes: Flow<Map<UserAction, UserActionResult>> = - viewModel.destinationScenes + actionsViewModel.actions @Composable override fun SceneScope.Content( modifier: Modifier, ) { + val viewModel = rememberViewModel { contentViewModelFactory.create() } OverlayShade( - viewModel = viewModel.overlayShadeViewModel, + viewModelFactory = viewModel.overlayShadeViewModelFactory, lockscreenContent = lockscreenContent, modifier = modifier, ) { Column { ExpandedShadeHeader( - viewModel = shadeHeaderViewModel, + viewModelFactory = shadeHeaderViewModelFactory, createTintedIconManager = tintedIconManagerFactory::create, createBatteryMeterViewController = batteryMeterViewControllerFactory::create, statusBarIconController = statusBarIconController, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt index facbcaffcb5a..445ffcb0c60c 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt @@ -53,6 +53,7 @@ import com.android.compose.animation.scene.LowestZIndexContentPicker import com.android.compose.animation.scene.SceneScope import com.android.compose.windowsizeclass.LocalWindowSizeClass import com.android.systemui.keyguard.ui.composable.LockscreenContent +import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.shared.model.ShadeAlignment import com.android.systemui.shade.ui.viewmodel.OverlayShadeViewModel @@ -63,11 +64,12 @@ import java.util.Optional /** The overlay shade renders a lightweight shade UI container on top of a background scene. */ @Composable fun SceneScope.OverlayShade( - viewModel: OverlayShadeViewModel, + viewModelFactory: OverlayShadeViewModel.Factory, lockscreenContent: Lazy<Optional<LockscreenContent>>, modifier: Modifier = Modifier, content: @Composable () -> Unit, ) { + val viewModel = rememberViewModel { viewModelFactory.create() } val backgroundScene by viewModel.backgroundScene.collectAsStateWithLifecycle() Box(modifier) { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt index 1cd48bf2e628..8c53740ebfd0 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt @@ -73,6 +73,7 @@ import com.android.systemui.common.ui.compose.windowinsets.CutoutLocation import com.android.systemui.common.ui.compose.windowinsets.LocalDisplayCutout import com.android.systemui.common.ui.compose.windowinsets.LocalScreenCornerRadius import com.android.systemui.compose.modifiers.sysuiResTag +import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.privacy.OngoingPrivacyChip import com.android.systemui.res.R import com.android.systemui.scene.shared.model.Scenes @@ -122,12 +123,13 @@ object ShadeHeader { @Composable fun SceneScope.CollapsedShadeHeader( - viewModel: ShadeHeaderViewModel, + viewModelFactory: ShadeHeaderViewModel.Factory, createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager, createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController, statusBarIconController: StatusBarIconController, modifier: Modifier = Modifier, ) { + val viewModel = rememberViewModel { viewModelFactory.create() } val isDisabled by viewModel.isDisabled.collectAsStateWithLifecycle() if (isDisabled) { return @@ -279,12 +281,13 @@ fun SceneScope.CollapsedShadeHeader( @Composable fun SceneScope.ExpandedShadeHeader( - viewModel: ShadeHeaderViewModel, + viewModelFactory: ShadeHeaderViewModel.Factory, createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager, createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController, statusBarIconController: StatusBarIconController, modifier: Modifier = Modifier, ) { + val viewModel = rememberViewModel { viewModelFactory.create() } val isDisabled by viewModel.isDisabled.collectAsStateWithLifecycle() if (isDisabled) { return diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt index 77b48d3d307e..0e3fcf4598af 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt @@ -80,6 +80,7 @@ import com.android.systemui.common.ui.compose.windowinsets.LocalDisplayCutout import com.android.systemui.common.ui.compose.windowinsets.LocalScreenCornerRadius import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.media.controls.ui.composable.MediaCarousel import com.android.systemui.media.controls.ui.composable.MediaContentPicker import com.android.systemui.media.controls.ui.composable.shouldElevateMedia @@ -102,7 +103,8 @@ import com.android.systemui.scene.session.ui.composable.SaveableSession import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.ui.composable.ComposableScene import com.android.systemui.shade.shared.model.ShadeMode -import com.android.systemui.shade.ui.viewmodel.ShadeSceneViewModel +import com.android.systemui.shade.ui.viewmodel.ShadeSceneActionsViewModel +import com.android.systemui.shade.ui.viewmodel.ShadeSceneContentViewModel import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel import com.android.systemui.statusbar.phone.StatusBarLocation @@ -145,7 +147,8 @@ class ShadeScene constructor( private val shadeSession: SaveableSession, private val notificationStackScrollView: Lazy<NotificationScrollView>, - private val viewModel: ShadeSceneViewModel, + private val actionsViewModelFactory: ShadeSceneActionsViewModel.Factory, + private val contentViewModelFactory: ShadeSceneContentViewModel.Factory, private val notificationsPlaceholderViewModel: NotificationsPlaceholderViewModel, private val tintedIconManagerFactory: TintedIconManager.Factory, private val batteryMeterViewControllerFactory: BatteryMeterViewController.Factory, @@ -157,12 +160,16 @@ constructor( override val key = Scenes.Shade + private val actionsViewModel: ShadeSceneActionsViewModel by lazy { + actionsViewModelFactory.create() + } + override suspend fun activate() { - viewModel.activate() + actionsViewModel.activate() } override val destinationScenes: Flow<Map<UserAction, UserActionResult>> = - viewModel.destinationScenes + actionsViewModel.actions @Composable override fun SceneScope.Content( @@ -170,7 +177,7 @@ constructor( ) = ShadeScene( notificationStackScrollView.get(), - viewModel = viewModel, + viewModel = rememberViewModel { contentViewModelFactory.create() }, notificationsPlaceholderViewModel = notificationsPlaceholderViewModel, createTintedIconManager = tintedIconManagerFactory::create, createBatteryMeterViewController = batteryMeterViewControllerFactory::create, @@ -196,7 +203,7 @@ constructor( @Composable private fun SceneScope.ShadeScene( notificationStackScrollView: NotificationScrollView, - viewModel: ShadeSceneViewModel, + viewModel: ShadeSceneContentViewModel, notificationsPlaceholderViewModel: NotificationsPlaceholderViewModel, createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager, createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController, @@ -242,7 +249,7 @@ private fun SceneScope.ShadeScene( @Composable private fun SceneScope.SingleShade( notificationStackScrollView: NotificationScrollView, - viewModel: ShadeSceneViewModel, + viewModel: ShadeSceneContentViewModel, notificationsPlaceholderViewModel: NotificationsPlaceholderViewModel, createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager, createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController, @@ -261,7 +268,7 @@ private fun SceneScope.SingleShade( key = QuickSettings.SharedValues.TilesSquishiness, canOverflow = false ) - val isClickable by viewModel.isClickable.collectAsStateWithLifecycle() + val isEmptySpaceClickable by viewModel.isEmptySpaceClickable.collectAsStateWithLifecycle() val isMediaVisible by viewModel.isMediaVisible.collectAsStateWithLifecycle() val shouldPunchHoleBehindScrim = @@ -299,9 +306,9 @@ private fun SceneScope.SingleShade( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth() - .thenIf(isClickable) { + .thenIf(isEmptySpaceClickable) { Modifier.clickable( - onClick = { viewModel.onContentClicked() } + onClick = { viewModel.onEmptySpaceClicked() } ) } .thenIf(cutoutLocation != CutoutLocation.CENTER) { @@ -309,7 +316,7 @@ private fun SceneScope.SingleShade( }, ) { CollapsedShadeHeader( - viewModel = viewModel.shadeHeaderViewModel, + viewModelFactory = viewModel.shadeHeaderViewModelFactory, createTintedIconManager = createTintedIconManager, createBatteryMeterViewController = createBatteryMeterViewController, statusBarIconController = statusBarIconController, @@ -361,6 +368,8 @@ private fun SceneScope.SingleShade( maxScrimTop = { maxNotifScrimTop.value }, shadeMode = ShadeMode.Single, shouldPunchHoleBehindScrim = shouldPunchHoleBehindScrim, + onEmptySpaceClick = + viewModel::onEmptySpaceClicked.takeIf { isEmptySpaceClickable }, ) }, ) @@ -407,7 +416,7 @@ private fun SceneScope.SingleShade( @Composable private fun SceneScope.SplitShade( notificationStackScrollView: NotificationScrollView, - viewModel: ShadeSceneViewModel, + viewModel: ShadeSceneContentViewModel, notificationsPlaceholderViewModel: NotificationsPlaceholderViewModel, createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager, createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController, @@ -468,8 +477,10 @@ private fun SceneScope.SplitShade( } } - val brightnessMirrorShowing by - viewModel.brightnessMirrorViewModel.isShowing.collectAsStateWithLifecycle() + val brightnessMirrorViewModel = rememberViewModel { + viewModel.brightnessMirrorViewModelFactory.create() + } + val brightnessMirrorShowing by brightnessMirrorViewModel.isShowing.collectAsStateWithLifecycle() val contentAlpha by animateFloatAsState( targetValue = if (brightnessMirrorShowing) 0f else 1f, @@ -481,6 +492,7 @@ private fun SceneScope.SplitShade( onDispose { notificationsPlaceholderViewModel.setAlphaForBrightnessMirror(1f) } } + val isEmptySpaceClickable by viewModel.isEmptySpaceClickable.collectAsStateWithLifecycle() val isMediaVisible by viewModel.isMediaVisible.collectAsStateWithLifecycle() val brightnessMirrorShowingModifier = Modifier.graphicsLayer { alpha = contentAlpha } @@ -503,7 +515,7 @@ private fun SceneScope.SplitShade( modifier = Modifier.fillMaxSize(), ) { CollapsedShadeHeader( - viewModel = viewModel.shadeHeaderViewModel, + viewModelFactory = viewModel.shadeHeaderViewModelFactory, createTintedIconManager = createTintedIconManager, createBatteryMeterViewController = createBatteryMeterViewController, statusBarIconController = statusBarIconController, @@ -522,7 +534,7 @@ private fun SceneScope.SplitShade( .graphicsLayer { translationX = unfoldTranslationXForStartSide }, ) { BrightnessMirror( - viewModel = viewModel.brightnessMirrorViewModel, + viewModel = brightnessMirrorViewModel, qsSceneAdapter = viewModel.qsSceneAdapter, // Need to use the offset measured from the container as the header // has to be accounted for @@ -591,6 +603,8 @@ private fun SceneScope.SplitShade( shouldPunchHoleBehindScrim = false, shouldReserveSpaceForNavBar = false, shadeMode = ShadeMode.Split, + onEmptySpaceClick = + viewModel::onEmptySpaceClicked.takeIf { isEmptySpaceClickable }, modifier = Modifier.weight(1f) .fillMaxHeight() 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 0105af3377fd..fe16ef75118b 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 @@ -127,16 +127,7 @@ private class PredictiveBackTransition( return coroutineScope .launch(start = CoroutineStart.ATOMIC) { try { - if (currentScene == toScene) { - animatable.animateTo(targetProgress, transformationSpec.progressSpec) - } else { - // If the back gesture is cancelled, the progress is animated back to 0f by - // the system. But we need this animate call anyways because - // PredictiveBackHandler doesn't guarantee that it ends at 0f. Since the - // remaining change in progress is usually very small, the progressSpec is - // omitted and the default spring spec used instead. - animatable.animateTo(targetProgress) - } + animatable.animateTo(targetProgress) } finally { state.finishTransition(this@PredictiveBackTransition, scene) } 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 index c414fbe1c2db..0eaecb09e97e 100644 --- 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 @@ -18,8 +18,6 @@ package com.android.compose.animation.scene import androidx.activity.BackEventCompat import androidx.activity.ComponentActivity -import androidx.compose.animation.core.LinearEasing -import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.rememberCoroutineScope @@ -61,23 +59,7 @@ class PredictiveBackHandlerTest { @Test fun testPredictiveBack() { - val transitionFrames = 2 - val layoutState = - rule.runOnUiThread { - MutableSceneTransitionLayoutState( - SceneA, - transitions = - transitions { - from(SceneA, to = SceneB) { - spec = - tween( - durationMillis = transitionFrames * 16, - easing = LinearEasing - ) - } - } - ) - } + val layoutState = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) } rule.setContent { SceneTransitionLayout(layoutState) { scene(SceneA, mapOf(Back to SceneB)) { Box(Modifier.fillMaxSize()) } @@ -106,27 +88,12 @@ class PredictiveBackHandlerTest { assertThat(layoutState.transitionState).hasCurrentScene(SceneA) assertThat(layoutState.transitionState).isIdle() - rule.mainClock.autoAdvance = false - // Start again and commit it. rule.runOnUiThread { dispatcher.dispatchOnBackStarted(backEvent()) dispatcher.dispatchOnBackProgressed(backEvent(progress = 0.4f)) dispatcher.onBackPressed() } - rule.mainClock.advanceTimeByFrame() - rule.waitForIdle() - val transition2 = assertThat(layoutState.transitionState).isTransition() - // verify that transition picks up progress from preview - assertThat(transition2).hasProgress(0.4f, tolerance = 0.0001f) - - rule.mainClock.advanceTimeByFrame() - rule.waitForIdle() - // verify that transition is half way between preview-end-state (0.4f) and target-state (1f) - // after one frame - assertThat(transition2).hasProgress(0.7f, tolerance = 0.0001f) - - rule.mainClock.autoAdvance = true rule.waitForIdle() assertThat(layoutState.transitionState).hasCurrentScene(SceneB) assertThat(layoutState.transitionState).isIdle() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerFullscreenSwipeTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerFullscreenSwipeTouchHandlerTest.java index 4850085c4b4e..d244482c05ee 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerFullscreenSwipeTouchHandlerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerFullscreenSwipeTouchHandlerTest.java @@ -62,7 +62,7 @@ import java.util.Optional; @SmallTest @RunWith(AndroidJUnit4.class) -@EnableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE) +@EnableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX) @DisableFlags(Flags.FLAG_COMMUNAL_BOUNCER_DO_NOT_MODIFY_PLUGIN_OPEN) public class BouncerFullscreenSwipeTouchHandlerTest extends SysuiTestCase { private KosmosJavaAdapter mKosmos; diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java index 0e98b840942b..b85e32b381df 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java @@ -74,7 +74,7 @@ import java.util.Optional; @SmallTest @RunWith(AndroidJUnit4.class) -@DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE) +@DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX) public class BouncerSwipeTouchHandlerTest extends SysuiTestCase { private KosmosJavaAdapter mKosmos; @Mock diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.kt index 204d4b09f3ae..38ea44976175 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.kt @@ -79,7 +79,7 @@ class ShadeTouchHandlerTest : SysuiTestCase() { // Verifies that a swipe down in the gesture region is captured by the shade touch handler. @Test - @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE) + @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX) fun testSwipeDown_captured() { val captured = swipe(Direction.DOWN) Truth.assertThat(captured).isTrue() @@ -87,7 +87,7 @@ class ShadeTouchHandlerTest : SysuiTestCase() { // Verifies that a swipe in the upward direction is not captured. @Test - @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE) + @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX) fun testSwipeUp_notCaptured() { val captured = swipe(Direction.UP) @@ -97,7 +97,7 @@ class ShadeTouchHandlerTest : SysuiTestCase() { // Verifies that a swipe down forwards captured touches to central surfaces for handling. @Test - @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE) + @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX) @EnableFlags(Flags.FLAG_COMMUNAL_HUB) fun testSwipeDown_communalEnabled_sentToCentralSurfaces() { kosmos.fakeFeatureFlagsClassic.set(COMMUNAL_SERVICE_ENABLED, true) @@ -110,7 +110,7 @@ class ShadeTouchHandlerTest : SysuiTestCase() { // Verifies that a swipe down forwards captured touches to the shade view for handling. @Test - @DisableFlags(Flags.FLAG_COMMUNAL_HUB, Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE) + @DisableFlags(Flags.FLAG_COMMUNAL_HUB, Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX) fun testSwipeDown_communalDisabled_sentToShadeView() { swipe(Direction.DOWN) @@ -121,7 +121,7 @@ class ShadeTouchHandlerTest : SysuiTestCase() { // Verifies that a swipe down while dreaming forwards captured touches to the shade view for // handling. @Test - @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE) + @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX) fun testSwipeDown_dreaming_sentToShadeView() { whenever(mDreamManager.isDreaming).thenReturn(true) swipe(Direction.DOWN) @@ -132,7 +132,7 @@ class ShadeTouchHandlerTest : SysuiTestCase() { // Verifies that a swipe up is not forwarded to central surfaces. @Test - @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE) + @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX) @EnableFlags(Flags.FLAG_COMMUNAL_HUB) fun testSwipeUp_communalEnabled_touchesNotSent() { kosmos.fakeFeatureFlagsClassic.set(COMMUNAL_SERVICE_ENABLED, true) @@ -146,7 +146,7 @@ class ShadeTouchHandlerTest : SysuiTestCase() { // Verifies that a swipe up is not forwarded to the shade view. @Test - @DisableFlags(Flags.FLAG_COMMUNAL_HUB, Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE) + @DisableFlags(Flags.FLAG_COMMUNAL_HUB, Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX) fun testSwipeUp_communalDisabled_touchesNotSent() { swipe(Direction.UP) @@ -156,7 +156,7 @@ class ShadeTouchHandlerTest : SysuiTestCase() { } @Test - @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE) + @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX) fun testCancelMotionEvent_popsTouchSession() { swipe(Direction.DOWN) val event = MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0) @@ -165,7 +165,7 @@ class ShadeTouchHandlerTest : SysuiTestCase() { } @Test - @EnableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE) + @EnableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX) fun testFullVerticalSwipe_initiatedWhenAvailable() { // Indicate touches are available mTouchHandler.onGlanceableTouchAvailable(true) @@ -176,7 +176,7 @@ class ShadeTouchHandlerTest : SysuiTestCase() { } @Test - @EnableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE) + @EnableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX) fun testFullVerticalSwipe_notInitiatedWhenNotAvailable() { // Indicate touches aren't available mTouchHandler.onGlanceableTouchAvailable(false) @@ -187,7 +187,7 @@ class ShadeTouchHandlerTest : SysuiTestCase() { } @Test - @EnableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE) + @EnableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX) fun testFullVerticalSwipe_resetsTouchStateOnUp() { // Indicate touches are available mTouchHandler.onGlanceableTouchAvailable(true) @@ -203,7 +203,7 @@ class ShadeTouchHandlerTest : SysuiTestCase() { } @Test - @EnableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE) + @EnableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX) fun testFullVerticalSwipe_resetsTouchStateOnCancel() { // Indicate touches are available mTouchHandler.onGlanceableTouchAvailable(true) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorTest.kt index ad7385344fac..d6712f09cd4e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorTest.kt @@ -225,7 +225,7 @@ class CommunalSceneTransitionInteractorTest : SysuiTestCase() { kosmos.fakeKeyguardRepository.setKeyguardOccluded(true) kosmos.fakeKeyguardRepository.setDreaming(true) kosmos.fakeKeyguardRepository.setDreamingWithOverlay(true) - advanceTimeBy(100L) + advanceTimeBy(600L) sceneTransitions.value = hubToBlank diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt index 12552489496d..cc945d63e15f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt @@ -533,6 +533,8 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { keyguardRepository.setDozeTransitionModel( DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH) ) + advanceTimeBy(600L) + keyguardRepository.setDreaming(true) keyguardRepository.setDreamingWithOverlay(true) advanceTimeBy(60L) @@ -641,6 +643,7 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { keyguardRepository.setDozeTransitionModel( DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH) ) + advanceTimeBy(600L) keyguardRepository.setDreaming(true) keyguardRepository.setDreamingWithOverlay(true) advanceTimeBy(60L) @@ -699,6 +702,7 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { keyguardRepository.setDozeTransitionModel( DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH) ) + advanceTimeBy(600L) keyguardRepository.setDreaming(true) keyguardRepository.setDreamingWithOverlay(true) advanceTimeBy(60L) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java index 6412276ba34b..3895595aaea6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java @@ -62,6 +62,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatchers; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -324,4 +325,13 @@ public class DreamOverlayContainerViewControllerTest extends SysuiTestCase { // enabled. mController.onViewAttached(); } + + @Test + public void destroy_cleansUpState() { + mController.destroy(); + verify(mStateController).removeCallback(any()); + verify(mAmbientStatusBarViewController).destroy(); + verify(mComplicationHostViewController).destroy(); + verify(mLowLightTransitionCoordinator).setLowLightEnterListener(ArgumentMatchers.isNull()); + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt index 5c09777ddde5..7a86e57779b9 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt @@ -596,6 +596,9 @@ class DreamOverlayServiceTest : SysuiTestCase() { // are created. verify(mDreamOverlayComponent).getDreamOverlayContainerViewController() verify(mAmbientTouchComponent).getTouchMonitor() + + // Verify DreamOverlayContainerViewController is destroyed. + verify(mDreamOverlayContainerViewController).destroy() } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryTest.kt index f82a7b8df089..5dd6c228e014 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryTest.kt @@ -85,7 +85,9 @@ class ContextualEducationRepositoryTest : SysuiTestCase() { GestureEduModel( signalCount = 2, educationShownCount = 1, - lastShortcutTriggeredTime = kosmos.fakeEduClock.instant() + lastShortcutTriggeredTime = kosmos.fakeEduClock.instant(), + lastEducationTime = kosmos.fakeEduClock.instant(), + usageSessionStartTime = kosmos.fakeEduClock.instant(), ) underTest.updateGestureEduModel(BACK) { newModel } val model by collectLastValue(underTest.readGestureEduModelFlow(BACK)) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt index 1b4632a546d4..6867089473da 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt @@ -22,9 +22,15 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.contextualeducation.GestureType import com.android.systemui.contextualeducation.GestureType.BACK import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.education.data.model.GestureEduModel +import com.android.systemui.education.data.repository.contextualEducationRepository +import com.android.systemui.education.data.repository.fakeEduClock +import com.android.systemui.education.shared.model.EducationUiType import com.android.systemui.kosmos.testScope import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test @@ -37,6 +43,7 @@ class KeyboardTouchpadEduInteractorTest : SysuiTestCase() { private val testScope = kosmos.testScope private val contextualEduInteractor = kosmos.contextualEducationInteractor private val underTest: KeyboardTouchpadEduInteractor = kosmos.keyboardTouchpadEduInteractor + private val eduClock = kosmos.fakeEduClock @Before fun setup() { @@ -46,12 +53,32 @@ class KeyboardTouchpadEduInteractorTest : SysuiTestCase() { @Test fun newEducationInfoOnMaxSignalCountReached() = testScope.runTest { - tryTriggeringEducation(BACK) + triggerMaxEducationSignals(BACK) val model by collectLastValue(underTest.educationTriggered) assertThat(model?.gestureType).isEqualTo(BACK) } @Test + fun newEducationToastOn1stEducation() = + testScope.runTest { + val model by collectLastValue(underTest.educationTriggered) + triggerMaxEducationSignals(BACK) + assertThat(model?.educationUiType).isEqualTo(EducationUiType.Toast) + } + + @Test + @kotlinx.coroutines.ExperimentalCoroutinesApi + fun newEducationNotificationOn2ndEducation() = + testScope.runTest { + val model by collectLastValue(underTest.educationTriggered) + triggerMaxEducationSignals(BACK) + // runCurrent() to trigger 1st education + runCurrent() + triggerMaxEducationSignals(BACK) + assertThat(model?.educationUiType).isEqualTo(EducationUiType.Notification) + } + + @Test fun noEducationInfoBeforeMaxSignalCountReached() = testScope.runTest { contextualEduInteractor.incrementSignalCount(BACK) @@ -64,11 +91,30 @@ class KeyboardTouchpadEduInteractorTest : SysuiTestCase() { testScope.runTest { val model by collectLastValue(underTest.educationTriggered) contextualEduInteractor.updateShortcutTriggerTime(BACK) - tryTriggeringEducation(BACK) + triggerMaxEducationSignals(BACK) assertThat(model).isNull() } - private suspend fun tryTriggeringEducation(gestureType: GestureType) { + @Test + fun startNewUsageSessionWhen2ndSignalReceivedAfterSessionDeadline() = + testScope.runTest { + val model by + collectLastValue(kosmos.contextualEducationRepository.readGestureEduModelFlow(BACK)) + contextualEduInteractor.incrementSignalCount(BACK) + eduClock.offset(KeyboardTouchpadEduInteractor.usageSessionDuration.plus(1.seconds)) + val secondSignalReceivedTime = eduClock.instant() + contextualEduInteractor.incrementSignalCount(BACK) + + assertThat(model) + .isEqualTo( + GestureEduModel( + signalCount = 1, + usageSessionStartTime = secondSignalReceivedTime + ) + ) + } + + private suspend fun triggerMaxEducationSignals(gestureType: GestureType) { // Increment max number of signal to try triggering education for (i in 1..KeyboardTouchpadEduInteractor.MAX_SIGNAL_COUNT) { contextualEduInteractor.incrementSignalCount(gestureType) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt index 273e3cbe76ea..fd4ed3896c43 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt @@ -112,6 +112,26 @@ class QSLongPressEffectTest : SysuiTestCase() { } @Test + fun onActionDown_whileClicked_startsWait() = + testWhileInState(QSLongPressEffect.State.CLICKED) { + // GIVEN an action down event occurs + longPressEffect.handleActionDown() + + // THEN the effect moves to the TIMEOUT_WAIT state + assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.TIMEOUT_WAIT) + } + + @Test + fun onActionDown_whileLongClicked_startsWait() = + testWhileInState(QSLongPressEffect.State.LONG_CLICKED) { + // GIVEN an action down event occurs + longPressEffect.handleActionDown() + + // THEN the effect moves to the TIMEOUT_WAIT state + assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.TIMEOUT_WAIT) + } + + @Test fun onActionCancel_whileWaiting_goesIdle() = testWhileInState(QSLongPressEffect.State.TIMEOUT_WAIT) { // GIVEN an action cancel occurs diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt index 032794c43f08..638c957c9fa7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt @@ -14,22 +14,6 @@ * limitations under the License. */ -/* - * 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.keyguard.domain.interactor import android.platform.test.annotations.EnableFlags @@ -46,17 +30,18 @@ import com.android.systemui.keyguard.shared.model.BiometricUnlockMode import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.util.KeyguardTransitionRepositorySpySubject.Companion.assertThat import com.android.systemui.kosmos.testScope -import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest +import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor import com.android.systemui.testKosmos -import kotlin.test.Test +import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Ignore +import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito.reset import org.mockito.Mockito.spy @@ -79,21 +64,6 @@ class FromDreamingTransitionInteractorTest : SysuiTestCase() { @Before fun setup() { underTest.start() - - kosmos.fakeKeyguardRepository.setDreaming(true) - kosmos.keyguardOcclusionInteractor.setWmNotifiedShowWhenLockedActivityOnTop(true) - - // Transition to DOZING and set the power interactor asleep. - powerInteractor.setAsleepForTest() - runBlocking { - transitionRepository.sendTransitionSteps( - from = KeyguardState.LOCKSCREEN, - to = KeyguardState.DREAMING, - testScope - ) - kosmos.fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockMode.NONE) - reset(transitionRepository) - } } @Test @@ -146,20 +116,27 @@ class FromDreamingTransitionInteractorTest : SysuiTestCase() { @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) fun testTransitionsToLockscreen_whenOccludingActivityEnds() = testScope.runTest { + runCurrent() + kosmos.fakeKeyguardRepository.setDreaming(true) - kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(onTop = true) + kosmos.keyguardOcclusionInteractor.setWmNotifiedShowWhenLockedActivityOnTop(true) + // Transition to DREAMING and set the power interactor awake + powerInteractor.setAwakeForTest() + transitionRepository.sendTransitionSteps( from = KeyguardState.LOCKSCREEN, to = KeyguardState.DREAMING, - testScope, + testScope ) - runCurrent() + kosmos.fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockMode.NONE) + // Get past initial setup + advanceTimeBy(600L) reset(transitionRepository) kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(onTop = false) kosmos.fakeKeyguardRepository.setDreaming(false) - runCurrent() + advanceTimeBy(60L) assertThat(transitionRepository) .startedTransition( @@ -171,6 +148,13 @@ class FromDreamingTransitionInteractorTest : SysuiTestCase() { @Test fun testTransitionToAlternateBouncer() = testScope.runTest { + transitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.DREAMING, + testScope, + ) + reset(transitionRepository) + kosmos.fakeKeyguardBouncerRepository.setAlternateVisible(true) runCurrent() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt index fc827a1478c7..ebefb4d51943 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt @@ -33,11 +33,15 @@ import com.android.systemui.keyguard.data.repository.fakeCommandQueue import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.CameraLaunchType +import com.android.systemui.keyguard.shared.model.DozeStateModel +import com.android.systemui.keyguard.shared.model.DozeTransitionModel import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.StatusBarState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.kosmos.testScope +import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest +import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.res.R import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Scenes @@ -47,6 +51,7 @@ import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -67,6 +72,7 @@ class KeyguardInteractorTest : SysuiTestCase() { private val configRepository by lazy { kosmos.fakeConfigurationRepository } private val bouncerRepository by lazy { kosmos.keyguardBouncerRepository } private val shadeRepository by lazy { kosmos.shadeRepository } + private val powerInteractor by lazy { kosmos.powerInteractor } private val keyguardTransitionRepository by lazy { kosmos.fakeKeyguardTransitionRepository } private val transitionState: MutableStateFlow<ObservableTransitionState> = @@ -350,6 +356,59 @@ class KeyguardInteractorTest : SysuiTestCase() { } @Test + fun isAbleToDream_falseWhenDozing() = + testScope.runTest { + val isAbleToDream by collectLastValue(underTest.isAbleToDream) + + repository.setDozeTransitionModel( + DozeTransitionModel(from = DozeStateModel.INITIALIZED, to = DozeStateModel.DOZE_AOD) + ) + + assertThat(isAbleToDream).isEqualTo(false) + } + + @Test + fun isAbleToDream_falseWhenNotDozingAndNotDreaming() = + testScope.runTest { + val isAbleToDream by collectLastValue(underTest.isAbleToDream) + + repository.setDozeTransitionModel( + DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH) + ) + powerInteractor.setAwakeForTest() + advanceTimeBy(1000L) + + assertThat(isAbleToDream).isEqualTo(false) + } + + @Test + fun isAbleToDream_trueWhenNotDozingAndIsDreaming_afterDelay() = + testScope.runTest { + val isAbleToDream by collectLastValue(underTest.isAbleToDream) + runCurrent() + + repository.setDreaming(true) + repository.setDozeTransitionModel( + DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH) + ) + powerInteractor.setAwakeForTest() + runCurrent() + + // After some delay, still false + advanceTimeBy(300L) + assertThat(isAbleToDream).isEqualTo(false) + + // After more delay, is true + advanceTimeBy(300L) + assertThat(isAbleToDream).isEqualTo(true) + + // Also changes back after the minimal debounce + repository.setDreaming(false) + advanceTimeBy(55L) + assertThat(isAbleToDream).isEqualTo(false) + } + + @Test @EnableSceneContainer fun animationDozingTransitions() = testScope.runTest { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt index 9762fd8e2158..8c1e8de315b1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt @@ -258,7 +258,7 @@ class KeyguardTransitionScenariosTest(flags: FlagsParameterization?) : SysuiTest keyguardRepository.setDozeTransitionModel( DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH) ) - runCurrent() + advanceTimeBy(600L) // GIVEN a prior transition has run to LOCKSCREEN runTransitionAndSetWakefulness(KeyguardState.GONE, KeyguardState.LOCKSCREEN) @@ -287,7 +287,7 @@ class KeyguardTransitionScenariosTest(flags: FlagsParameterization?) : SysuiTest keyguardRepository.setDozeTransitionModel( DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH) ) - runCurrent() + advanceTimeBy(600L) // GIVEN a prior transition has run to LOCKSCREEN runTransitionAndSetWakefulness(KeyguardState.GONE, KeyguardState.LOCKSCREEN) @@ -625,6 +625,9 @@ class KeyguardTransitionScenariosTest(flags: FlagsParameterization?) : SysuiTest @DisableSceneContainer fun dreamingToGoneWithKeyguardNotShowing() = testScope.runTest { + // Setup - Move past initial delay with [KeyguardInteractor#isAbleToDream] + advanceTimeBy(600L) + // GIVEN a prior transition has run to DREAMING keyguardRepository.setDreamingWithOverlay(true) runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.DREAMING) @@ -754,15 +757,35 @@ class KeyguardTransitionScenariosTest(flags: FlagsParameterization?) : SysuiTest } @Test + @BrokenWithSceneContainer(339465026) + fun goneToOccluded() = + testScope.runTest { + // GIVEN a prior transition has run to GONE + runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.GONE) + + // WHEN an occluding app is running and showDismissibleKeyguard is called + keyguardRepository.setKeyguardOccluded(true) + keyguardRepository.showDismissibleKeyguard() + runCurrent() + + assertThat(transitionRepository) + .startedTransition( + from = KeyguardState.GONE, + to = KeyguardState.OCCLUDED, + ownerName = + "FromGoneTransitionInteractor" + "(Dismissible keyguard with occlusion)", + animatorAssertion = { it.isNotNull() } + ) + + coroutineContext.cancelChildren() + } + + @Test @DisableSceneContainer fun goneToDreaming() = testScope.runTest { - // GIVEN a device that is not dreaming or dozing - keyguardRepository.setDreamingWithOverlay(false) - keyguardRepository.setDozeTransitionModel( - DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH) - ) - runCurrent() + // Setup - Move past initial delay with [KeyguardInteractor#isAbleToDream] + advanceTimeBy(600L) // GIVEN a prior transition has run to GONE runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.GONE) @@ -1130,6 +1153,9 @@ class KeyguardTransitionScenariosTest(flags: FlagsParameterization?) : SysuiTest @DisableSceneContainer fun primaryBouncerToGlanceableHubWhileDreaming() = testScope.runTest { + // Setup - Move past initial delay with [KeyguardInteractor#isAbleToDream] + advanceTimeBy(600L) + // GIVEN the device is idle on the glanceable hub communalSceneInteractor.changeScene(CommunalScenes.Communal) runCurrent() @@ -1144,6 +1170,7 @@ class KeyguardTransitionScenariosTest(flags: FlagsParameterization?) : SysuiTest // GIVEN that we are dreaming and occluded keyguardRepository.setDreaming(true) keyguardRepository.setKeyguardOccluded(true) + advanceTimeBy(60L) // WHEN the primaryBouncer stops showing bouncerRepository.setPrimaryShow(false) @@ -2181,12 +2208,14 @@ class KeyguardTransitionScenariosTest(flags: FlagsParameterization?) : SysuiTest @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR) fun glanceableHubToDreaming() = testScope.runTest { + runCurrent() + // GIVEN that we are dreaming and not dozing keyguardRepository.setDreaming(true) keyguardRepository.setDozeTransitionModel( DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH) ) - runCurrent() + advanceTimeBy(600L) // GIVEN a prior transition has run to GLANCEABLE_HUB runTransitionAndSetWakefulness(KeyguardState.DREAMING, KeyguardState.GLANCEABLE_HUB) @@ -2233,7 +2262,7 @@ class KeyguardTransitionScenariosTest(flags: FlagsParameterization?) : SysuiTest keyguardRepository.setDozeTransitionModel( DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH) ) - advanceTimeBy(100L) + advanceTimeBy(600L) // GIVEN a prior transition has run to GLANCEABLE_HUB communalSceneInteractor.changeScene(CommunalScenes.Communal) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModelTest.kt index 3f6e2291fd1f..df8afdbcf7a8 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModelTest.kt @@ -23,6 +23,7 @@ import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRe import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository import com.android.systemui.biometrics.shared.model.FingerprintSensorType import com.android.systemui.biometrics.shared.model.SensorStrength +import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository @@ -117,6 +118,24 @@ class AlternateBouncerToAodTransitionViewModelTest : SysuiTestCase() { } @Test + fun lockscreenAlphaStartsFromViewStateAccessorAlpha() = + testScope.runTest { + val viewState = ViewStateAccessor(alpha = { 0.5f }) + val alpha by collectLastValue(underTest.lockscreenAlpha(viewState)) + + keyguardTransitionRepository.sendTransitionStep(step(0f, TransitionState.STARTED)) + + keyguardTransitionRepository.sendTransitionStep(step(0f)) + assertThat(alpha).isEqualTo(0.5f) + + keyguardTransitionRepository.sendTransitionStep(step(0.5f)) + assertThat(alpha).isEqualTo(0.75f) + + keyguardTransitionRepository.sendTransitionStep(step(1f)) + assertThat(alpha).isEqualTo(1f) + } + + @Test fun deviceEntryBackgroundViewDisappear() = testScope.runTest { val values by collectValues(underTest.deviceEntryBackgroundViewAlpha) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt index 46b370fedf37..d1f908dfc795 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt @@ -16,27 +16,16 @@ package com.android.systemui.lifecycle -import android.view.View import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.test.junit4.createComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.util.Assert import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.awaitCancellation -import kotlinx.coroutines.launch -import kotlinx.coroutines.test.runCurrent -import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.kotlin.argumentCaptor -import org.mockito.kotlin.doReturn -import org.mockito.kotlin.mock -import org.mockito.kotlin.stub -import org.mockito.kotlin.verify @SmallTest @RunWith(AndroidJUnit4::class) @@ -121,45 +110,4 @@ class SysUiViewModelTest : SysuiTestCase() { assertThat(isActive).isFalse() } - - @Test - fun viewModel_viewBinder() = runTest { - Assert.setTestThread(Thread.currentThread()) - - val view: View = mock { on { isAttachedToWindow } doReturn false } - val viewModel = FakeViewModel() - backgroundScope.launch { - view.viewModel( - minWindowLifecycleState = WindowLifecycleState.ATTACHED, - factory = { viewModel }, - ) { - awaitCancellation() - } - } - runCurrent() - - assertThat(viewModel.isActivated).isFalse() - - view.stub { on { isAttachedToWindow } doReturn true } - argumentCaptor<View.OnAttachStateChangeListener>() - .apply { verify(view).addOnAttachStateChangeListener(capture()) } - .allValues - .forEach { it.onViewAttachedToWindow(view) } - runCurrent() - - assertThat(viewModel.isActivated).isTrue() - } -} - -private class FakeViewModel : SysUiViewModel() { - var isActivated = false - - override suspend fun onActivated() { - isActivated = true - try { - awaitCancellation() - } finally { - isActivated = false - } - } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneActionsViewModelTest.kt index 16c70901eacc..8f925d52e649 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneActionsViewModelTest.kt @@ -31,12 +31,13 @@ import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintA import com.android.systemui.keyguard.domain.interactor.keyguardEnabledInteractor import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus import com.android.systemui.kosmos.testScope +import com.android.systemui.lifecycle.activateIn import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.domain.resolver.homeSceneFamilyResolver import com.android.systemui.scene.shared.model.SceneFamilies import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.data.repository.fakeShadeRepository -import com.android.systemui.shade.ui.viewmodel.notificationsShadeSceneViewModel +import com.android.systemui.shade.ui.viewmodel.notificationsShadeSceneActionsViewModel import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -51,23 +52,24 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) @TestableLooper.RunWithLooper @EnableSceneContainer -class NotificationsShadeSceneViewModelTest : SysuiTestCase() { +class NotificationsShadeSceneActionsViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope private val sceneInteractor by lazy { kosmos.sceneInteractor } private val deviceUnlockedInteractor by lazy { kosmos.deviceUnlockedInteractor } - private val underTest by lazy { kosmos.notificationsShadeSceneViewModel } + private val underTest by lazy { kosmos.notificationsShadeSceneActionsViewModel } @Test fun upTransitionSceneKey_deviceLocked_lockscreen() = testScope.runTest { - val destinationScenes by collectLastValue(underTest.destinationScenes) + val actions by collectLastValue(underTest.actions) lockDevice() + underTest.activateIn(this) - assertThat(destinationScenes?.get(Swipe.Up)?.toScene).isEqualTo(SceneFamilies.Home) - assertThat(destinationScenes?.get(Swipe.Down)).isNull() + assertThat(actions?.get(Swipe.Up)?.toScene).isEqualTo(SceneFamilies.Home) + assertThat(actions?.get(Swipe.Down)).isNull() assertThat(kosmos.homeSceneFamilyResolver.resolvedScene.value) .isEqualTo(Scenes.Lockscreen) } @@ -75,23 +77,25 @@ class NotificationsShadeSceneViewModelTest : SysuiTestCase() { @Test fun upTransitionSceneKey_deviceLocked_keyguardDisabled_gone() = testScope.runTest { - val destinationScenes by collectLastValue(underTest.destinationScenes) + val actions by collectLastValue(underTest.actions) lockDevice() kosmos.keyguardEnabledInteractor.notifyKeyguardEnabled(false) + underTest.activateIn(this) - assertThat(destinationScenes?.get(Swipe.Up)?.toScene).isEqualTo(SceneFamilies.Home) + assertThat(actions?.get(Swipe.Up)?.toScene).isEqualTo(SceneFamilies.Home) assertThat(kosmos.homeSceneFamilyResolver.resolvedScene.value).isEqualTo(Scenes.Gone) } @Test fun upTransitionSceneKey_deviceUnlocked_gone() = testScope.runTest { - val destinationScenes by collectLastValue(underTest.destinationScenes) + val actions by collectLastValue(underTest.actions) lockDevice() unlockDevice() + underTest.activateIn(this) - assertThat(destinationScenes?.get(Swipe.Up)?.toScene).isEqualTo(SceneFamilies.Home) - assertThat(destinationScenes?.get(Swipe.Down)).isNull() + assertThat(actions?.get(Swipe.Up)?.toScene).isEqualTo(SceneFamilies.Home) + assertThat(actions?.get(Swipe.Down)).isNull() assertThat(sceneInteractor.currentScene.value).isEqualTo(Scenes.Gone) } @@ -99,11 +103,12 @@ class NotificationsShadeSceneViewModelTest : SysuiTestCase() { fun downTransitionSceneKey_deviceLocked_bottomAligned_lockscreen() = testScope.runTest { kosmos.fakeShadeRepository.setDualShadeAlignedToBottom(true) - val destinationScenes by collectLastValue(underTest.destinationScenes) + val actions by collectLastValue(underTest.actions) lockDevice() + underTest.activateIn(this) - assertThat(destinationScenes?.get(Swipe.Down)?.toScene).isEqualTo(SceneFamilies.Home) - assertThat(destinationScenes?.get(Swipe.Up)).isNull() + assertThat(actions?.get(Swipe.Down)?.toScene).isEqualTo(SceneFamilies.Home) + assertThat(actions?.get(Swipe.Up)).isNull() assertThat(kosmos.homeSceneFamilyResolver.resolvedScene.value) .isEqualTo(Scenes.Lockscreen) } @@ -112,26 +117,28 @@ class NotificationsShadeSceneViewModelTest : SysuiTestCase() { fun downTransitionSceneKey_deviceUnlocked_bottomAligned_gone() = testScope.runTest { kosmos.fakeShadeRepository.setDualShadeAlignedToBottom(true) - val destinationScenes by collectLastValue(underTest.destinationScenes) + val actions by collectLastValue(underTest.actions) lockDevice() unlockDevice() + underTest.activateIn(this) - assertThat(destinationScenes?.get(Swipe.Down)?.toScene).isEqualTo(SceneFamilies.Home) - assertThat(destinationScenes?.get(Swipe.Up)).isNull() + assertThat(actions?.get(Swipe.Down)?.toScene).isEqualTo(SceneFamilies.Home) + assertThat(actions?.get(Swipe.Up)).isNull() assertThat(sceneInteractor.currentScene.value).isEqualTo(Scenes.Gone) } @Test fun upTransitionSceneKey_authMethodSwipe_lockscreenNotDismissed_goesToLockscreen() = testScope.runTest { - val destinationScenes by collectLastValue(underTest.destinationScenes) + val actions by collectLastValue(underTest.actions) kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true) kosmos.fakeAuthenticationRepository.setAuthenticationMethod( AuthenticationMethodModel.None ) sceneInteractor.changeScene(Scenes.Lockscreen, "reason") + underTest.activateIn(this) - assertThat(destinationScenes?.get(Swipe.Up)?.toScene).isEqualTo(SceneFamilies.Home) + assertThat(actions?.get(Swipe.Up)?.toScene).isEqualTo(SceneFamilies.Home) assertThat(kosmos.homeSceneFamilyResolver.resolvedScene.value) .isEqualTo(Scenes.Lockscreen) } @@ -139,7 +146,7 @@ class NotificationsShadeSceneViewModelTest : SysuiTestCase() { @Test fun upTransitionSceneKey_authMethodSwipe_lockscreenDismissed_goesToGone() = testScope.runTest { - val destinationScenes by collectLastValue(underTest.destinationScenes) + val actions by collectLastValue(underTest.actions) kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true) kosmos.fakeAuthenticationRepository.setAuthenticationMethod( AuthenticationMethodModel.None @@ -147,8 +154,9 @@ class NotificationsShadeSceneViewModelTest : SysuiTestCase() { sceneInteractor // force the lazy; this will kick off StateFlows runCurrent() sceneInteractor.changeScene(Scenes.Gone, "reason") + underTest.activateIn(this) - assertThat(destinationScenes?.get(Swipe.Up)?.toScene).isEqualTo(SceneFamilies.Home) + assertThat(actions?.get(Swipe.Up)?.toScene).isEqualTo(SceneFamilies.Home) assertThat(kosmos.homeSceneFamilyResolver.resolvedScene.value).isEqualTo(Scenes.Gone) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt index 3ca802eb7091..036380853a71 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt @@ -49,9 +49,8 @@ import com.android.systemui.scene.domain.resolver.homeSceneFamilyResolver import com.android.systemui.scene.domain.startable.sceneContainerStartable import com.android.systemui.scene.shared.model.SceneFamilies import com.android.systemui.scene.shared.model.Scenes -import com.android.systemui.settings.brightness.ui.viewmodel.brightnessMirrorViewModel -import com.android.systemui.shade.ui.viewmodel.shadeHeaderViewModel -import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModel +import com.android.systemui.settings.brightness.ui.viewmodel.brightnessMirrorViewModelFactory +import com.android.systemui.shade.ui.viewmodel.shadeHeaderViewModelFactory import com.android.systemui.testKosmos import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock @@ -93,10 +92,9 @@ class QuickSettingsSceneViewModelTest : SysuiTestCase() { sceneContainerStartable.start() underTest = QuickSettingsSceneViewModel( - brightnessMirrorViewModel = kosmos.brightnessMirrorViewModel, - shadeHeaderViewModel = kosmos.shadeHeaderViewModel, + brightnessMirrorViewModelFactory = kosmos.brightnessMirrorViewModelFactory, + shadeHeaderViewModelFactory = kosmos.shadeHeaderViewModelFactory, qsSceneAdapter = qsFlexiglassAdapter, - notifications = kosmos.notificationsPlaceholderViewModel, footerActionsViewModelFactory = footerActionsViewModelFactory, footerActionsController = footerActionsController, sceneBackInteractor = sceneBackInteractor, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneActionsViewModelTest.kt index a7a3a0f3008a..647fdf6d6931 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneActionsViewModelTest.kt @@ -32,6 +32,7 @@ import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintA import com.android.systemui.keyguard.domain.interactor.keyguardEnabledInteractor import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus import com.android.systemui.kosmos.testScope +import com.android.systemui.lifecycle.activateIn import com.android.systemui.qs.panels.ui.viewmodel.editModeViewModel import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.domain.resolver.homeSceneFamilyResolver @@ -44,6 +45,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -52,49 +54,54 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) @TestableLooper.RunWithLooper @EnableSceneContainer -class QuickSettingsShadeSceneViewModelTest : SysuiTestCase() { +class QuickSettingsShadeSceneActionsViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope private val sceneInteractor = kosmos.sceneInteractor private val deviceUnlockedInteractor = kosmos.deviceUnlockedInteractor - private val underTest by lazy { kosmos.quickSettingsShadeSceneViewModel } + private val underTest by lazy { kosmos.quickSettingsShadeSceneActionsViewModel } + + @Before + fun setUp() { + underTest.activateIn(testScope) + } @Test fun upTransitionSceneKey_deviceLocked_lockscreen() = testScope.runTest { - val destinationScenes by collectLastValue(underTest.destinationScenes) + val actions by collectLastValue(underTest.actions) val homeScene by collectLastValue(kosmos.homeSceneFamilyResolver.resolvedScene) lockDevice() - assertThat(destinationScenes?.get(Swipe.Up)?.toScene).isEqualTo(SceneFamilies.Home) - assertThat(destinationScenes?.get(Swipe.Down)).isNull() + assertThat(actions?.get(Swipe.Up)?.toScene).isEqualTo(SceneFamilies.Home) + assertThat(actions?.get(Swipe.Down)).isNull() assertThat(homeScene).isEqualTo(Scenes.Lockscreen) } @Test fun upTransitionSceneKey_deviceLocked_keyguardDisabled_gone() = testScope.runTest { - val destinationScenes by collectLastValue(underTest.destinationScenes) + val actions by collectLastValue(underTest.actions) val homeScene by collectLastValue(kosmos.homeSceneFamilyResolver.resolvedScene) lockDevice() kosmos.keyguardEnabledInteractor.notifyKeyguardEnabled(false) - assertThat(destinationScenes?.get(Swipe.Up)?.toScene).isEqualTo(SceneFamilies.Home) + assertThat(actions?.get(Swipe.Up)?.toScene).isEqualTo(SceneFamilies.Home) assertThat(homeScene).isEqualTo(Scenes.Gone) } @Test fun upTransitionSceneKey_deviceUnlocked_gone() = testScope.runTest { - val destinationScenes by collectLastValue(underTest.destinationScenes) + val actions by collectLastValue(underTest.actions) val homeScene by collectLastValue(kosmos.homeSceneFamilyResolver.resolvedScene) lockDevice() unlockDevice() - assertThat(destinationScenes?.get(Swipe.Up)?.toScene).isEqualTo(SceneFamilies.Home) - assertThat(destinationScenes?.get(Swipe.Down)).isNull() + assertThat(actions?.get(Swipe.Up)?.toScene).isEqualTo(SceneFamilies.Home) + assertThat(actions?.get(Swipe.Down)).isNull() assertThat(homeScene).isEqualTo(Scenes.Gone) } @@ -102,12 +109,12 @@ class QuickSettingsShadeSceneViewModelTest : SysuiTestCase() { fun downTransitionSceneKey_deviceLocked_bottomAligned_lockscreen() = testScope.runTest { kosmos.fakeShadeRepository.setDualShadeAlignedToBottom(true) - val destinationScenes by collectLastValue(underTest.destinationScenes) + val actions by collectLastValue(underTest.actions) val homeScene by collectLastValue(kosmos.homeSceneFamilyResolver.resolvedScene) lockDevice() - assertThat(destinationScenes?.get(Swipe.Down)?.toScene).isEqualTo(SceneFamilies.Home) - assertThat(destinationScenes?.get(Swipe.Up)).isNull() + assertThat(actions?.get(Swipe.Down)?.toScene).isEqualTo(SceneFamilies.Home) + assertThat(actions?.get(Swipe.Up)).isNull() assertThat(homeScene).isEqualTo(Scenes.Lockscreen) } @@ -115,20 +122,20 @@ class QuickSettingsShadeSceneViewModelTest : SysuiTestCase() { fun downTransitionSceneKey_deviceUnlocked_bottomAligned_gone() = testScope.runTest { kosmos.fakeShadeRepository.setDualShadeAlignedToBottom(true) - val destinationScenes by collectLastValue(underTest.destinationScenes) + val actions by collectLastValue(underTest.actions) val homeScene by collectLastValue(kosmos.homeSceneFamilyResolver.resolvedScene) lockDevice() unlockDevice() - assertThat(destinationScenes?.get(Swipe.Down)?.toScene).isEqualTo(SceneFamilies.Home) - assertThat(destinationScenes?.get(Swipe.Up)).isNull() + assertThat(actions?.get(Swipe.Down)?.toScene).isEqualTo(SceneFamilies.Home) + assertThat(actions?.get(Swipe.Up)).isNull() assertThat(homeScene).isEqualTo(Scenes.Gone) } @Test fun upTransitionSceneKey_authMethodSwipe_lockscreenNotDismissed_goesToLockscreen() = testScope.runTest { - val destinationScenes by collectLastValue(underTest.destinationScenes) + val actions by collectLastValue(underTest.actions) val homeScene by collectLastValue(kosmos.homeSceneFamilyResolver.resolvedScene) kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true) kosmos.fakeAuthenticationRepository.setAuthenticationMethod( @@ -136,14 +143,14 @@ class QuickSettingsShadeSceneViewModelTest : SysuiTestCase() { ) sceneInteractor.changeScene(Scenes.Lockscreen, "reason") - assertThat(destinationScenes?.get(Swipe.Up)?.toScene).isEqualTo(SceneFamilies.Home) + assertThat(actions?.get(Swipe.Up)?.toScene).isEqualTo(SceneFamilies.Home) assertThat(homeScene).isEqualTo(Scenes.Lockscreen) } @Test fun upTransitionSceneKey_authMethodSwipe_lockscreenDismissed_goesToGone() = testScope.runTest { - val destinationScenes by collectLastValue(underTest.destinationScenes) + val actions by collectLastValue(underTest.actions) val homeScene by collectLastValue(kosmos.homeSceneFamilyResolver.resolvedScene) kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true) kosmos.fakeAuthenticationRepository.setAuthenticationMethod( @@ -152,26 +159,26 @@ class QuickSettingsShadeSceneViewModelTest : SysuiTestCase() { runCurrent() sceneInteractor.changeScene(Scenes.Gone, "reason") - assertThat(destinationScenes?.get(Swipe.Up)?.toScene).isEqualTo(SceneFamilies.Home) + assertThat(actions?.get(Swipe.Up)?.toScene).isEqualTo(SceneFamilies.Home) assertThat(homeScene).isEqualTo(Scenes.Gone) } @Test fun backTransitionSceneKey_notEditing_Home() = testScope.runTest { - val destinationScenes by collectLastValue(underTest.destinationScenes) + val actions by collectLastValue(underTest.actions) - assertThat(destinationScenes?.get(Back)?.toScene).isEqualTo(SceneFamilies.Home) + assertThat(actions?.get(Back)?.toScene).isEqualTo(SceneFamilies.Home) } @Test fun backTransition_editing_noDestination() = testScope.runTest { - val destinationScenes by collectLastValue(underTest.destinationScenes) + val actions by collectLastValue(underTest.actions) kosmos.editModeViewModel.startEditing() - assertThat(destinationScenes!!).isNotEmpty() - assertThat(destinationScenes?.get(Back)).isNull() + assertThat(actions!!).isNotEmpty() + assertThat(actions?.get(Back)).isNull() } private fun TestScope.lockDevice() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/OWNERS b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/OWNERS new file mode 100644 index 000000000000..e322e38ac1e1 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/OWNERS @@ -0,0 +1 @@ +file:/packages/SystemUI/src/com/android/systemui/scene/OWNERS diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt index 72a5cd130427..66e45ab8ccbe 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt @@ -63,8 +63,10 @@ import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.shared.model.fakeSceneDataSource import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel import com.android.systemui.shade.domain.interactor.shadeInteractor -import com.android.systemui.shade.ui.viewmodel.ShadeSceneViewModel -import com.android.systemui.shade.ui.viewmodel.shadeSceneViewModel +import com.android.systemui.shade.ui.viewmodel.ShadeSceneActionsViewModel +import com.android.systemui.shade.ui.viewmodel.ShadeSceneContentViewModel +import com.android.systemui.shade.ui.viewmodel.shadeSceneActionsViewModel +import com.android.systemui.shade.ui.viewmodel.shadeSceneContentViewModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository import com.android.systemui.telephony.data.repository.fakeTelephonyRepository @@ -147,7 +149,8 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { ) } - private lateinit var shadeSceneViewModel: ShadeSceneViewModel + private lateinit var shadeSceneContentViewModel: ShadeSceneContentViewModel + private lateinit var shadeSceneActionsViewModel: ShadeSceneActionsViewModel private val powerInteractor by lazy { kosmos.powerInteractor } @@ -186,12 +189,15 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { bouncerActionButtonInteractor = kosmos.bouncerActionButtonInteractor bouncerViewModel = kosmos.bouncerViewModel - shadeSceneViewModel = kosmos.shadeSceneViewModel + shadeSceneContentViewModel = kosmos.shadeSceneContentViewModel + shadeSceneActionsViewModel = kosmos.shadeSceneActionsViewModel val startable = kosmos.sceneContainerStartable startable.start() lockscreenSceneActionsViewModel.activateIn(testScope) + shadeSceneContentViewModel.activateIn(testScope) + shadeSceneActionsViewModel.activateIn(testScope) assertWithMessage("Initial scene key mismatch!") .that(sceneContainerViewModel.currentScene.value) @@ -220,8 +226,8 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { @Test fun swipeUpOnLockscreen_enterCorrectPin_unlocksDevice() = testScope.runTest { - val destinationScenes by collectLastValue(lockscreenSceneActionsViewModel.actions) - val upDestinationSceneKey = destinationScenes?.get(Swipe.Up)?.toScene + val actions by collectLastValue(lockscreenSceneActionsViewModel.actions) + val upDestinationSceneKey = actions?.get(Swipe.Up)?.toScene assertThat(upDestinationSceneKey).isEqualTo(Scenes.Bouncer) emulateUserDrivenTransition( to = upDestinationSceneKey, @@ -240,8 +246,8 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { testScope.runTest { setAuthMethod(AuthenticationMethodModel.None, enableLockscreen = true) - val destinationScenes by collectLastValue(lockscreenSceneActionsViewModel.actions) - val upDestinationSceneKey = destinationScenes?.get(Swipe.Up)?.toScene + val actions by collectLastValue(lockscreenSceneActionsViewModel.actions) + val upDestinationSceneKey = actions?.get(Swipe.Up)?.toScene assertThat(upDestinationSceneKey).isEqualTo(Scenes.Gone) emulateUserDrivenTransition( to = upDestinationSceneKey, @@ -251,7 +257,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { @Test fun swipeUpOnShadeScene_withAuthMethodSwipe_lockscreenNotDismissed_goesToLockscreen() = testScope.runTest { - val destinationScenes by collectLastValue(shadeSceneViewModel.destinationScenes) + val actions by collectLastValue(shadeSceneActionsViewModel.actions) val homeScene by collectLastValue(kosmos.homeSceneFamilyResolver.resolvedScene) setAuthMethod(AuthenticationMethodModel.None, enableLockscreen = true) assertCurrentScene(Scenes.Lockscreen) @@ -260,7 +266,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { emulateUserDrivenTransition(to = Scenes.Shade) assertCurrentScene(Scenes.Shade) - val upDestinationSceneKey = destinationScenes?.get(Swipe.Up)?.toScene + val upDestinationSceneKey = actions?.get(Swipe.Up)?.toScene assertThat(upDestinationSceneKey).isEqualTo(SceneFamilies.Home) assertThat(homeScene).isEqualTo(Scenes.Lockscreen) emulateUserDrivenTransition( @@ -271,7 +277,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { @Test fun swipeUpOnShadeScene_withAuthMethodSwipe_lockscreenDismissed_goesToGone() = testScope.runTest { - val destinationScenes by collectLastValue(shadeSceneViewModel.destinationScenes) + val actions by collectLastValue(shadeSceneActionsViewModel.actions) val canSwipeToEnter by collectLastValue(deviceEntryInteractor.canSwipeToEnter) val homeScene by collectLastValue(kosmos.homeSceneFamilyResolver.resolvedScene) @@ -288,7 +294,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { emulateUserDrivenTransition(to = Scenes.Shade) assertCurrentScene(Scenes.Shade) - val upDestinationSceneKey = destinationScenes?.get(Swipe.Up)?.toScene + val upDestinationSceneKey = actions?.get(Swipe.Up)?.toScene assertThat(upDestinationSceneKey).isEqualTo(SceneFamilies.Home) assertThat(homeScene).isEqualTo(Scenes.Gone) emulateUserDrivenTransition( @@ -346,8 +352,8 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { fun swipeUpOnLockscreenWhileUnlocked_dismissesLockscreen() = testScope.runTest { unlockDevice() - val destinationScenes by collectLastValue(lockscreenSceneActionsViewModel.actions) - val upDestinationSceneKey = destinationScenes?.get(Swipe.Up)?.toScene + val actions by collectLastValue(lockscreenSceneActionsViewModel.actions) + val upDestinationSceneKey = actions?.get(Swipe.Up)?.toScene assertThat(upDestinationSceneKey).isEqualTo(Scenes.Gone) } @@ -368,8 +374,8 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { fun dismissingIme_whileOnPasswordBouncer_navigatesToLockscreen() = testScope.runTest { setAuthMethod(AuthenticationMethodModel.Password) - val destinationScenes by collectLastValue(lockscreenSceneActionsViewModel.actions) - val upDestinationSceneKey = destinationScenes?.get(Swipe.Up)?.toScene + val actions by collectLastValue(lockscreenSceneActionsViewModel.actions) + val upDestinationSceneKey = actions?.get(Swipe.Up)?.toScene assertThat(upDestinationSceneKey).isEqualTo(Scenes.Bouncer) emulateUserDrivenTransition( to = upDestinationSceneKey, @@ -386,8 +392,8 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { fun bouncerActionButtonClick_opensEmergencyServicesDialer() = testScope.runTest { setAuthMethod(AuthenticationMethodModel.Password) - val destinationScenes by collectLastValue(lockscreenSceneActionsViewModel.actions) - val upDestinationSceneKey = destinationScenes?.get(Swipe.Up)?.toScene + val actions by collectLastValue(lockscreenSceneActionsViewModel.actions) + val upDestinationSceneKey = actions?.get(Swipe.Up)?.toScene assertThat(upDestinationSceneKey).isEqualTo(Scenes.Bouncer) emulateUserDrivenTransition(to = upDestinationSceneKey) @@ -406,8 +412,8 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { testScope.runTest { setAuthMethod(AuthenticationMethodModel.Password) startPhoneCall() - val destinationScenes by collectLastValue(lockscreenSceneActionsViewModel.actions) - val upDestinationSceneKey = destinationScenes?.get(Swipe.Up)?.toScene + val actions by collectLastValue(lockscreenSceneActionsViewModel.actions) + val upDestinationSceneKey = actions?.get(Swipe.Up)?.toScene assertThat(upDestinationSceneKey).isEqualTo(Scenes.Bouncer) emulateUserDrivenTransition(to = upDestinationSceneKey) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModelTest.kt index 0ffabd807ba7..3f087b48f509 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModelTest.kt @@ -29,6 +29,7 @@ import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus import com.android.systemui.kosmos.testScope +import com.android.systemui.lifecycle.activateIn import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.testKosmos @@ -37,6 +38,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -54,6 +56,11 @@ class OverlayShadeViewModelTest : SysuiTestCase() { private val underTest = kosmos.overlayShadeViewModel + @Before + fun setUp() { + underTest.activateIn(testScope) + } + @Test fun backgroundScene_deviceLocked_lockscreen() = testScope.runTest { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt index 3ded8a346ce9..f6fe667ff813 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt @@ -15,6 +15,7 @@ import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus import com.android.systemui.kosmos.testScope +import com.android.systemui.lifecycle.activateIn import com.android.systemui.plugins.activityStarter import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Scenes @@ -52,6 +53,7 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { @Before fun setUp() { MockitoAnnotations.initMocks(this) + underTest.activateIn(testScope) } @Test @@ -107,15 +109,15 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { @Test fun onSystemIconContainerClicked_unlocked_collapsesShadeToGone() = - testScope.runTest { - setDeviceEntered(true) - setScene(Scenes.Shade) + testScope.runTest { + setDeviceEntered(true) + setScene(Scenes.Shade) - underTest.onSystemIconContainerClicked() - runCurrent() + underTest.onSystemIconContainerClicked() + runCurrent() - assertThat(sceneInteractor.currentScene.value).isEqualTo(Scenes.Gone) - } + assertThat(sceneInteractor.currentScene.value).isEqualTo(Scenes.Gone) + } companion object { private val SUB_1 = @@ -137,7 +139,7 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { private fun setScene(key: SceneKey) { sceneInteractor.changeScene(key, "test") sceneInteractor.setTransitionState( - MutableStateFlow<ObservableTransitionState>(ObservableTransitionState.Idle(key)) + MutableStateFlow<ObservableTransitionState>(ObservableTransitionState.Idle(key)) ) testScope.runCurrent() } @@ -146,16 +148,16 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { if (isEntered) { // Unlock the device marking the device has entered. kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( - SuccessFingerprintAuthenticationStatus(0, true) + SuccessFingerprintAuthenticationStatus(0, true) ) runCurrent() } setScene( - if (isEntered) { - Scenes.Gone - } else { - Scenes.Lockscreen - } + if (isEntered) { + Scenes.Gone + } else { + Scenes.Lockscreen + } ) assertThat(deviceEntryInteractor.isDeviceEntered.value).isEqualTo(isEntered) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneActionsViewModelTest.kt index 3b2c9819c9b7..06a02c65bc64 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneActionsViewModelTest.kt @@ -27,7 +27,6 @@ import com.android.compose.animation.scene.SwipeDirection import com.android.systemui.SysuiTestCase import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository import com.android.systemui.authentication.shared.model.AuthenticationMethodModel -import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository import com.android.systemui.coroutines.collectLastValue import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor @@ -37,8 +36,6 @@ import com.android.systemui.keyguard.domain.interactor.keyguardEnabledInteractor import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus import com.android.systemui.kosmos.testScope import com.android.systemui.lifecycle.activateIn -import com.android.systemui.media.controls.data.repository.mediaFilterRepository -import com.android.systemui.media.controls.shared.model.MediaData import com.android.systemui.qs.ui.adapter.fakeQSSceneAdapter import com.android.systemui.res.R import com.android.systemui.scene.domain.interactor.sceneInteractor @@ -49,14 +46,10 @@ import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade import com.android.systemui.shade.data.repository.shadeRepository import com.android.systemui.shade.domain.startable.shadeStartable import com.android.systemui.shade.shared.flag.DualShade -import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.testKosmos -import com.android.systemui.unfold.fakeUnfoldTransitionProgressProvider import com.google.common.truth.Truth.assertThat -import java.util.Locale import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest @@ -70,7 +63,7 @@ import org.junit.runner.RunWith @TestableLooper.RunWithLooper @EnableSceneContainer @DisableFlags(DualShade.FLAG_NAME) -class ShadeSceneViewModelTest : SysuiTestCase() { +class ShadeSceneActionsViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope @@ -78,7 +71,7 @@ class ShadeSceneViewModelTest : SysuiTestCase() { private val shadeRepository by lazy { kosmos.shadeRepository } private val qsSceneAdapter by lazy { kosmos.fakeQSSceneAdapter } - private val underTest: ShadeSceneViewModel by lazy { kosmos.shadeSceneViewModel } + private val underTest: ShadeSceneActionsViewModel by lazy { kosmos.shadeSceneActionsViewModel } @Before fun setUp() { @@ -88,13 +81,13 @@ class ShadeSceneViewModelTest : SysuiTestCase() { @Test fun upTransitionSceneKey_deviceLocked_lockScreen() = testScope.runTest { - val destinationScenes by collectLastValue(underTest.destinationScenes) + val actions by collectLastValue(underTest.actions) val homeScene by collectLastValue(kosmos.homeSceneFamilyResolver.resolvedScene) kosmos.fakeAuthenticationRepository.setAuthenticationMethod( AuthenticationMethodModel.Pin ) - assertThat(destinationScenes?.get(Swipe(SwipeDirection.Up))?.toScene) + assertThat(actions?.get(Swipe(SwipeDirection.Up))?.toScene) .isEqualTo(SceneFamilies.Home) assertThat(homeScene).isEqualTo(Scenes.Lockscreen) } @@ -102,14 +95,14 @@ class ShadeSceneViewModelTest : SysuiTestCase() { @Test fun upTransitionSceneKey_deviceUnlocked_gone() = testScope.runTest { - val destinationScenes by collectLastValue(underTest.destinationScenes) + val actions by collectLastValue(underTest.actions) val homeScene by collectLastValue(kosmos.homeSceneFamilyResolver.resolvedScene) kosmos.fakeAuthenticationRepository.setAuthenticationMethod( AuthenticationMethodModel.Pin ) setDeviceEntered(true) - assertThat(destinationScenes?.get(Swipe(SwipeDirection.Up))?.toScene) + assertThat(actions?.get(Swipe(SwipeDirection.Up))?.toScene) .isEqualTo(SceneFamilies.Home) assertThat(homeScene).isEqualTo(Scenes.Gone) } @@ -117,14 +110,14 @@ class ShadeSceneViewModelTest : SysuiTestCase() { @Test fun upTransitionSceneKey_keyguardDisabled_gone() = testScope.runTest { - val destinationScenes by collectLastValue(underTest.destinationScenes) + val actions by collectLastValue(underTest.actions) val homeScene by collectLastValue(kosmos.homeSceneFamilyResolver.resolvedScene) kosmos.fakeAuthenticationRepository.setAuthenticationMethod( AuthenticationMethodModel.Pin ) kosmos.keyguardEnabledInteractor.notifyKeyguardEnabled(false) - assertThat(destinationScenes?.get(Swipe(SwipeDirection.Up))?.toScene) + assertThat(actions?.get(Swipe(SwipeDirection.Up))?.toScene) .isEqualTo(SceneFamilies.Home) assertThat(homeScene).isEqualTo(Scenes.Gone) } @@ -132,7 +125,7 @@ class ShadeSceneViewModelTest : SysuiTestCase() { @Test fun upTransitionSceneKey_authMethodSwipe_lockscreenNotDismissed_goesToLockscreen() = testScope.runTest { - val destinationScenes by collectLastValue(underTest.destinationScenes) + val actions by collectLastValue(underTest.actions) val homeScene by collectLastValue(kosmos.homeSceneFamilyResolver.resolvedScene) kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true) kosmos.fakeAuthenticationRepository.setAuthenticationMethod( @@ -140,7 +133,7 @@ class ShadeSceneViewModelTest : SysuiTestCase() { ) sceneInteractor.changeScene(Scenes.Lockscreen, "reason") - assertThat(destinationScenes?.get(Swipe(SwipeDirection.Up))?.toScene) + assertThat(actions?.get(Swipe(SwipeDirection.Up))?.toScene) .isEqualTo(SceneFamilies.Home) assertThat(homeScene).isEqualTo(Scenes.Lockscreen) } @@ -148,7 +141,7 @@ class ShadeSceneViewModelTest : SysuiTestCase() { @Test fun upTransitionSceneKey_authMethodSwipe_lockscreenDismissed_goesToGone() = testScope.runTest { - val destinationScenes by collectLastValue(underTest.destinationScenes) + val actions by collectLastValue(underTest.actions) val homeScene by collectLastValue(kosmos.homeSceneFamilyResolver.resolvedScene) kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true) kosmos.fakeAuthenticationRepository.setAuthenticationMethod( @@ -157,7 +150,7 @@ class ShadeSceneViewModelTest : SysuiTestCase() { runCurrent() sceneInteractor.changeScene(Scenes.Gone, "reason") - assertThat(destinationScenes?.get(Swipe(SwipeDirection.Up))?.toScene) + assertThat(actions?.get(Swipe(SwipeDirection.Up))?.toScene) .isEqualTo(SceneFamilies.Home) assertThat(homeScene).isEqualTo(Scenes.Gone) } @@ -165,78 +158,22 @@ class ShadeSceneViewModelTest : SysuiTestCase() { @Test fun upTransitionKey_splitShadeEnabled_isGoneToSplitShade() = testScope.runTest { - val destinationScenes by collectLastValue(underTest.destinationScenes) + val actions by collectLastValue(underTest.actions) shadeRepository.setShadeLayoutWide(true) runCurrent() - assertThat(destinationScenes?.get(Swipe(SwipeDirection.Up))?.transitionKey) + assertThat(actions?.get(Swipe(SwipeDirection.Up))?.transitionKey) .isEqualTo(ToSplitShade) } @Test fun upTransitionKey_splitShadeDisable_isNull() = testScope.runTest { - val destinationScenes by collectLastValue(underTest.destinationScenes) + val actions by collectLastValue(underTest.actions) shadeRepository.setShadeLayoutWide(false) runCurrent() - assertThat(destinationScenes?.get(Swipe(SwipeDirection.Up))?.transitionKey).isNull() - } - - @Test - fun isClickable_deviceUnlocked_false() = - testScope.runTest { - val isClickable by collectLastValue(underTest.isClickable) - kosmos.fakeAuthenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin - ) - setDeviceEntered(true) - runCurrent() - - assertThat(isClickable).isFalse() - } - - @Test - fun isClickable_deviceLockedSecurely_true() = - testScope.runTest { - val isClickable by collectLastValue(underTest.isClickable) - kosmos.fakeAuthenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin - ) - runCurrent() - - assertThat(isClickable).isTrue() - } - - @Test - fun onContentClicked_deviceLockedSecurely_switchesToLockscreen() = - testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.currentScene) - kosmos.fakeAuthenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin - ) - runCurrent() - - underTest.onContentClicked() - - assertThat(currentScene).isEqualTo(Scenes.Lockscreen) - } - - @Test - fun addAndRemoveMedia_mediaVisibilityisUpdated() = - testScope.runTest { - val isMediaVisible by collectLastValue(underTest.isMediaVisible) - val userMedia = MediaData(active = true) - - assertThat(isMediaVisible).isFalse() - - kosmos.mediaFilterRepository.addSelectedUserMediaEntry(userMedia) - - assertThat(isMediaVisible).isTrue() - - kosmos.mediaFilterRepository.removeSelectedUserMediaEntry(userMedia.instanceId) - - assertThat(isMediaVisible).isFalse() + assertThat(actions?.get(Swipe(SwipeDirection.Up))?.transitionKey).isNull() } @Test @@ -244,8 +181,8 @@ class ShadeSceneViewModelTest : SysuiTestCase() { testScope.runTest { overrideResource(R.bool.config_use_split_notification_shade, true) kosmos.shadeStartable.start() - val destinationScenes by collectLastValue(underTest.destinationScenes) - assertThat(destinationScenes?.get(Swipe(SwipeDirection.Down))?.toScene).isNull() + val actions by collectLastValue(underTest.actions) + assertThat(actions?.get(Swipe(SwipeDirection.Down))?.toScene).isNull() } @Test @@ -253,90 +190,25 @@ class ShadeSceneViewModelTest : SysuiTestCase() { testScope.runTest { overrideResource(R.bool.config_use_split_notification_shade, false) kosmos.shadeStartable.start() - val destinationScenes by collectLastValue(underTest.destinationScenes) - assertThat(destinationScenes?.get(Swipe(SwipeDirection.Down))?.toScene) + val actions by collectLastValue(underTest.actions) + assertThat(actions?.get(Swipe(SwipeDirection.Down))?.toScene) .isEqualTo(Scenes.QuickSettings) } @Test fun upTransitionSceneKey_customizing_noTransition() = testScope.runTest { - val destinationScenes by collectLastValue(underTest.destinationScenes) + val actions by collectLastValue(underTest.actions) qsSceneAdapter.setCustomizing(true) assertThat( - destinationScenes!!.keys.filterIsInstance<Swipe>().filter { + actions!!.keys.filterIsInstance<Swipe>().filter { it.direction == SwipeDirection.Up } ) .isEmpty() } - @Test - fun shadeMode() = - testScope.runTest { - val shadeMode by collectLastValue(underTest.shadeMode) - - shadeRepository.setShadeLayoutWide(true) - assertThat(shadeMode).isEqualTo(ShadeMode.Split) - - shadeRepository.setShadeLayoutWide(false) - assertThat(shadeMode).isEqualTo(ShadeMode.Single) - - shadeRepository.setShadeLayoutWide(true) - assertThat(shadeMode).isEqualTo(ShadeMode.Split) - } - - @Test - fun unfoldTransitionProgress() = - testScope.runTest { - val maxTranslation = prepareConfiguration() - val translations by - collectLastValue( - combine( - underTest.unfoldTranslationX(isOnStartSide = true), - underTest.unfoldTranslationX(isOnStartSide = false), - ) { start, end -> - Translations( - start = start, - end = end, - ) - } - ) - - val unfoldProvider = kosmos.fakeUnfoldTransitionProgressProvider - unfoldProvider.onTransitionStarted() - assertThat(translations?.start).isEqualTo(0f) - assertThat(translations?.end).isEqualTo(-0f) - - repeat(10) { repetition -> - val transitionProgress = 0.1f * (repetition + 1) - unfoldProvider.onTransitionProgress(transitionProgress) - assertThat(translations?.start).isEqualTo((1 - transitionProgress) * maxTranslation) - assertThat(translations?.end).isEqualTo(-(1 - transitionProgress) * maxTranslation) - } - - unfoldProvider.onTransitionFinishing() - assertThat(translations?.start).isEqualTo(0f) - assertThat(translations?.end).isEqualTo(-0f) - - unfoldProvider.onTransitionFinished() - assertThat(translations?.start).isEqualTo(0f) - assertThat(translations?.end).isEqualTo(-0f) - } - - private fun prepareConfiguration(): Int { - val configuration = context.resources.configuration - configuration.setLayoutDirection(Locale.US) - kosmos.fakeConfigurationRepository.onConfigurationChange(configuration) - val maxTranslation = 10 - kosmos.fakeConfigurationRepository.setDimensionPixelSize( - R.dimen.notification_side_paddings, - maxTranslation - ) - return maxTranslation - } - private fun TestScope.setDeviceEntered(isEntered: Boolean) { if (isEntered) { // Unlock the device marking the device has entered. @@ -362,9 +234,4 @@ class ShadeSceneViewModelTest : SysuiTestCase() { ) runCurrent() } - - private data class Translations( - val start: Float, - val end: Float, - ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneContentViewModelTest.kt new file mode 100644 index 000000000000..558606f00300 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneContentViewModelTest.kt @@ -0,0 +1,231 @@ +/* + * Copyright (C) 2023 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.shade.ui.viewmodel + +import android.platform.test.annotations.DisableFlags +import android.testing.TestableLooper +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.compose.animation.scene.ObservableTransitionState +import com.android.compose.animation.scene.SceneKey +import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor +import com.android.systemui.flags.EnableSceneContainer +import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository +import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus +import com.android.systemui.kosmos.testScope +import com.android.systemui.lifecycle.activateIn +import com.android.systemui.media.controls.data.repository.mediaFilterRepository +import com.android.systemui.media.controls.shared.model.MediaData +import com.android.systemui.qs.ui.adapter.fakeQSSceneAdapter +import com.android.systemui.res.R +import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.shade.data.repository.shadeRepository +import com.android.systemui.shade.shared.flag.DualShade +import com.android.systemui.shade.shared.model.ShadeMode +import com.android.systemui.testKosmos +import com.android.systemui.unfold.fakeUnfoldTransitionProgressProvider +import com.google.common.truth.Truth.assertThat +import java.util.Locale +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +@TestableLooper.RunWithLooper +@EnableSceneContainer +@DisableFlags(DualShade.FLAG_NAME) +class ShadeSceneContentViewModelTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val sceneInteractor by lazy { kosmos.sceneInteractor } + private val shadeRepository by lazy { kosmos.shadeRepository } + private val qsSceneAdapter by lazy { kosmos.fakeQSSceneAdapter } + + private val underTest: ShadeSceneContentViewModel by lazy { kosmos.shadeSceneContentViewModel } + + @Before + fun setUp() { + underTest.activateIn(testScope) + } + + @Test + fun isEmptySpaceClickable_deviceUnlocked_false() = + testScope.runTest { + val isEmptySpaceClickable by collectLastValue(underTest.isEmptySpaceClickable) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Pin + ) + setDeviceEntered(true) + runCurrent() + + assertThat(isEmptySpaceClickable).isFalse() + } + + @Test + fun isEmptySpaceClickable_deviceLockedSecurely_true() = + testScope.runTest { + val isEmptySpaceClickable by collectLastValue(underTest.isEmptySpaceClickable) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Pin + ) + runCurrent() + + assertThat(isEmptySpaceClickable).isTrue() + } + + @Test + fun onEmptySpaceClicked_deviceLockedSecurely_switchesToLockscreen() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Pin + ) + runCurrent() + + underTest.onEmptySpaceClicked() + + assertThat(currentScene).isEqualTo(Scenes.Lockscreen) + } + + @Test + fun addAndRemoveMedia_mediaVisibilityisUpdated() = + testScope.runTest { + val isMediaVisible by collectLastValue(underTest.isMediaVisible) + val userMedia = MediaData(active = true) + + assertThat(isMediaVisible).isFalse() + + kosmos.mediaFilterRepository.addSelectedUserMediaEntry(userMedia) + + assertThat(isMediaVisible).isTrue() + + kosmos.mediaFilterRepository.removeSelectedUserMediaEntry(userMedia.instanceId) + + assertThat(isMediaVisible).isFalse() + } + + @Test + fun shadeMode() = + testScope.runTest { + val shadeMode by collectLastValue(underTest.shadeMode) + + shadeRepository.setShadeLayoutWide(true) + assertThat(shadeMode).isEqualTo(ShadeMode.Split) + + shadeRepository.setShadeLayoutWide(false) + assertThat(shadeMode).isEqualTo(ShadeMode.Single) + + shadeRepository.setShadeLayoutWide(true) + assertThat(shadeMode).isEqualTo(ShadeMode.Split) + } + + @Test + fun unfoldTransitionProgress() = + testScope.runTest { + val maxTranslation = prepareConfiguration() + val translations by + collectLastValue( + combine( + underTest.unfoldTranslationX(isOnStartSide = true), + underTest.unfoldTranslationX(isOnStartSide = false), + ) { start, end -> + Translations( + start = start, + end = end, + ) + } + ) + + val unfoldProvider = kosmos.fakeUnfoldTransitionProgressProvider + unfoldProvider.onTransitionStarted() + assertThat(translations?.start).isEqualTo(0f) + assertThat(translations?.end).isEqualTo(-0f) + + repeat(10) { repetition -> + val transitionProgress = 0.1f * (repetition + 1) + unfoldProvider.onTransitionProgress(transitionProgress) + assertThat(translations?.start).isEqualTo((1 - transitionProgress) * maxTranslation) + assertThat(translations?.end).isEqualTo(-(1 - transitionProgress) * maxTranslation) + } + + unfoldProvider.onTransitionFinishing() + assertThat(translations?.start).isEqualTo(0f) + assertThat(translations?.end).isEqualTo(-0f) + + unfoldProvider.onTransitionFinished() + assertThat(translations?.start).isEqualTo(0f) + assertThat(translations?.end).isEqualTo(-0f) + } + + private fun prepareConfiguration(): Int { + val configuration = context.resources.configuration + configuration.setLayoutDirection(Locale.US) + kosmos.fakeConfigurationRepository.onConfigurationChange(configuration) + val maxTranslation = 10 + kosmos.fakeConfigurationRepository.setDimensionPixelSize( + R.dimen.notification_side_paddings, + maxTranslation + ) + return maxTranslation + } + + private fun TestScope.setDeviceEntered(isEntered: Boolean) { + if (isEntered) { + // Unlock the device marking the device has entered. + kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( + SuccessFingerprintAuthenticationStatus(0, true) + ) + runCurrent() + } + setScene( + if (isEntered) { + Scenes.Gone + } else { + Scenes.Lockscreen + } + ) + assertThat(kosmos.deviceEntryInteractor.isDeviceEntered.value).isEqualTo(isEntered) + } + + private fun TestScope.setScene(key: SceneKey) { + sceneInteractor.changeScene(key, "test") + sceneInteractor.setTransitionState( + MutableStateFlow<ObservableTransitionState>(ObservableTransitionState.Idle(key)) + ) + runCurrent() + } + + private data class Translations( + val start: Float, + val end: Float, + ) +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt index 32f66c1ccd66..11504aadf743 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt @@ -172,7 +172,7 @@ class ZenModeInteractorTest : SysuiTestCase() { @Test fun shouldAskForZenDuration_changesWithSetting() = testScope.runTest { - val manualDnd = TestModeBuilder.MANUAL_DND + val manualDnd = TestModeBuilder.MANUAL_DND_ACTIVE settingsRepository.setInt(ZEN_DURATION, ZEN_DURATION_FOREVER) runCurrent() @@ -201,7 +201,7 @@ class ZenModeInteractorTest : SysuiTestCase() { @Test fun activateMode_usesCorrectDuration() = testScope.runTest { - val manualDnd = TestModeBuilder.MANUAL_DND + val manualDnd = TestModeBuilder.MANUAL_DND_ACTIVE zenModeRepository.addModes(listOf(manualDnd)) settingsRepository.setInt(ZEN_DURATION, ZEN_DURATION_FOREVER) runCurrent() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt index 62161bfeffb3..bcad7e7bc31c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt @@ -69,7 +69,7 @@ class ModesDialogViewModelTest : SysuiTestCase() { .setName("Disabled by other") .setEnabled(false, /* byUser= */ false) .build(), - TestModeBuilder.MANUAL_DND, + TestModeBuilder.MANUAL_DND_ACTIVE, TestModeBuilder() .setName("Enabled") .setEnabled(true) @@ -91,7 +91,7 @@ class ModesDialogViewModelTest : SysuiTestCase() { assertThat(this.enabled).isEqualTo(false) } with(tiles?.elementAt(1)!!) { - assertThat(this.text).isEqualTo("Manual DND") + assertThat(this.text).isEqualTo("Do Not Disturb") assertThat(this.subtext).isEqualTo("On") assertThat(this.enabled).isEqualTo(true) } diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index e49f8c84b560..875159642f93 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -1367,13 +1367,18 @@ <!-- Media projection that launched from 1P/3P apps --> <!-- 1P/3P app media projection permission dialog title. [CHAR LIMIT=NONE] --> - <string name="media_projection_entry_app_permission_dialog_title">Start recording or casting with <xliff:g id="app_seeking_permission" example="Meet">%s</xliff:g>?</string> + <string name="media_projection_entry_app_permission_dialog_title">Share your screen with <xliff:g id="app_seeking_permission" example="Meet">%s</xliff:g>?</string> + + <!-- 1P/3P app media projection permission option for capturing just a single app [CHAR LIMIT=50] --> + <string name="media_projection_entry_app_permission_dialog_option_text_single_app">Share one app</string> + <!-- 1P/3P app media projection permission option for capturing the whole screen [CHAR LIMIT=50] --> + <string name="media_projection_entry_app_permission_dialog_option_text_entire_screen">Share entire screen</string> <!-- 1P/3P app media projection permission warning for capturing the whole screen. [CHAR LIMIT=350] --> - <string name="media_projection_entry_app_permission_dialog_warning_entire_screen">When you’re sharing, recording, or casting, <xliff:g id="app_seeking_permission" example="Meet">%s</xliff:g> has access to anything visible on your screen or played on your device. So be careful with things like passwords, payment details, messages, photos, and audio and video.</string> + <string name="media_projection_entry_app_permission_dialog_warning_entire_screen">When you’re sharing your entire screen, anything on your screen is visible to <xliff:g id="app_seeking_permission" example="Meet">%s</xliff:g>. So be careful with things like passwords, payment details, messages, photos, and audio and video.</string> <!-- 1P/3P app media projection permission warning for capturing an app. [CHAR LIMIT=350] --> - <string name="media_projection_entry_app_permission_dialog_warning_single_app">When you’re sharing, recording, or casting an app, <xliff:g id="app_seeking_permission" example="Meet">%s</xliff:g> has access to anything shown or played on that app. So be careful with things like passwords, payment details, messages, photos, and audio and video.</string> + <string name="media_projection_entry_app_permission_dialog_warning_single_app">When you’re sharing an app, anything shown or played in that app is visible to <xliff:g id="app_seeking_permission" example="Meet">%s</xliff:g>. So be careful with things like passwords, payment details, messages, photos, and audio and video.</string> <!-- 1P/3P apps media projection permission button to continue with app selection or recording [CHAR LIMIT=60] --> - <string name="media_projection_entry_app_permission_dialog_continue">Start</string> + <string name="media_projection_entry_app_permission_dialog_continue_entire_screen">Share screen</string> <!-- 1P/3P apps disabled the single app projection option. [CHAR LIMIT=NONE] --> <string name="media_projection_entry_app_permission_dialog_single_app_disabled"><xliff:g id="app_name" example="Meet">%1$s</xliff:g> has disabled this option</string> @@ -3674,6 +3679,7 @@ --> <string name="shortcut_helper_key_combinations_or_separator">or</string> + <!-- TOUCHPAD TUTORIAL--> <!-- Label for button opening tutorial for back gesture on touchpad [CHAR LIMIT=NONE] --> <string name="touchpad_tutorial_back_gesture_button">Back gesture</string> <!-- Label for button opening tutorial for back gesture on touchpad [CHAR LIMIT=NONE] --> @@ -3688,19 +3694,29 @@ <!-- Touchpad back gesture guidance in gestures tutorial [CHAR LIMIT=NONE] --> <string name="touchpad_back_gesture_guidance">To go back, swipe left or right using three fingers anywhere on the touchpad.\n\nYou can also use the keyboard shortcut Action + ESC for this.</string> - <!-- Screen title after gesture was done successfully [CHAR LIMIT=NONE] --> - <string name="touchpad_tutorial_gesture_done">Great job!</string> + <!-- Screen title after back gesture was done successfully [CHAR LIMIT=NONE] --> + <string name="touchpad_back_gesture_success_title">Great job!</string> <!-- Text shown to the user after they complete back gesture tutorial [CHAR LIMIT=NONE] --> - <string name="touchpad_back_gesture_finished">You completed the go back gesture.</string> + <string name="touchpad_back_gesture_success_body">You completed the go back gesture.</string> <!-- HOME GESTURE --> <!-- Touchpad home gesture action name in tutorial [CHAR LIMIT=NONE] --> <string name="touchpad_home_gesture_action_title">Go home</string> <!-- Touchpad home gesture guidance in gestures tutorial [CHAR LIMIT=NONE] --> <string name="touchpad_home_gesture_guidance">To go to your home screen at any time, swipe up with three fingers from the bottom of your screen.</string> <!-- Screen title after home gesture was done successfully [CHAR LIMIT=NONE] --> - <string name="touchpad_home_gesture_done">Nice!</string> + <string name="touchpad_home_gesture_success_title">Nice!</string> <!-- Text shown to the user after they complete home gesture tutorial [CHAR LIMIT=NONE] --> - <string name="touchpad_home_gesture_finished">You completed the go home gesture.</string> + <string name="touchpad_home_gesture_success_body">You completed the go home gesture.</string> + + <!-- KEYBOARD TUTORIAL--> + <!-- Action key tutorial title [CHAR LIMIT=NONE] --> + <string name="tutorial_action_key_title">Action key</string> + <!-- Action key tutorial guidance[CHAR LIMIT=NONE] --> + <string name="tutorial_action_key_guidance">To access your apps, press the action key on your keyboard.</string> + <!-- Screen title after action key pressed successfully [CHAR LIMIT=NONE] --> + <string name="tutorial_action_key_success_title">Congratulations!</string> + <!-- Text shown to the user after they complete action key tutorial [CHAR LIMIT=NONE] --> + <string name="tutorial_action_key_success_body">You completed the action key gesture.\n\nAction + / shows all the shortcuts you have available.</string> <!-- Content description for keyboard backlight brightness dialog [CHAR LIMIT=NONE] --> <string name="keyboard_backlight_dialog_title">Keyboard backlight</string> diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationModeSwitch.java b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationModeSwitch.java index d9d9e3781242..e91bb6aefeec 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationModeSwitch.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationModeSwitch.java @@ -46,6 +46,7 @@ import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import android.widget.ImageView; +import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.graphics.SfVsyncFrameCallbackProvider; import com.android.systemui.res.R; @@ -76,6 +77,7 @@ class MagnificationModeSwitch implements MagnificationGestureDetector.OnGestureL private final Context mContext; private final AccessibilityManager mAccessibilityManager; private final WindowManager mWindowManager; + private final ViewCaptureAwareWindowManager mViewCaptureAwareWindowManager; private final ImageView mImageView; private final Runnable mWindowInsetChangeRunnable; private final SfVsyncFrameCallbackProvider mSfVsyncFrameProvider; @@ -99,17 +101,21 @@ class MagnificationModeSwitch implements MagnificationGestureDetector.OnGestureL void onClick(int displayId); } - MagnificationModeSwitch(@UiContext Context context, ClickListener clickListener) { - this(context, createView(context), new SfVsyncFrameCallbackProvider(), clickListener); + MagnificationModeSwitch(@UiContext Context context, ClickListener clickListener, + ViewCaptureAwareWindowManager viewCaptureAwareWindowManager) { + this(context, createView(context), new SfVsyncFrameCallbackProvider(), clickListener, + viewCaptureAwareWindowManager); } @VisibleForTesting MagnificationModeSwitch(Context context, @NonNull ImageView imageView, - SfVsyncFrameCallbackProvider sfVsyncFrameProvider, ClickListener clickListener) { + SfVsyncFrameCallbackProvider sfVsyncFrameProvider, ClickListener clickListener, + ViewCaptureAwareWindowManager viewCaptureAwareWindowManager) { mContext = context; mConfiguration = new Configuration(context.getResources().getConfiguration()); mAccessibilityManager = mContext.getSystemService(AccessibilityManager.class); mWindowManager = mContext.getSystemService(WindowManager.class); + mViewCaptureAwareWindowManager = viewCaptureAwareWindowManager; mSfVsyncFrameProvider = sfVsyncFrameProvider; mClickListener = clickListener; mParams = createLayoutParams(context); @@ -276,7 +282,7 @@ class MagnificationModeSwitch implements MagnificationGestureDetector.OnGestureL mImageView.animate().cancel(); mIsFadeOutAnimating = false; mImageView.setAlpha(0f); - mWindowManager.removeView(mImageView); + mViewCaptureAwareWindowManager.removeView(mImageView); mContext.unregisterComponentCallbacks(this); mIsVisible = false; } @@ -310,7 +316,7 @@ class MagnificationModeSwitch implements MagnificationGestureDetector.OnGestureL mParams.y = mDraggableWindowBounds.bottom; mToLeftScreenEdge = false; } - mWindowManager.addView(mImageView, mParams); + mViewCaptureAwareWindowManager.addView(mImageView, mParams); // Exclude magnification switch button from system gesture area. setSystemGestureExclusion(); mIsVisible = true; diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/ModeSwitchesController.java b/packages/SystemUI/src/com/android/systemui/accessibility/ModeSwitchesController.java index 63f9cc2c1b53..53827e65344a 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/ModeSwitchesController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/ModeSwitchesController.java @@ -25,6 +25,7 @@ import android.content.Context; import android.hardware.display.DisplayManager; import android.view.Display; +import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.dagger.SysUISingleton; @@ -47,8 +48,10 @@ public class ModeSwitchesController implements ClickListener { private ClickListener mClickListenerDelegate; @Inject - public ModeSwitchesController(Context context, DisplayManager displayManager) { - mSwitchSupplier = new SwitchSupplier(context, displayManager, this::onClick); + public ModeSwitchesController(Context context, DisplayManager displayManager, + ViewCaptureAwareWindowManager viewCaptureAwareWindowManager) { + mSwitchSupplier = new SwitchSupplier(context, displayManager, this::onClick, + viewCaptureAwareWindowManager); } @VisibleForTesting @@ -115,6 +118,7 @@ public class ModeSwitchesController implements ClickListener { private final Context mContext; private final ClickListener mClickListener; + private final ViewCaptureAwareWindowManager mViewCaptureAwareWindowManager; /** * Supplies the switch for the given display. @@ -124,17 +128,20 @@ public class ModeSwitchesController implements ClickListener { * @param clickListener The callback that will run when the switch is clicked */ SwitchSupplier(Context context, DisplayManager displayManager, - ClickListener clickListener) { + ClickListener clickListener, + ViewCaptureAwareWindowManager viewCaptureAwareWindowManager) { super(displayManager); mContext = context; mClickListener = clickListener; + mViewCaptureAwareWindowManager = viewCaptureAwareWindowManager; } @Override protected MagnificationModeSwitch createInstance(Display display) { final Context uiContext = mContext.createWindowContext(display, TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY, /* options */ null); - return new MagnificationModeSwitch(uiContext, mClickListener); + return new MagnificationModeSwitch(uiContext, mClickListener, + mViewCaptureAwareWindowManager); } } } diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.kt b/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.kt index d5790a44a887..a093f58b88ba 100644 --- a/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.kt @@ -118,7 +118,8 @@ constructor( if (Flags.dreamOverlayBouncerSwipeDirectionFiltering()) { (abs(distanceY.toDouble()) > abs(distanceX.toDouble()) && distanceY > 0) && - if (Flags.hubmodeFullscreenVerticalSwipe()) touchAvailable else true + if (Flags.hubmodeFullscreenVerticalSwipeFix()) touchAvailable + else true } else { // If the user scrolling favors a vertical direction, begin capturing // scrolls. @@ -175,7 +176,7 @@ constructor( } init { - if (Flags.hubmodeFullscreenVerticalSwipe()) { + if (Flags.hubmodeFullscreenVerticalSwipeFix()) { scope.launch { communalViewModel.glanceableTouchAvailable.collect { onGlanceableTouchAvailable(it) @@ -218,7 +219,7 @@ constructor( val normalRegion = Rect(0, Math.round(height * (1 - bouncerZoneScreenPercentage)), width, height) - if (Flags.hubmodeFullscreenVerticalSwipe()) { + if (Flags.hubmodeFullscreenVerticalSwipeFix()) { region.op(bounds, Region.Op.UNION) exclusionRect?.apply { region.op(this, Region.Op.DIFFERENCE) } } @@ -265,7 +266,7 @@ constructor( when (motionEvent.action) { MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> { - if (Flags.hubmodeFullscreenVerticalSwipe() && capture == true) { + if (Flags.hubmodeFullscreenVerticalSwipeFix() && capture == true) { communalViewModel.onResetTouchState() } touchSession?.apply { pop() } diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.kt b/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.kt index 06b41de12941..9da9a3a98ef5 100644 --- a/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.kt @@ -61,7 +61,7 @@ constructor( private var touchAvailable = false init { - if (Flags.hubmodeFullscreenVerticalSwipe()) { + if (Flags.hubmodeFullscreenVerticalSwipeFix()) { scope.launch { communalViewModel.glanceableTouchAvailable.collect { onGlanceableTouchAvailable(it) @@ -107,7 +107,8 @@ constructor( capture = abs(distanceY.toDouble()) > abs(distanceX.toDouble()) && distanceY < 0 && - if (Flags.hubmodeFullscreenVerticalSwipe()) touchAvailable else true + if (Flags.hubmodeFullscreenVerticalSwipeFix()) touchAvailable + else true if (capture == true) { // Send the initial touches over, as the input listener has already // processed these touches. @@ -144,7 +145,7 @@ constructor( override fun getTouchInitiationRegion(bounds: Rect, region: Region, exclusionRect: Rect?) { // If fullscreen swipe, use entire space minus exclusion region - if (Flags.hubmodeFullscreenVerticalSwipe()) { + if (Flags.hubmodeFullscreenVerticalSwipeFix()) { region.op(bounds, Region.Op.UNION) exclusionRect?.apply { region.op(this, Region.Op.DIFFERENCE) } diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchHandler.java b/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchHandler.java index 190bc1587525..d27e72a9c185 100644 --- a/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchHandler.java @@ -122,4 +122,9 @@ public interface TouchHandler { * @param session */ void onSessionStart(TouchSession session); + + /** + * Called when the handler is being torn down. + */ + default void onDestroy() {} } diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java b/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java index efa55e90081e..1be6f9e7ca4f 100644 --- a/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java +++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java @@ -581,6 +581,10 @@ public class TouchMonitor { mBoundsFlow.cancel(new CancellationException()); } + for (TouchHandler handler : mHandlers) { + handler.onDestroy(); + } + mInitialized = false; } diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt index 4dafa93ab5c2..9d82e7677a87 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt @@ -297,7 +297,7 @@ constructor( val DeviceItem.isMediaDevice: Boolean get() = - cachedBluetoothDevice.connectableProfiles.any { + cachedBluetoothDevice.uiAccessibleProfiles.any { it is A2dpProfile || it is HearingAidProfile || it is LeAudioProfile || diff --git a/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayRepository.kt b/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayRepository.kt index 69ddb62cc05a..40e2f174cfb7 100644 --- a/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayRepository.kt @@ -160,7 +160,10 @@ constructor( .stateIn( bgApplicationScope, SharingStarted.WhileSubscribed(), - emptySet(), + // This is necessary because there might be multiple displays, and we could + // have missed events for those added before this process or flow started. + // Note it causes a binder call from the main thread (it's traced). + getDisplays().map { display -> display.displayId }.toSet(), ) } else { oldEnabledDisplays.map { enabledDisplaysSet -> @@ -186,8 +189,12 @@ constructor( .stateIn( bgApplicationScope, started = SharingStarted.WhileSubscribed(), - initialValue = setOf(defaultDisplay) - ) + // This triggers a single binder call on the UI thread per process. The + // alternative would be to use sharedFlows, but they are prohibited due to + // performance concerns. + // Ultimately, this is a trade-off between a one-time UI thread binder call and + // the constant overhead of sharedFlows. + initialValue = getDisplays()) } else { oldEnabledDisplays } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt index b45ebd865c55..24ac542c6266 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt @@ -44,6 +44,7 @@ import com.android.systemui.statusbar.BlurUtils import com.android.systemui.statusbar.CrossFadeHelper import javax.inject.Inject import javax.inject.Named +import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.launch /** Controller for dream overlay animations. */ @@ -84,51 +85,62 @@ constructor( private var mCurrentBlurRadius: Float = 0f + private var mLifecycleFlowHandle: DisposableHandle? = null + fun init(view: View) { this.view = view - view.repeatWhenAttached { - repeatOnLifecycle(Lifecycle.State.CREATED) { - launch { - dreamViewModel.dreamOverlayTranslationY.collect { px -> - ComplicationLayoutParams.iteratePositions( - { position: Int -> setElementsTranslationYAtPosition(px, position) }, - POSITION_TOP or POSITION_BOTTOM - ) + mLifecycleFlowHandle = + view.repeatWhenAttached { + repeatOnLifecycle(Lifecycle.State.CREATED) { + launch { + dreamViewModel.dreamOverlayTranslationY.collect { px -> + ComplicationLayoutParams.iteratePositions( + { position: Int -> + setElementsTranslationYAtPosition(px, position) + }, + POSITION_TOP or POSITION_BOTTOM + ) + } } - } - launch { - dreamViewModel.dreamOverlayTranslationX.collect { px -> - ComplicationLayoutParams.iteratePositions( - { position: Int -> setElementsTranslationXAtPosition(px, position) }, - POSITION_TOP or POSITION_BOTTOM - ) + launch { + dreamViewModel.dreamOverlayTranslationX.collect { px -> + ComplicationLayoutParams.iteratePositions( + { position: Int -> + setElementsTranslationXAtPosition(px, position) + }, + POSITION_TOP or POSITION_BOTTOM + ) + } } - } - launch { - dreamViewModel.dreamOverlayAlpha.collect { alpha -> - ComplicationLayoutParams.iteratePositions( - { position: Int -> - setElementsAlphaAtPosition( - alpha = alpha, - position = position, - fadingOut = true, - ) - }, - POSITION_TOP or POSITION_BOTTOM - ) + launch { + dreamViewModel.dreamOverlayAlpha.collect { alpha -> + ComplicationLayoutParams.iteratePositions( + { position: Int -> + setElementsAlphaAtPosition( + alpha = alpha, + position = position, + fadingOut = true, + ) + }, + POSITION_TOP or POSITION_BOTTOM + ) + } } - } - launch { - dreamViewModel.transitionEnded.collect { _ -> - mOverlayStateController.setExitAnimationsRunning(false) + launch { + dreamViewModel.transitionEnded.collect { _ -> + mOverlayStateController.setExitAnimationsRunning(false) + } } } } - } + } + + fun destroy() { + mLifecycleFlowHandle?.dispose() } /** diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java index 76c7d2383751..bf6d266ac42f 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java @@ -59,6 +59,7 @@ import com.android.systemui.touch.TouchInsetManager; import com.android.systemui.util.ViewController; import kotlinx.coroutines.CoroutineDispatcher; +import kotlinx.coroutines.DisposableHandle; import kotlinx.coroutines.flow.FlowKt; import java.util.Arrays; @@ -185,6 +186,8 @@ public class DreamOverlayContainerViewController extends } }; + private DisposableHandle mFlowHandle; + @Inject public DreamOverlayContainerViewController( DreamOverlayContainerView containerView, @@ -252,6 +255,17 @@ public class DreamOverlayContainerViewController extends } @Override + public void destroy() { + mStateController.removeCallback(mDreamOverlayStateCallback); + mStatusBarViewController.destroy(); + mComplicationHostViewController.destroy(); + mDreamOverlayAnimationsController.destroy(); + mLowLightTransitionCoordinator.setLowLightEnterListener(null); + + super.destroy(); + } + + @Override protected void onViewAttached() { mWakingUpFromSwipe = false; mJitterStartTimeMillis = System.currentTimeMillis(); @@ -263,7 +277,7 @@ public class DreamOverlayContainerViewController extends emptyRegion.recycle(); if (dreamHandlesBeingObscured()) { - collectFlow( + mFlowHandle = collectFlow( mView, FlowKt.distinctUntilChanged(combineFlows( mKeyguardTransitionInteractor.isFinishedIn( @@ -295,6 +309,10 @@ public class DreamOverlayContainerViewController extends @Override protected void onViewDetached() { + if (mFlowHandle != null) { + mFlowHandle.dispose(); + mFlowHandle = null; + } mHandler.removeCallbacksAndMessages(null); mPrimaryBouncerCallbackInteractor.removeBouncerExpansionCallback(mBouncerExpansionCallback); mBouncerlessScrimController.removeCallback(mBouncerlessExpansionCallback); diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java index 931066d5c582..4b9e5a024393 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java @@ -70,8 +70,12 @@ import com.android.systemui.shade.ShadeExpansionChangeEvent; import com.android.systemui.touch.TouchInsetManager; import com.android.systemui.util.concurrency.DelayableExecutor; +import kotlinx.coroutines.Job; + +import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; +import java.util.concurrent.CancellationException; import java.util.function.Consumer; import javax.inject.Inject; @@ -140,6 +144,8 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ private ComponentName mCurrentBlockedGestureDreamActivityComponent; + private final ArrayList<Job> mFlows = new ArrayList<>(); + /** * This {@link LifecycleRegistry} controls when dream overlay functionality, like touch * handling, should be active. It will automatically be paused when the dream overlay is hidden @@ -309,12 +315,12 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ mExecutor.execute(() -> setLifecycleStateLocked(Lifecycle.State.CREATED)); - collectFlow(getLifecycle(), mCommunalInteractor.isCommunalAvailable(), - mIsCommunalAvailableCallback); - collectFlow(getLifecycle(), communalInteractor.isCommunalVisible(), - mCommunalVisibleConsumer); - collectFlow(getLifecycle(), keyguardInteractor.primaryBouncerShowing, - mBouncerShowingConsumer); + mFlows.add(collectFlow(getLifecycle(), mCommunalInteractor.isCommunalAvailable(), + mIsCommunalAvailableCallback)); + mFlows.add(collectFlow(getLifecycle(), communalInteractor.isCommunalVisible(), + mCommunalVisibleConsumer)); + mFlows.add(collectFlow(getLifecycle(), keyguardInteractor.primaryBouncerShowing, + mBouncerShowingConsumer)); } @NonNull @@ -339,6 +345,11 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ public void onDestroy() { mKeyguardUpdateMonitor.removeCallback(mKeyguardCallback); + for (Job job : mFlows) { + job.cancel(new CancellationException()); + } + mFlows.clear(); + mExecutor.execute(() -> { setLifecycleStateLocked(Lifecycle.State.DESTROYED); @@ -559,6 +570,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ if (mStarted && mWindow != null) { try { + mWindow.clearContentView(); mWindowManager.removeView(mWindow.getDecorView()); } catch (IllegalArgumentException e) { Log.e(TAG, "Error removing decor view when resetting overlay", e); @@ -569,7 +581,10 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ mStateController.setLowLightActive(false); mStateController.setEntryAnimationsFinished(false); - mDreamOverlayContainerViewController = null; + if (mDreamOverlayContainerViewController != null) { + mDreamOverlayContainerViewController.destroy(); + mDreamOverlayContainerViewController = null; + } if (mTouchMonitor != null) { mTouchMonitor.destroy(); diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java index ee7b6f52ac55..5ba780f9c99d 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java @@ -33,7 +33,11 @@ import com.android.systemui.communal.domain.interactor.CommunalInteractor; import com.android.systemui.dreams.touch.dagger.CommunalTouchModule; import com.android.systemui.statusbar.phone.CentralSurfaces; +import kotlinx.coroutines.Job; + +import java.util.ArrayList; import java.util.Optional; +import java.util.concurrent.CancellationException; import java.util.function.Consumer; import javax.inject.Inject; @@ -49,6 +53,8 @@ public class CommunalTouchHandler implements TouchHandler { private final ConfigurationInteractor mConfigurationInteractor; private Boolean mIsEnabled = false; + private ArrayList<Job> mFlows = new ArrayList<>(); + private int mLayoutDirection = LayoutDirection.LTR; @VisibleForTesting @@ -70,17 +76,17 @@ public class CommunalTouchHandler implements TouchHandler { mCommunalInteractor = communalInteractor; mConfigurationInteractor = configurationInteractor; - collectFlow( + mFlows.add(collectFlow( mLifecycle, mCommunalInteractor.isCommunalAvailable(), mIsCommunalAvailableCallback - ); + )); - collectFlow( + mFlows.add(collectFlow( mLifecycle, mConfigurationInteractor.getLayoutDirection(), mLayoutDirectionCallback - ); + )); } @Override @@ -140,4 +146,13 @@ public class CommunalTouchHandler implements TouchHandler { } }); } + + @Override + public void onDestroy() { + for (Job job : mFlows) { + job.cancel(new CancellationException()); + } + mFlows.clear(); + TouchHandler.super.onDestroy(); + } } diff --git a/packages/SystemUI/src/com/android/systemui/education/data/model/GestureEduModel.kt b/packages/SystemUI/src/com/android/systemui/education/data/model/GestureEduModel.kt index 9f6cb4d027e6..a171f8775768 100644 --- a/packages/SystemUI/src/com/android/systemui/education/data/model/GestureEduModel.kt +++ b/packages/SystemUI/src/com/android/systemui/education/data/model/GestureEduModel.kt @@ -26,4 +26,6 @@ data class GestureEduModel( val signalCount: Int = 0, val educationShownCount: Int = 0, val lastShortcutTriggeredTime: Instant? = null, + val usageSessionStartTime: Instant? = null, + val lastEducationTime: Instant? = null, ) diff --git a/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt b/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt index 22ba4ad648f8..7c3d63388aa1 100644 --- a/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt @@ -73,6 +73,8 @@ constructor( const val SIGNAL_COUNT_SUFFIX = "_SIGNAL_COUNT" const val NUMBER_OF_EDU_SHOWN_SUFFIX = "_NUMBER_OF_EDU_SHOWN" const val LAST_SHORTCUT_TRIGGERED_TIME_SUFFIX = "_LAST_SHORTCUT_TRIGGERED_TIME" + const val USAGE_SESSION_START_TIME_SUFFIX = "_USAGE_SESSION_START_TIME" + const val LAST_EDUCATION_TIME_SUFFIX = "_LAST_EDUCATION_TIME" const val DATASTORE_DIR = "education/USER%s_ContextualEducation" } @@ -113,6 +115,14 @@ constructor( preferences[getLastShortcutTriggeredTimeKey(gestureType)]?.let { Instant.ofEpochSecond(it) }, + usageSessionStartTime = + preferences[getUsageSessionStartTimeKey(gestureType)]?.let { + Instant.ofEpochSecond(it) + }, + lastEducationTime = + preferences[getLastEducationTimeKey(gestureType)]?.let { + Instant.ofEpochSecond(it) + }, ) } @@ -125,11 +135,21 @@ constructor( val updatedModel = transform(currentModel) preferences[getSignalCountKey(gestureType)] = updatedModel.signalCount preferences[getEducationShownCountKey(gestureType)] = updatedModel.educationShownCount - updateTimeByInstant( + setInstant( preferences, updatedModel.lastShortcutTriggeredTime, getLastShortcutTriggeredTimeKey(gestureType) ) + setInstant( + preferences, + updatedModel.usageSessionStartTime, + getUsageSessionStartTimeKey(gestureType) + ) + setInstant( + preferences, + updatedModel.lastEducationTime, + getLastEducationTimeKey(gestureType) + ) } } @@ -142,7 +162,13 @@ constructor( private fun getLastShortcutTriggeredTimeKey(gestureType: GestureType): Preferences.Key<Long> = longPreferencesKey(gestureType.name + LAST_SHORTCUT_TRIGGERED_TIME_SUFFIX) - private fun updateTimeByInstant( + private fun getUsageSessionStartTimeKey(gestureType: GestureType): Preferences.Key<Long> = + longPreferencesKey(gestureType.name + USAGE_SESSION_START_TIME_SUFFIX) + + private fun getLastEducationTimeKey(gestureType: GestureType): Preferences.Key<Long> = + longPreferencesKey(gestureType.name + LAST_EDUCATION_TIME_SUFFIX) + + private fun setInstant( preferences: MutablePreferences, instant: Instant?, key: Preferences.Key<Long> diff --git a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractor.kt b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractor.kt index 5ec1006f8c42..db5c386a6c65 100644 --- a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractor.kt @@ -68,7 +68,13 @@ constructor( } suspend fun incrementSignalCount(gestureType: GestureType) { - repository.updateGestureEduModel(gestureType) { it.copy(signalCount = it.signalCount + 1) } + repository.updateGestureEduModel(gestureType) { + it.copy( + signalCount = it.signalCount + 1, + usageSessionStartTime = + if (it.signalCount == 0) clock.instant() else it.usageSessionStartTime + ) + } } suspend fun updateShortcutTriggerTime(gestureType: GestureType) { @@ -76,4 +82,22 @@ constructor( it.copy(lastShortcutTriggeredTime = clock.instant()) } } + + suspend fun updateOnEduTriggered(gestureType: GestureType) { + repository.updateGestureEduModel(gestureType) { + it.copy( + // Reset signal counter and usageSessionStartTime after edu triggered + signalCount = 0, + lastEducationTime = clock.instant(), + educationShownCount = it.educationShownCount + 1, + usageSessionStartTime = null + ) + } + } + + suspend fun startNewUsageSession(gestureType: GestureType) { + repository.updateGestureEduModel(gestureType) { + it.copy(usageSessionStartTime = clock.instant(), signalCount = 1) + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt index 9016c7339c25..3a3fb8c6acbe 100644 --- a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt @@ -17,17 +17,19 @@ package com.android.systemui.education.domain.interactor import com.android.systemui.CoreStartable +import com.android.systemui.contextualeducation.GestureType.BACK import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.contextualeducation.GestureType.BACK +import com.android.systemui.education.dagger.ContextualEducationModule.EduClock import com.android.systemui.education.data.model.GestureEduModel import com.android.systemui.education.shared.model.EducationInfo import com.android.systemui.education.shared.model.EducationUiType +import java.time.Clock import javax.inject.Inject +import kotlin.time.Duration.Companion.hours import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.launch /** Allow listening to new contextual education triggered */ @@ -36,11 +38,13 @@ class KeyboardTouchpadEduInteractor @Inject constructor( @Background private val backgroundScope: CoroutineScope, - private val contextualEducationInteractor: ContextualEducationInteractor + private val contextualEducationInteractor: ContextualEducationInteractor, + @EduClock private val clock: Clock, ) : CoreStartable { companion object { const val MAX_SIGNAL_COUNT: Int = 2 + val usageSessionDuration = 72.hours } private val _educationTriggered = MutableStateFlow<EducationInfo?>(null) @@ -48,25 +52,30 @@ constructor( override fun start() { backgroundScope.launch { - contextualEducationInteractor.backGestureModelFlow - .mapNotNull { getEduType(it) } - .collect { _educationTriggered.value = EducationInfo(BACK, it) } - } - } - - private fun getEduType(model: GestureEduModel): EducationUiType? { - if (isEducationNeeded(model)) { - return EducationUiType.Toast - } else { - return null + contextualEducationInteractor.backGestureModelFlow.collect { + if (isUsageSessionExpired(it)) { + contextualEducationInteractor.startNewUsageSession(BACK) + } else if (isEducationNeeded(it)) { + _educationTriggered.value = EducationInfo(BACK, getEduType(it)) + contextualEducationInteractor.updateOnEduTriggered(BACK) + } + } } } private fun isEducationNeeded(model: GestureEduModel): Boolean { // Todo: b/354884305 - add complete education logic to show education in correct scenarios - val shortcutWasTriggered = model.lastShortcutTriggeredTime == null + val noShortcutTriggered = model.lastShortcutTriggeredTime == null val signalCountReached = model.signalCount >= MAX_SIGNAL_COUNT + return noShortcutTriggered && signalCountReached + } - return shortcutWasTriggered && signalCountReached + private fun isUsageSessionExpired(model: GestureEduModel): Boolean { + return model.usageSessionStartTime + ?.plusSeconds(usageSessionDuration.inWholeSeconds) + ?.isBefore(clock.instant()) ?: false } + + private fun getEduType(model: GestureEduModel) = + if (model.educationShownCount > 0) EducationUiType.Notification else EducationUiType.Toast } diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt index 562ba369f47a..cd0b3f9b6693 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt @@ -58,7 +58,7 @@ class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, ha // Internal notification frontend dependencies NotificationAvalancheSuppression.token dependsOn VisualInterruptionRefactor.token PriorityPeopleSection.token dependsOn SortBySectionTimeFlag.token - NotificationMinimalismPrototype.token dependsOn NotificationsHeadsUpRefactor.token + NotificationMinimalismPrototype.token dependsOn NotificationThrottleHun.token NotificationsHeadsUpRefactor.token dependsOn NotificationThrottleHun.token // SceneContainer dependencies diff --git a/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt b/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt index 4652b2a30eb4..e50c05c7f6ed 100644 --- a/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt +++ b/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt @@ -107,7 +107,11 @@ constructor( fun handleActionDown() { logEvent(qsTile?.tileSpec, state, "action down received") when (state) { - State.IDLE -> { + State.IDLE, + // ACTION_DOWN typically only happens in State.IDLE but including CLICKED and + // LONG_CLICKED just to be safe`b + State.CLICKED, + State.LONG_CLICKED -> { setState(State.TIMEOUT_WAIT) } State.RUNNING_BACKWARDS_FROM_UP, diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/PhysicalKeyboardCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/keyboard/PhysicalKeyboardCoreStartable.kt index 90867edd8236..da671e330302 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/PhysicalKeyboardCoreStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/PhysicalKeyboardCoreStartable.kt @@ -17,7 +17,6 @@ package com.android.systemui.keyboard -import android.hardware.input.InputSettings import com.android.systemui.CoreStartable import com.android.systemui.Flags import com.android.systemui.dagger.SysUISingleton @@ -40,12 +39,10 @@ constructor( private val featureFlags: FeatureFlags, ) : CoreStartable { override fun start() { + stickyKeysIndicatorCoordinator.get().startListening() if (featureFlags.isEnabled(LegacyFlag.KEYBOARD_BACKLIGHT_INDICATOR)) { keyboardBacklightDialogCoordinator.get().startListening() } - if (InputSettings.isAccessibilityStickyKeysFeatureEnabled()) { - stickyKeysIndicatorCoordinator.get().startListening() - } if (Flags.keyboardDockingIndicator()) { keyboardDockingIndicationViewBinder.get().startListening() } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java index 9f3311373709..871d04693452 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java @@ -645,6 +645,9 @@ public class KeyguardService extends Service { public void showDismissibleKeyguard() { trace("showDismissibleKeyguard"); checkPermission(); + if (mFoldGracePeriodProvider.get().isEnabled()) { + mKeyguardInteractor.showDismissibleKeyguard(); + } mKeyguardViewMediator.showDismissibleKeyguard(); if (SceneContainerFlag.isEnabled() && mFoldGracePeriodProvider.get().isEnabled()) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt index edf17c1e9e80..81b0064f0f03 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt @@ -232,6 +232,9 @@ interface KeyguardRepository { /** Receive an event for doze time tick */ val dozeTimeTick: Flow<Long> + /** Receive an event lockscreen being shown in a dismissible state */ + val showDismissibleKeyguard: MutableStateFlow<Long> + /** Observable for DismissAction */ val dismissAction: StateFlow<DismissAction> @@ -305,6 +308,8 @@ interface KeyguardRepository { fun dozeTimeTick() + fun showDismissibleKeyguard() + fun setDismissAction(dismissAction: DismissAction) suspend fun setKeyguardDone(keyguardDoneType: KeyguardDone) @@ -439,6 +444,12 @@ constructor( _dozeTimeTick.value = systemClock.uptimeMillis() } + override val showDismissibleKeyguard = MutableStateFlow<Long>(0L) + + override fun showDismissibleKeyguard() { + showDismissibleKeyguard.value = systemClock.uptimeMillis() + } + private val _lastDozeTapToWakePosition = MutableStateFlow<Point?>(null) override val lastDozeTapToWakePosition = _lastDozeTapToWakePosition.asStateFlow() diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt index 3775d191949e..17c1e823a1ca 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt @@ -133,7 +133,12 @@ constructor( transitionInteractor.startedKeyguardState.replayCache.last() == KeyguardState.DREAMING ) { - startTransitionTo(KeyguardState.LOCKSCREEN) + if (powerInteractor.detailedWakefulness.value.isAwake()) { + startTransitionTo( + KeyguardState.LOCKSCREEN, + ownerReason = "Dream has ended and device is awake" + ) + } } } } @@ -144,7 +149,7 @@ constructor( scope.launch { combine( keyguardInteractor.isKeyguardOccluded, - keyguardInteractor.isDreaming + keyguardInteractor.isAbleToDream // Debounce the dreaming signal since there is a race condition between // the occluded and dreaming signals. We therefore add a small delay // to give enough time for occluded to flip to false when the dream @@ -172,7 +177,7 @@ constructor( } scope.launch { - keyguardInteractor.isDreaming + keyguardInteractor.isAbleToDream .filter { !it } .sample(deviceEntryInteractor.isUnlocked, ::Pair) .collect { (_, dismissable) -> diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt index 8f4110c7cc57..db5a63bbf446 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt @@ -74,6 +74,7 @@ constructor( listenForGoneToAodOrDozing() listenForGoneToDreaming() listenForGoneToLockscreenOrHub() + listenForGoneToOccluded() listenForGoneToDreamingLockscreenHosted() } @@ -81,6 +82,27 @@ constructor( scope.launch("$TAG#showKeyguard") { startTransitionTo(KeyguardState.LOCKSCREEN) } } + /** + * A special case supported on foldables, where folding the device may put the device on an + * unlocked lockscreen, but if an occluding app is already showing (like a active phone call), + * then go directly to OCCLUDED. + */ + private fun listenForGoneToOccluded() { + scope.launch("$TAG#listenForGoneToOccluded") { + keyguardInteractor.showDismissibleKeyguard + .filterRelevantKeyguardState() + .sample(keyguardInteractor.isKeyguardOccluded, ::Pair) + .collect { (_, isKeyguardOccluded) -> + if (isKeyguardOccluded) { + startTransitionTo( + KeyguardState.OCCLUDED, + ownerReason = "Dismissible keyguard with occlusion" + ) + } + } + } + } + // Primarily for when the user chooses to lock down the device private fun listenForGoneToLockscreenOrHub() { if (KeyguardWmStateRefactor.isEnabled) { @@ -166,11 +188,12 @@ constructor( interpolator = Interpolators.LINEAR duration = when (toState) { - KeyguardState.DREAMING -> TO_DREAMING_DURATION KeyguardState.AOD -> TO_AOD_DURATION KeyguardState.DOZING -> TO_DOZING_DURATION + KeyguardState.DREAMING -> TO_DREAMING_DURATION KeyguardState.LOCKSCREEN -> TO_LOCKSCREEN_DURATION KeyguardState.GLANCEABLE_HUB -> TO_GLANCEABLE_HUB_DURATION + KeyguardState.OCCLUDED -> TO_OCCLUDED_DURATION else -> DEFAULT_DURATION }.inWholeMilliseconds } @@ -179,10 +202,11 @@ constructor( companion object { private const val TAG = "FromGoneTransitionInteractor" private val DEFAULT_DURATION = 500.milliseconds - val TO_DREAMING_DURATION = 933.milliseconds val TO_AOD_DURATION = 1300.milliseconds val TO_DOZING_DURATION = 933.milliseconds + val TO_DREAMING_DURATION = 933.milliseconds val TO_LOCKSCREEN_DURATION = DEFAULT_DURATION val TO_GLANCEABLE_HUB_DURATION = DEFAULT_DURATION + val TO_OCCLUDED_DURATION = 100.milliseconds } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt index 42490c4176c6..0df989e9353f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt @@ -1,20 +1,18 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2022 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 + * 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. + * 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. */ - @file:OptIn(ExperimentalCoroutinesApi::class) package com.android.systemui.keyguard.domain.interactor @@ -156,14 +154,23 @@ constructor( val isPulsing: Flow<Boolean> = dozeTransitionModel.map { it.to == DozeStateModel.DOZE_PULSING } + /** Whether the system is dreaming with an overlay active */ + val isDreamingWithOverlay: Flow<Boolean> = repository.isDreamingWithOverlay + /** * Whether the system is dreaming. [isDreaming] will be always be true when [isDozing] is true, - * but not vice-versa. + * but not vice-versa. Also accounts for [isDreamingWithOverlay] */ - val isDreaming: StateFlow<Boolean> = repository.isDreaming - - /** Whether the system is dreaming with an overlay active */ - val isDreamingWithOverlay: Flow<Boolean> = repository.isDreamingWithOverlay + val isDreaming: StateFlow<Boolean> = + merge( + repository.isDreaming, + repository.isDreamingWithOverlay, + ) + .stateIn( + scope = applicationScope, + started = SharingStarted.Eagerly, + initialValue = false, + ) /** Whether the system is dreaming and the active dream is hosted in lockscreen */ val isActiveDreamLockscreenHosted: StateFlow<Boolean> = repository.isActiveDreamLockscreenHosted @@ -172,6 +179,9 @@ constructor( val onCameraLaunchDetected: Flow<CameraLaunchSourceModel> = repository.onCameraLaunchDetected.filter { it.type != CameraLaunchType.IGNORE } + /** Event for when an unlocked keyguard has been requested, such as on device fold */ + val showDismissibleKeyguard: Flow<Long> = repository.showDismissibleKeyguard.asStateFlow() + /** * Dozing and dreaming have overlapping events. If the doze state remains in FINISH, it means * that doze mode is not running and DREAMING is ok to commence. @@ -179,12 +189,25 @@ constructor( * Allow a brief moment to prevent rapidly oscillating between true/false signals. */ val isAbleToDream: Flow<Boolean> = - merge(isDreaming, isDreamingWithOverlay) - .combine(dozeTransitionModel) { isDreaming, dozeTransitionModel -> - isDreaming && isDozeOff(dozeTransitionModel.to) + dozeTransitionModel + .flatMapLatest { dozeTransitionModel -> + if (isDozeOff(dozeTransitionModel.to)) { + // When dozing stops, it is a very early signal that the device is exiting the + // dream state. DreamManagerService eventually notifies window manager, which + // invokes SystemUI through KeyguardService. Because of this substantial delay, + // do not immediately process any dreaming information when exiting AOD. It + // should actually be quite strange to leave AOD and then go straight to + // DREAMING so this should be fine. + delay(500L) + isDreaming + .sample(powerInteractor.isAwake) { isDreaming, isAwake -> + isDreaming && isAwake + } + .debounce(50L) + } else { + flowOf(false) + } } - .sample(powerInteractor.isAwake) { isAbleToDream, isAwake -> isAbleToDream && isAwake } - .debounce(50L) .stateIn( scope = applicationScope, started = SharingStarted.WhileSubscribed(), @@ -469,6 +492,10 @@ constructor( CameraLaunchSourceModel(type = cameraLaunchSourceIntToType(source)) } + fun showDismissibleKeyguard() { + repository.showDismissibleKeyguard() + } + companion object { private const val TAG = "KeyguardInteractor" } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt index e132eb7ec32a..b89eb2723fab 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt @@ -98,6 +98,16 @@ constructor( } scope.launch { + keyguardInteractor.isDreaming.collect { logger.log(TAG, VERBOSE, "isDreaming", it) } + } + + scope.launch { + keyguardInteractor.isDreamingWithOverlay.collect { + logger.log(TAG, VERBOSE, "isDreamingWithOverlay", it) + } + } + + scope.launch { keyguardInteractor.isAbleToDream.collect { logger.log(TAG, VERBOSE, "isAbleToDream", it) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt index 162a0d233efd..15e6b1d78ea0 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt @@ -30,18 +30,23 @@ import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle +import com.android.app.tracing.coroutines.launch import com.android.keyguard.logging.KeyguardQuickAffordancesLogger import com.android.settingslib.Utils import com.android.systemui.animation.Expandable import com.android.systemui.animation.view.LaunchableImageView import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.ui.binder.IconViewBinder +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewModel import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.plugins.FalsingManager import com.android.systemui.res.R import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.util.doOnEnd +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine @@ -49,11 +54,20 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch /** This is only for a SINGLE Quick affordance */ -object KeyguardQuickAffordanceViewBinder { +@SysUISingleton +class KeyguardQuickAffordanceViewBinder +@Inject +constructor( + private val falsingManager: FalsingManager?, + private val vibratorHelper: VibratorHelper?, + private val logger: KeyguardQuickAffordancesLogger, + @Main private val mainImmediateDispatcher: CoroutineDispatcher, +) { - private const val EXIT_DOZE_BUTTON_REVEAL_ANIMATION_DURATION_MS = 250L - private const val SCALE_SELECTED_BUTTON = 1.23f - private const val DIM_ALPHA = 0.3f + private val EXIT_DOZE_BUTTON_REVEAL_ANIMATION_DURATION_MS = 250L + private val SCALE_SELECTED_BUTTON = 1.23f + private val DIM_ALPHA = 0.3f + private val TAG = "KeyguardQuickAffordanceViewBinder" /** * Defines interface for an object that acts as the binding between the view and its view-model. @@ -73,30 +87,24 @@ object KeyguardQuickAffordanceViewBinder { view: LaunchableImageView, viewModel: Flow<KeyguardQuickAffordanceViewModel>, alpha: Flow<Float>, - falsingManager: FalsingManager?, - vibratorHelper: VibratorHelper?, - logger: KeyguardQuickAffordancesLogger, messageDisplayer: (Int) -> Unit, ): Binding { val button = view as ImageView val configurationBasedDimensions = MutableStateFlow(loadFromResources(view)) val disposableHandle = - view.repeatWhenAttached { + view.repeatWhenAttached(mainImmediateDispatcher) { repeatOnLifecycle(Lifecycle.State.STARTED) { - launch { + launch("$TAG#viewModel") { viewModel.collect { buttonModel -> updateButton( view = button, viewModel = buttonModel, - falsingManager = falsingManager, messageDisplayer = messageDisplayer, - vibratorHelper = vibratorHelper, - logger = logger, ) } } - launch { + launch("$TAG#updateButtonAlpha") { updateButtonAlpha( view = button, viewModel = viewModel, @@ -104,7 +112,7 @@ object KeyguardQuickAffordanceViewBinder { ) } - launch { + launch("$TAG#configurationBasedDimensions") { configurationBasedDimensions.collect { dimensions -> button.updateLayoutParams<ViewGroup.LayoutParams> { width = dimensions.buttonSizePx.width @@ -131,10 +139,7 @@ object KeyguardQuickAffordanceViewBinder { private fun updateButton( view: ImageView, viewModel: KeyguardQuickAffordanceViewModel, - falsingManager: FalsingManager?, messageDisplayer: (Int) -> Unit, - vibratorHelper: VibratorHelper?, - logger: KeyguardQuickAffordancesLogger, ) { if (!viewModel.isVisible) { view.isInvisible = true diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt index 6faca1e28b39..6031ef60e1be 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt @@ -50,7 +50,6 @@ import androidx.core.view.isInvisible import com.android.internal.policy.SystemBarUtils import com.android.keyguard.ClockEventController import com.android.keyguard.KeyguardClockSwitch -import com.android.keyguard.logging.KeyguardQuickAffordancesLogger import com.android.systemui.animation.view.LaunchableImageView import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor import com.android.systemui.broadcast.BroadcastDispatcher @@ -79,7 +78,6 @@ import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel import com.android.systemui.keyguard.ui.viewmodel.OccludingAppDeviceEntryMessageViewModel import com.android.systemui.monet.ColorScheme import com.android.systemui.monet.Style -import com.android.systemui.plugins.FalsingManager import com.android.systemui.plugins.clocks.ClockController import com.android.systemui.res.R import com.android.systemui.shade.domain.interactor.ShadeInteractor @@ -89,7 +87,6 @@ import com.android.systemui.shared.clocks.shared.model.ClockPreviewConstants import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots import com.android.systemui.shared.quickaffordance.shared.model.KeyguardPreviewConstants import com.android.systemui.statusbar.KeyguardIndicationController -import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController import com.android.systemui.statusbar.phone.KeyguardBottomAreaView import com.android.systemui.statusbar.phone.ScreenOffAnimationController @@ -133,8 +130,6 @@ constructor( private val broadcastDispatcher: BroadcastDispatcher, private val lockscreenSmartspaceController: LockscreenSmartspaceController, private val udfpsOverlayInteractor: UdfpsOverlayInteractor, - private val falsingManager: FalsingManager, - private val vibratorHelper: VibratorHelper, private val indicationController: KeyguardIndicationController, private val keyguardRootViewModel: KeyguardRootViewModel, private val keyguardBlueprintViewModel: KeyguardBlueprintViewModel, @@ -148,7 +143,7 @@ constructor( private val defaultShortcutsSection: DefaultShortcutsSection, private val keyguardClockInteractor: KeyguardClockInteractor, private val keyguardClockViewModel: KeyguardClockViewModel, - private val quickAffordancesLogger: KeyguardQuickAffordancesLogger, + private val keyguardQuickAffordanceViewBinder: KeyguardQuickAffordanceViewBinder, ) { val hostToken: IBinder? = bundle.getBinder(KEY_HOST_TOKEN) private val width: Int = bundle.getInt(KEY_VIEW_WIDTH) @@ -458,13 +453,10 @@ constructor( keyguardRootView.findViewById<LaunchableImageView?>(R.id.start_button)?.let { imageView -> shortcutsBindings.add( - KeyguardQuickAffordanceViewBinder.bind( + keyguardQuickAffordanceViewBinder.bind( view = imageView, viewModel = quickAffordancesCombinedViewModel.startButton, alpha = flowOf(1f), - falsingManager = falsingManager, - vibratorHelper = vibratorHelper, - logger = quickAffordancesLogger, ) { message -> indicationController.showTransientIndication(message) } @@ -473,13 +465,10 @@ constructor( keyguardRootView.findViewById<LaunchableImageView?>(R.id.end_button)?.let { imageView -> shortcutsBindings.add( - KeyguardQuickAffordanceViewBinder.bind( + keyguardQuickAffordanceViewBinder.bind( view = imageView, viewModel = quickAffordancesCombinedViewModel.endButton, alpha = flowOf(1f), - falsingManager = falsingManager, - vibratorHelper = vibratorHelper, - logger = quickAffordancesLogger, ) { message -> indicationController.showTransientIndication(message) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/KeyguardBlueprintModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/KeyguardBlueprintModule.kt index 2dc930121a71..bf6f2c44521a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/KeyguardBlueprintModule.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/KeyguardBlueprintModule.kt @@ -42,12 +42,6 @@ abstract class KeyguardBlueprintModule { ): KeyguardBlueprint @Binds - @IntoSet - abstract fun bindShortcutsBesideUdfpsLockscreenBlueprint( - shortcutsBesideUdfpsLockscreenBlueprint: ShortcutsBesideUdfpsKeyguardBlueprint - ): KeyguardBlueprint - - @Binds @IntoMap @ClassKey(KeyguardBlueprintInteractor::class) abstract fun bindsKeyguardBlueprintInteractor( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/ShortcutsBesideUdfpsKeyguardBlueprint.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/ShortcutsBesideUdfpsKeyguardBlueprint.kt deleted file mode 100644 index b984a6808d1c..000000000000 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/ShortcutsBesideUdfpsKeyguardBlueprint.kt +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright (C) 2023 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.keyguard.ui.view.layout.blueprints - -import com.android.systemui.communal.ui.view.layout.sections.CommunalTutorialIndicatorSection -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.keyguard.shared.model.KeyguardBlueprint -import com.android.systemui.keyguard.shared.model.KeyguardSection -import com.android.systemui.keyguard.ui.view.layout.sections.AccessibilityActionsSection -import com.android.systemui.keyguard.ui.view.layout.sections.AlignShortcutsToUdfpsSection -import com.android.systemui.keyguard.ui.view.layout.sections.AodBurnInSection -import com.android.systemui.keyguard.ui.view.layout.sections.AodNotificationIconsSection -import com.android.systemui.keyguard.ui.view.layout.sections.ClockSection -import com.android.systemui.keyguard.ui.view.layout.sections.DefaultDeviceEntrySection -import com.android.systemui.keyguard.ui.view.layout.sections.DefaultIndicationAreaSection -import com.android.systemui.keyguard.ui.view.layout.sections.DefaultNotificationStackScrollLayoutSection -import com.android.systemui.keyguard.ui.view.layout.sections.DefaultSettingsPopupMenuSection -import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusBarSection -import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusViewSection -import com.android.systemui.keyguard.ui.view.layout.sections.DefaultUdfpsAccessibilityOverlaySection -import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardSectionsModule -import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardSliceViewSection -import com.android.systemui.keyguard.ui.view.layout.sections.SmartspaceSection -import com.android.systemui.util.kotlin.getOrNull -import java.util.Optional -import javax.inject.Inject -import javax.inject.Named -import kotlinx.coroutines.ExperimentalCoroutinesApi - -/** Vertically aligns the shortcuts with the udfps. */ -@ExperimentalCoroutinesApi -@SysUISingleton -class ShortcutsBesideUdfpsKeyguardBlueprint -@Inject -constructor( - accessibilityActionsSection: AccessibilityActionsSection, - alignShortcutsToUdfpsSection: AlignShortcutsToUdfpsSection, - defaultIndicationAreaSection: DefaultIndicationAreaSection, - defaultDeviceEntrySection: DefaultDeviceEntrySection, - @Named(KeyguardSectionsModule.KEYGUARD_AMBIENT_INDICATION_AREA_SECTION) - defaultAmbientIndicationAreaSection: Optional<KeyguardSection>, - defaultSettingsPopupMenuSection: DefaultSettingsPopupMenuSection, - defaultStatusViewSection: DefaultStatusViewSection, - defaultStatusBarSection: DefaultStatusBarSection, - defaultNotificationStackScrollLayoutSection: DefaultNotificationStackScrollLayoutSection, - aodNotificationIconsSection: AodNotificationIconsSection, - aodBurnInSection: AodBurnInSection, - communalTutorialIndicatorSection: CommunalTutorialIndicatorSection, - clockSection: ClockSection, - smartspaceSection: SmartspaceSection, - keyguardSliceViewSection: KeyguardSliceViewSection, - udfpsAccessibilityOverlaySection: DefaultUdfpsAccessibilityOverlaySection, -) : KeyguardBlueprint { - override val id: String = SHORTCUTS_BESIDE_UDFPS - - override val sections = - listOfNotNull( - accessibilityActionsSection, - defaultIndicationAreaSection, - alignShortcutsToUdfpsSection, - defaultAmbientIndicationAreaSection.getOrNull(), - defaultSettingsPopupMenuSection, - defaultStatusViewSection, - defaultStatusBarSection, - defaultNotificationStackScrollLayoutSection, - aodNotificationIconsSection, - smartspaceSection, - aodBurnInSection, - communalTutorialIndicatorSection, - clockSection, - keyguardSliceViewSection, - defaultDeviceEntrySection, - udfpsAccessibilityOverlaySection, // Add LAST: Intentionally has z-order above others - ) - - companion object { - const val SHORTCUTS_BESIDE_UDFPS = "shortcuts-besides-udfps" - } -} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AlignShortcutsToUdfpsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AlignShortcutsToUdfpsSection.kt deleted file mode 100644 index 1ba830bdb1ea..000000000000 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AlignShortcutsToUdfpsSection.kt +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright (C) 2023 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.keyguard.ui.view.layout.sections - -import android.content.res.Resources -import androidx.constraintlayout.widget.ConstraintLayout -import androidx.constraintlayout.widget.ConstraintSet -import androidx.constraintlayout.widget.ConstraintSet.BOTTOM -import androidx.constraintlayout.widget.ConstraintSet.LEFT -import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID -import androidx.constraintlayout.widget.ConstraintSet.RIGHT -import androidx.constraintlayout.widget.ConstraintSet.TOP -import com.android.keyguard.logging.KeyguardQuickAffordancesLogger -import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor -import com.android.systemui.keyguard.KeyguardBottomAreaRefactor -import com.android.systemui.keyguard.ui.binder.KeyguardQuickAffordanceViewBinder -import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordancesCombinedViewModel -import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel -import com.android.systemui.plugins.FalsingManager -import com.android.systemui.res.R -import com.android.systemui.statusbar.KeyguardIndicationController -import com.android.systemui.statusbar.VibratorHelper -import javax.inject.Inject - -class AlignShortcutsToUdfpsSection -@Inject -constructor( - @Main private val resources: Resources, - private val keyguardQuickAffordancesCombinedViewModel: - KeyguardQuickAffordancesCombinedViewModel, - private val keyguardRootViewModel: KeyguardRootViewModel, - private val falsingManager: FalsingManager, - private val indicationController: KeyguardIndicationController, - private val vibratorHelper: VibratorHelper, - private val shortcutsLogger: KeyguardQuickAffordancesLogger, -) : BaseShortcutSection() { - override fun addViews(constraintLayout: ConstraintLayout) { - if (KeyguardBottomAreaRefactor.isEnabled) { - addLeftShortcut(constraintLayout) - addRightShortcut(constraintLayout) - } - } - - override fun bindData(constraintLayout: ConstraintLayout) { - if (KeyguardBottomAreaRefactor.isEnabled) { - leftShortcutHandle = - KeyguardQuickAffordanceViewBinder.bind( - constraintLayout.requireViewById(R.id.start_button), - keyguardQuickAffordancesCombinedViewModel.startButton, - keyguardQuickAffordancesCombinedViewModel.transitionAlpha, - falsingManager, - vibratorHelper, - shortcutsLogger, - ) { - indicationController.showTransientIndication(it) - } - rightShortcutHandle = - KeyguardQuickAffordanceViewBinder.bind( - constraintLayout.requireViewById(R.id.end_button), - keyguardQuickAffordancesCombinedViewModel.endButton, - keyguardQuickAffordancesCombinedViewModel.transitionAlpha, - falsingManager, - vibratorHelper, - shortcutsLogger, - ) { - indicationController.showTransientIndication(it) - } - } - } - - override fun applyConstraints(constraintSet: ConstraintSet) { - val width = resources.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_width) - val height = resources.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_height) - - val lockIconViewId = - if (DeviceEntryUdfpsRefactor.isEnabled) { - R.id.device_entry_icon_view - } else { - R.id.lock_icon_view - } - - constraintSet.apply { - constrainWidth(R.id.start_button, width) - constrainHeight(R.id.start_button, height) - connect(R.id.start_button, LEFT, PARENT_ID, LEFT) - connect(R.id.start_button, RIGHT, lockIconViewId, LEFT) - connect(R.id.start_button, TOP, lockIconViewId, TOP) - connect(R.id.start_button, BOTTOM, lockIconViewId, BOTTOM) - - constrainWidth(R.id.end_button, width) - constrainHeight(R.id.end_button, height) - connect(R.id.end_button, RIGHT, PARENT_ID, RIGHT) - connect(R.id.end_button, LEFT, lockIconViewId, RIGHT) - connect(R.id.end_button, TOP, lockIconViewId, TOP) - connect(R.id.end_button, BOTTOM, lockIconViewId, BOTTOM) - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt index 64c46dbf05aa..e558033728ba 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt @@ -26,7 +26,6 @@ import androidx.constraintlayout.widget.ConstraintSet.LEFT import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID import androidx.constraintlayout.widget.ConstraintSet.RIGHT import androidx.constraintlayout.widget.ConstraintSet.VISIBILITY_MODE_IGNORE -import com.android.keyguard.logging.KeyguardQuickAffordancesLogger import com.android.systemui.animation.view.LaunchableImageView import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyguard.KeyguardBottomAreaRefactor @@ -35,10 +34,8 @@ import com.android.systemui.keyguard.ui.binder.KeyguardQuickAffordanceViewBinder import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordancesCombinedViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel -import com.android.systemui.plugins.FalsingManager import com.android.systemui.res.R import com.android.systemui.statusbar.KeyguardIndicationController -import com.android.systemui.statusbar.VibratorHelper import dagger.Lazy import javax.inject.Inject @@ -49,11 +46,9 @@ constructor( private val keyguardQuickAffordancesCombinedViewModel: KeyguardQuickAffordancesCombinedViewModel, private val keyguardRootViewModel: KeyguardRootViewModel, - private val falsingManager: FalsingManager, private val indicationController: KeyguardIndicationController, - private val vibratorHelper: VibratorHelper, private val keyguardBlueprintInteractor: Lazy<KeyguardBlueprintInteractor>, - private val shortcutsLogger: KeyguardQuickAffordancesLogger, + private val keyguardQuickAffordanceViewBinder: KeyguardQuickAffordanceViewBinder, ) : BaseShortcutSection() { // Amount to increase the bottom margin by to avoid colliding with inset @@ -82,24 +77,18 @@ constructor( override fun bindData(constraintLayout: ConstraintLayout) { if (KeyguardBottomAreaRefactor.isEnabled) { leftShortcutHandle = - KeyguardQuickAffordanceViewBinder.bind( + keyguardQuickAffordanceViewBinder.bind( constraintLayout.requireViewById(R.id.start_button), keyguardQuickAffordancesCombinedViewModel.startButton, keyguardQuickAffordancesCombinedViewModel.transitionAlpha, - falsingManager, - vibratorHelper, - shortcutsLogger, ) { indicationController.showTransientIndication(it) } rightShortcutHandle = - KeyguardQuickAffordanceViewBinder.bind( + keyguardQuickAffordanceViewBinder.bind( constraintLayout.requireViewById(R.id.end_button), keyguardQuickAffordancesCombinedViewModel.endButton, keyguardQuickAffordancesCombinedViewModel.transitionAlpha, - falsingManager, - vibratorHelper, - shortcutsLogger, ) { indicationController.showTransientIndication(it) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModel.kt index c590f07d9b50..992550cdca5a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModel.kt @@ -16,6 +16,7 @@ package com.android.systemui.keyguard.ui.viewmodel +import android.util.MathUtils import com.android.systemui.dagger.SysUISingleton import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor import com.android.systemui.keyguard.domain.interactor.FromAlternateBouncerTransitionInteractor @@ -47,6 +48,15 @@ constructor( edge = Edge.create(from = ALTERNATE_BOUNCER, to = AOD), ) + fun lockscreenAlpha(viewState: ViewStateAccessor): Flow<Float> { + var startAlpha = 1f + return transitionAnimation.sharedFlow( + duration = FromAlternateBouncerTransitionInteractor.TO_AOD_DURATION, + onStart = { startAlpha = viewState.alpha() }, + onStep = { MathUtils.lerp(startAlpha, 1f, it) }, + ) + } + val deviceEntryBackgroundViewAlpha: Flow<Float> = transitionAnimation.sharedFlow( duration = FromAlternateBouncerTransitionInteractor.TO_AOD_DURATION, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModel.kt index 5a559fc3aa45..470f17b74032 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModel.kt @@ -42,9 +42,6 @@ constructor( val transitionToAlternateBouncerProgress = keyguardTransitionInteractor.transitionValue(ALTERNATE_BOUNCER) - val forcePluginOpen: Flow<Boolean> = - transitionToAlternateBouncerProgress.map { it > 0f }.distinctUntilChanged() - /** An observable for the scrim alpha. */ val scrimAlpha = transitionToAlternateBouncerProgress.map { it * alternateBouncerScrimAlpha } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModel.kt index 680f966708be..2426f9745885 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModel.kt @@ -19,6 +19,7 @@ package com.android.systemui.keyguard.ui.viewmodel import androidx.annotation.VisibleForTesting import com.android.app.tracing.FlowTracing.traceEmissionCount +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor @@ -29,19 +30,23 @@ import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAfforda import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.stateIn @OptIn(ExperimentalCoroutinesApi::class) class KeyguardQuickAffordancesCombinedViewModel @Inject constructor( + @Application private val applicationScope: CoroutineScope, private val quickAffordanceInteractor: KeyguardQuickAffordanceInteractor, private val keyguardInteractor: KeyguardInteractor, shadeInteractor: ShadeInteractor, @@ -133,9 +138,14 @@ constructor( /** The source of truth of alpha for all of the quick affordances on lockscreen */ val transitionAlpha: Flow<Float> = merge( - fadeInAlpha, - fadeOutAlpha, - ) + fadeInAlpha, + fadeOutAlpha, + ) + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = 0f, + ) /** * Whether quick affordances are "opaque enough" to be considered visible to and interactive by @@ -199,38 +209,42 @@ constructor( private fun button( position: KeyguardQuickAffordancePosition ): Flow<KeyguardQuickAffordanceViewModel> { - return previewMode.flatMapLatest { previewMode -> - combine( - if (previewMode.isInPreviewMode) { - quickAffordanceInteractor.quickAffordanceAlwaysVisible(position = position) - } else { - quickAffordanceInteractor.quickAffordance(position = position) - }, - keyguardInteractor.animateDozingTransitions.distinctUntilChanged(), - areQuickAffordancesFullyOpaque, - selectedPreviewSlotId, - quickAffordanceInteractor.useLongPress(), - ) { model, animateReveal, isFullyOpaque, selectedPreviewSlotId, useLongPress -> - val slotId = position.toSlotId() - val isSelected = selectedPreviewSlotId == slotId - model.toViewModel( - animateReveal = !previewMode.isInPreviewMode && animateReveal, - isClickable = isFullyOpaque && !previewMode.isInPreviewMode, - isSelected = - previewMode.isInPreviewMode && - previewMode.shouldHighlightSelectedAffordance && - isSelected, - isDimmed = - previewMode.isInPreviewMode && - previewMode.shouldHighlightSelectedAffordance && - !isSelected, - forceInactive = previewMode.isInPreviewMode, - slotId = slotId, - useLongPress = useLongPress, - ) - } - .distinctUntilChanged() - }.traceEmissionCount({"QuickAfforcances#button${position.toSlotId()}"}) + return previewMode + .flatMapLatest { previewMode -> + combine( + if (previewMode.isInPreviewMode) { + quickAffordanceInteractor.quickAffordanceAlwaysVisible( + position = position + ) + } else { + quickAffordanceInteractor.quickAffordance(position = position) + }, + keyguardInteractor.animateDozingTransitions.distinctUntilChanged(), + areQuickAffordancesFullyOpaque, + selectedPreviewSlotId, + quickAffordanceInteractor.useLongPress(), + ) { model, animateReveal, isFullyOpaque, selectedPreviewSlotId, useLongPress -> + val slotId = position.toSlotId() + val isSelected = selectedPreviewSlotId == slotId + model.toViewModel( + animateReveal = !previewMode.isInPreviewMode && animateReveal, + isClickable = isFullyOpaque && !previewMode.isInPreviewMode, + isSelected = + previewMode.isInPreviewMode && + previewMode.shouldHighlightSelectedAffordance && + isSelected, + isDimmed = + previewMode.isInPreviewMode && + previewMode.shouldHighlightSelectedAffordance && + !isSelected, + forceInactive = previewMode.isInPreviewMode, + slotId = slotId, + useLongPress = useLongPress, + ) + } + .distinctUntilChanged() + } + .traceEmissionCount({ "QuickAfforcances#button${position.toSlotId()}" }) } private fun KeyguardQuickAffordanceModel.toViewModel( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt index 38a2b1bad3bf..050ef6f94f0a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt @@ -83,6 +83,7 @@ constructor( private val communalInteractor: CommunalInteractor, private val keyguardTransitionInteractor: KeyguardTransitionInteractor, private val notificationsKeyguardInteractor: NotificationsKeyguardInteractor, + private val alternateBouncerToAodTransitionViewModel: AlternateBouncerToAodTransitionViewModel, private val alternateBouncerToGoneTransitionViewModel: AlternateBouncerToGoneTransitionViewModel, private val alternateBouncerToLockscreenTransitionViewModel: @@ -239,6 +240,7 @@ constructor( merge( alphaOnShadeExpansion, keyguardInteractor.dismissAlpha, + alternateBouncerToAodTransitionViewModel.lockscreenAlpha(viewState), alternateBouncerToGoneTransitionViewModel.lockscreenAlpha(viewState), alternateBouncerToLockscreenTransitionViewModel.lockscreenAlpha(viewState), aodToGoneTransitionViewModel.lockscreenAlpha(viewState), diff --git a/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt b/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt index c2b5d98699b4..661da6d2af13 100644 --- a/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt +++ b/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt @@ -227,33 +227,13 @@ private fun inferTraceSectionName(): String { } /** - * Runs the given [block] in a new coroutine when `this` [View]'s Window's [WindowLifecycleState] is - * at least at [state] (or immediately after calling this function if the window is already at least - * at [state]), automatically canceling the work when the window is no longer at least at that - * state. - * - * [block] may be run multiple times, running once per every time this` [View]'s Window's - * [WindowLifecycleState] becomes at least at [state]. - */ -suspend fun View.repeatOnWindowLifecycle( - state: WindowLifecycleState, - block: suspend CoroutineScope.() -> Unit, -): Nothing { - when (state) { - WindowLifecycleState.ATTACHED -> repeatWhenAttachedToWindow(block) - WindowLifecycleState.VISIBLE -> repeatWhenWindowIsVisible(block) - WindowLifecycleState.FOCUSED -> repeatWhenWindowHasFocus(block) - } -} - -/** * Runs the given [block] every time the [View] becomes attached (or immediately after calling this * function, if the view was already attached), automatically canceling the work when the view * becomes detached. * * Only use from the main thread. * - * [block] may be run multiple times, running once per every time the view is attached. + * The [block] may be run multiple times, running once per every time the view is attached. */ @MainThread suspend fun View.repeatWhenAttachedToWindow(block: suspend CoroutineScope.() -> Unit): Nothing { @@ -269,7 +249,7 @@ suspend fun View.repeatWhenAttachedToWindow(block: suspend CoroutineScope.() -> * * Only use from the main thread. * - * [block] may be run multiple times, running once per every time the window becomes visible. + * The [block] may be run multiple times, running once per every time the window becomes visible. */ @MainThread suspend fun View.repeatWhenWindowIsVisible(block: suspend CoroutineScope.() -> Unit): Nothing { @@ -285,7 +265,7 @@ suspend fun View.repeatWhenWindowIsVisible(block: suspend CoroutineScope.() -> U * * Only use from the main thread. * - * [block] may be run multiple times, running once per every time the window is focused. + * The [block] may be run multiple times, running once per every time the window is focused. */ @MainThread suspend fun View.repeatWhenWindowHasFocus(block: suspend CoroutineScope.() -> Unit): Nothing { @@ -294,21 +274,6 @@ suspend fun View.repeatWhenWindowHasFocus(block: suspend CoroutineScope.() -> Un awaitCancellation() // satisfies return type of Nothing } -/** Lifecycle states for a [View]'s interaction with a [android.view.Window]. */ -enum class WindowLifecycleState { - /** Indicates that the [View] is attached to a [android.view.Window]. */ - ATTACHED, - /** - * Indicates that the [View] is attached to a [android.view.Window], and the window is visible. - */ - VISIBLE, - /** - * Indicates that the [View] is attached to a [android.view.Window], and the window is visible - * and focused. - */ - FOCUSED -} - private val View.isAttached get() = conflatedCallbackFlow { val onAttachListener = diff --git a/packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt b/packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt index 77314813c34a..0af5feaff3b2 100644 --- a/packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt @@ -16,10 +16,9 @@ package com.android.systemui.lifecycle -import android.view.View import androidx.compose.runtime.Composable -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember /** Base class for all System UI view-models. */ abstract class SysUiViewModel : SafeActivatable() { @@ -38,20 +37,8 @@ abstract class SysUiViewModel : SafeActivatable() { fun <T : SysUiViewModel> rememberViewModel( key: Any = Unit, factory: () -> T, -): T = rememberActivated(key, factory) - -/** - * Invokes [block] in a new coroutine with a new [SysUiViewModel] that is automatically activated - * whenever `this` [View]'s Window's [WindowLifecycleState] is at least at - * [minWindowLifecycleState], and is automatically canceled once that is no longer the case. - */ -suspend fun <T : SysUiViewModel> View.viewModel( - minWindowLifecycleState: WindowLifecycleState, - factory: () -> T, - block: suspend CoroutineScope.(T) -> Unit, -): Nothing = - repeatOnWindowLifecycle(minWindowLifecycleState) { - val instance = factory() - launch { instance.activate() } - block(instance) - } +): T { + val instance = remember(key) { factory() } + LaunchedEffect(instance) { instance.activate() } + return instance +} diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/ShareToAppPermissionDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/ShareToAppPermissionDialogDelegate.kt index 8bf220277c12..4b132d0db3fc 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/ShareToAppPermissionDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/ShareToAppPermissionDialogDelegate.kt @@ -48,6 +48,7 @@ class ShareToAppPermissionDialogDelegate( appName, hostUid, mediaProjectionMetricsLogger, + dialogIconDrawable = R.drawable.ic_present_to_all, ) { override fun onCreate(dialog: AlertDialog, savedInstanceState: Bundle?) { super.onCreate(dialog, savedInstanceState) @@ -83,22 +84,28 @@ class ShareToAppPermissionDialogDelegate( listOf( ScreenShareOption( mode = SINGLE_APP, - spinnerText = R.string.screen_share_permission_dialog_option_single_app, + spinnerText = + R.string + .media_projection_entry_app_permission_dialog_option_text_single_app, warningText = R.string .media_projection_entry_app_permission_dialog_warning_single_app, startButtonText = - R.string.media_projection_entry_app_permission_dialog_continue, + R.string + .media_projection_entry_generic_permission_dialog_continue_single_app, spinnerDisabledText = singleAppDisabledText, ), ScreenShareOption( mode = ENTIRE_SCREEN, - spinnerText = R.string.screen_share_permission_dialog_option_entire_screen, + spinnerText = + R.string + .media_projection_entry_app_permission_dialog_option_text_entire_screen, warningText = R.string .media_projection_entry_app_permission_dialog_warning_entire_screen, startButtonText = - R.string.media_projection_entry_app_permission_dialog_continue, + R.string + .media_projection_entry_app_permission_dialog_continue_entire_screen, ) ) return if (singleAppDisabledText != null) { diff --git a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneActionsViewModel.kt index fa8e13ab2b72..2d2b869a49ea 100644 --- a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneActionsViewModel.kt @@ -20,23 +20,27 @@ import com.android.compose.animation.scene.Back import com.android.compose.animation.scene.Swipe import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult -import com.android.systemui.dagger.SysUISingleton import com.android.systemui.scene.shared.model.SceneFamilies +import com.android.systemui.scene.ui.viewmodel.SceneActionsViewModel import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.shared.model.ShadeAlignment -import javax.inject.Inject -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOf +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject -/** Models UI state and handles user input for the Notifications Shade scene. */ -@SysUISingleton -class NotificationsShadeSceneViewModel -@Inject +/** + * Models the UI state for the user actions that the user can perform to navigate to other scenes. + * + * Different from the [NotificationsShadeSceneContentViewModel] which models the _content_ of the + * scene. + */ +class NotificationsShadeSceneActionsViewModel +@AssistedInject constructor( - shadeInteractor: ShadeInteractor, -) { - val destinationScenes: Flow<Map<UserAction, UserActionResult>> = - flowOf( + private val shadeInteractor: ShadeInteractor, +) : SceneActionsViewModel() { + + override suspend fun hydrateActions(setActions: (Map<UserAction, UserActionResult>) -> Unit) { + setActions( mapOf( if (shadeInteractor.shadeAlignment == ShadeAlignment.Top) { Swipe.Up @@ -46,4 +50,10 @@ constructor( Back to SceneFamilies.Home, ) ) + } + + @AssistedFactory + interface Factory { + fun create(): NotificationsShadeSceneActionsViewModel + } } diff --git a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneContentViewModel.kt new file mode 100644 index 000000000000..c1c7320c42db --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneContentViewModel.kt @@ -0,0 +1,49 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.android.systemui.notifications.ui.viewmodel + +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor +import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.shade.ui.viewmodel.BaseShadeSceneViewModel +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.ExperimentalCoroutinesApi + +/** + * Models UI state used to render the content of the notifications shade scene. + * + * Different from [NotificationsShadeSceneActionsViewModel], which only models user actions that can + * be performed to navigate to other scenes. + */ +class NotificationsShadeSceneContentViewModel +@AssistedInject +constructor( + deviceEntryInteractor: DeviceEntryInteractor, + sceneInteractor: SceneInteractor, +) : + BaseShadeSceneViewModel( + deviceEntryInteractor, + sceneInteractor, + ) { + + @AssistedFactory + interface Factory { + fun create(): NotificationsShadeSceneContentViewModel + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt index b1cc55d03b04..7258882e9ffa 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt @@ -34,7 +34,6 @@ import com.android.systemui.scene.shared.model.SceneFamilies import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.settings.brightness.ui.viewModel.BrightnessMirrorViewModel import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel -import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject import kotlinx.coroutines.flow.Flow @@ -48,10 +47,9 @@ import kotlinx.coroutines.flow.map class QuickSettingsSceneViewModel @Inject constructor( - val brightnessMirrorViewModel: BrightnessMirrorViewModel, - val shadeHeaderViewModel: ShadeHeaderViewModel, + val brightnessMirrorViewModelFactory: BrightnessMirrorViewModel.Factory, + val shadeHeaderViewModelFactory: ShadeHeaderViewModel.Factory, val qsSceneAdapter: QSSceneAdapter, - val notifications: NotificationsPlaceholderViewModel, private val footerActionsViewModelFactory: FooterActionsViewModel.Factory, private val footerActionsController: FooterActionsController, sceneBackInteractor: SceneBackInteractor, diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneActionsViewModel.kt new file mode 100644 index 000000000000..d2967b87b967 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneActionsViewModel.kt @@ -0,0 +1,72 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.android.systemui.qs.ui.viewmodel + +import com.android.compose.animation.scene.Back +import com.android.compose.animation.scene.Swipe +import com.android.compose.animation.scene.UserAction +import com.android.compose.animation.scene.UserActionResult +import com.android.systemui.scene.shared.model.SceneFamilies +import com.android.systemui.scene.ui.viewmodel.SceneActionsViewModel +import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.shade.shared.model.ShadeAlignment +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.map + +/** + * Models the UI state for the user actions that the user can perform to navigate to other scenes. + * + * Different from the [QuickSettingsShadeSceneContentViewModel] which models the _content_ of the + * scene. + */ +class QuickSettingsShadeSceneActionsViewModel +@AssistedInject +constructor( + private val shadeInteractor: ShadeInteractor, + val quickSettingsContainerViewModel: QuickSettingsContainerViewModel, +) : SceneActionsViewModel() { + + override suspend fun hydrateActions(setActions: (Map<UserAction, UserActionResult>) -> Unit) { + quickSettingsContainerViewModel.editModeViewModel.isEditing + .map { editing -> + buildMap { + put( + if (shadeInteractor.shadeAlignment == ShadeAlignment.Top) { + Swipe.Up + } else { + Swipe.Down + }, + UserActionResult(SceneFamilies.Home) + ) + if (!editing) { + put(Back, UserActionResult(SceneFamilies.Home)) + } + } + } + .collectLatest { actions -> setActions(actions) } + } + + @AssistedFactory + interface Factory { + fun create(): QuickSettingsShadeSceneActionsViewModel + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneContentViewModel.kt new file mode 100644 index 000000000000..abfca4b9aa4a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneContentViewModel.kt @@ -0,0 +1,44 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.android.systemui.qs.ui.viewmodel + +import com.android.systemui.lifecycle.SysUiViewModel +import com.android.systemui.shade.ui.viewmodel.OverlayShadeViewModel +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.ExperimentalCoroutinesApi + +/** + * Models UI state used to render the content of the quick settings shade scene. + * + * Different from [QuickSettingsShadeSceneActionsViewModel], which only models user actions that can + * be performed to navigate to other scenes. + */ +class QuickSettingsShadeSceneContentViewModel +@AssistedInject +constructor( + val overlayShadeViewModelFactory: OverlayShadeViewModel.Factory, + val quickSettingsContainerViewModel: QuickSettingsContainerViewModel, +) : SysUiViewModel() { + + @AssistedFactory + interface Factory { + fun create(): QuickSettingsShadeSceneContentViewModel + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModel.kt deleted file mode 100644 index e012f2cac1fb..000000000000 --- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModel.kt +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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.ui.viewmodel - -import com.android.compose.animation.scene.Back -import com.android.compose.animation.scene.Swipe -import com.android.compose.animation.scene.UserAction -import com.android.compose.animation.scene.UserActionResult -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.scene.shared.model.SceneFamilies -import com.android.systemui.shade.domain.interactor.ShadeInteractor -import com.android.systemui.shade.shared.model.ShadeAlignment -import com.android.systemui.shade.ui.viewmodel.OverlayShadeViewModel -import javax.inject.Inject -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map - -/** Models UI state and handles user input for the Quick Settings Shade scene. */ -@SysUISingleton -class QuickSettingsShadeSceneViewModel -@Inject -constructor( - private val shadeInteractor: ShadeInteractor, - val overlayShadeViewModel: OverlayShadeViewModel, - val quickSettingsContainerViewModel: QuickSettingsContainerViewModel, -) { - - val destinationScenes: Flow<Map<UserAction, UserActionResult>> = - quickSettingsContainerViewModel.editModeViewModel.isEditing.map { editing -> - buildMap { - put( - if (shadeInteractor.shadeAlignment == ShadeAlignment.Top) { - Swipe.Up - } else { - Swipe.Down - }, - UserActionResult(SceneFamilies.Home) - ) - if (!editing) { - put(Back, UserActionResult(SceneFamilies.Home)) - } - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/scene/OWNERS b/packages/SystemUI/src/com/android/systemui/scene/OWNERS new file mode 100644 index 000000000000..033ff1409b31 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/scene/OWNERS @@ -0,0 +1,2 @@ +justinweir@google.com +nijamkin@google.com diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java index 117035422c51..108564ca080e 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java @@ -63,7 +63,9 @@ public class RecordingService extends Service implements ScreenMediaRecorderList protected static final int NOTIF_BASE_ID = 4273; private static final String TAG = "RecordingService"; private static final String CHANNEL_ID = "screen_record"; - private static final String GROUP_KEY = "screen_record_saved"; + @VisibleForTesting static final String GROUP_KEY_SAVED = "screen_record_saved"; + private static final String GROUP_KEY_ERROR_STARTING = "screen_record_error_starting"; + @VisibleForTesting static final String GROUP_KEY_ERROR_SAVING = "screen_record_error_saving"; private static final String EXTRA_RESULT_CODE = "extra_resultCode"; protected static final String EXTRA_PATH = "extra_path"; private static final String EXTRA_AUDIO_SOURCE = "extra_useAudio"; @@ -181,7 +183,7 @@ public class RecordingService extends Service implements ScreenMediaRecorderList mUiEventLogger.log(Events.ScreenRecordEvent.SCREEN_RECORD_START); } else { updateState(false); - createErrorStartingNotification(); + createErrorStartingNotification(currentUser); stopForeground(STOP_FOREGROUND_DETACH); stopSelf(); return Service.START_NOT_STICKY; @@ -276,8 +278,8 @@ public class RecordingService extends Service implements ScreenMediaRecorderList * errors. */ @VisibleForTesting - protected void createErrorStartingNotification() { - createErrorNotification(strings().getStartError()); + protected void createErrorStartingNotification(UserHandle currentUser) { + createErrorNotification(currentUser, strings().getStartError(), GROUP_KEY_ERROR_STARTING); } /** @@ -285,17 +287,22 @@ public class RecordingService extends Service implements ScreenMediaRecorderList * errors. */ @VisibleForTesting - protected void createErrorSavingNotification() { - createErrorNotification(strings().getSaveError()); + protected void createErrorSavingNotification(UserHandle currentUser) { + createErrorNotification(currentUser, strings().getSaveError(), GROUP_KEY_ERROR_SAVING); } - private void createErrorNotification(String notificationContentTitle) { + private void createErrorNotification( + UserHandle currentUser, String notificationContentTitle, String groupKey) { + // Make sure error notifications get their own group. + postGroupSummaryNotification(currentUser, notificationContentTitle, groupKey); + Bundle extras = new Bundle(); extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, strings().getTitle()); Notification.Builder builder = new Notification.Builder(this, getChannelId()) .setSmallIcon(R.drawable.ic_screenrecord) .setContentTitle(notificationContentTitle) + .setGroup(groupKey) .addExtras(extras); startForeground(mNotificationId, builder.build()); } @@ -350,7 +357,7 @@ public class RecordingService extends Service implements ScreenMediaRecorderList .setContentText( strings().getBackgroundProcessingLabel()) .setSmallIcon(R.drawable.ic_screenrecord) - .setGroup(GROUP_KEY) + .setGroup(GROUP_KEY_SAVED) .addExtras(extras); return builder.build(); } @@ -387,7 +394,7 @@ public class RecordingService extends Service implements ScreenMediaRecorderList PendingIntent.FLAG_IMMUTABLE)) .addAction(shareAction) .setAutoCancel(true) - .setGroup(GROUP_KEY) + .setGroup(GROUP_KEY_SAVED) .addExtras(extras); // Add thumbnail if available @@ -402,21 +409,28 @@ public class RecordingService extends Service implements ScreenMediaRecorderList } /** - * Adds a group notification so that save notifications from multiple recordings are - * grouped together, and the foreground service recording notification is not + * Adds a group summary notification for save notifications so that save notifications from + * multiple recordings are grouped together, and the foreground service recording notification + * is not. */ - private void postGroupNotification(UserHandle currentUser) { + private void postGroupSummaryNotificationForSaves(UserHandle currentUser) { + postGroupSummaryNotification(currentUser, strings().getSaveTitle(), GROUP_KEY_SAVED); + } + + /** Posts a group summary notification for the given group. */ + private void postGroupSummaryNotification( + UserHandle currentUser, String notificationContentTitle, String groupKey) { Bundle extras = new Bundle(); extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, strings().getTitle()); Notification groupNotif = new Notification.Builder(this, getChannelId()) .setSmallIcon(R.drawable.ic_screenrecord) - .setContentTitle(strings().getSaveTitle()) - .setGroup(GROUP_KEY) + .setContentTitle(notificationContentTitle) + .setGroup(groupKey) .setGroupSummary(true) .setExtras(extras) .build(); - mNotificationManager.notifyAsUser(getTag(), NOTIF_BASE_ID, groupNotif, currentUser); + mNotificationManager.notifyAsUser(getTag(), mNotificationId, groupNotif, currentUser); } private void stopService() { @@ -427,6 +441,7 @@ public class RecordingService extends Service implements ScreenMediaRecorderList if (userId == USER_ID_NOT_SPECIFIED) { userId = mUserContextTracker.getUserContext().getUserId(); } + UserHandle currentUser = new UserHandle(userId); Log.d(getTag(), "notifying for user " + userId); setTapsVisible(mOriginalShowTaps); try { @@ -444,7 +459,7 @@ public class RecordingService extends Service implements ScreenMediaRecorderList Log.e(getTag(), "stopRecording called, but there was an error when ending" + "recording"); exception.printStackTrace(); - createErrorSavingNotification(); + createErrorSavingNotification(currentUser); } catch (Throwable throwable) { if (getRecorder() != null) { // Something unexpected happen, SystemUI will crash but let's delete @@ -468,7 +483,7 @@ public class RecordingService extends Service implements ScreenMediaRecorderList Log.d(getTag(), "saving recording"); Notification notification = createSaveNotification( getRecorder() != null ? getRecorder().save() : null); - postGroupNotification(currentUser); + postGroupSummaryNotificationForSaves(currentUser); mNotificationManager.notifyAsUser(null, mNotificationId, notification, currentUser); } catch (IOException | IllegalStateException e) { diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java index 540d4c43c58d..7b802a2a40aa 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java @@ -17,7 +17,6 @@ package com.android.systemui.screenshot; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; -import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT; import static com.android.systemui.Flags.screenshotSaveImageExporter; import static com.android.systemui.screenshot.LogConfig.DEBUG_ANIM; @@ -31,7 +30,6 @@ import static com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_INTERAC import android.animation.Animator; import android.animation.AnimatorListenerAdapter; -import android.annotation.MainThread; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.BroadcastReceiver; @@ -52,17 +50,12 @@ import android.util.DisplayMetrics; import android.util.Log; import android.view.Display; import android.view.ScrollCaptureResponse; -import android.view.View; -import android.view.ViewGroup; import android.view.ViewRootImpl; -import android.view.ViewTreeObserver; -import android.view.WindowInsets; import android.view.WindowManager; import android.widget.Toast; import android.window.WindowContext; import com.android.internal.logging.UiEventLogger; -import com.android.internal.policy.PhoneWindow; import com.android.settingslib.applications.InterestingConfigChanges; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.broadcast.BroadcastSender; @@ -115,11 +108,9 @@ public class ScreenshotController implements InteractiveScreenshotHandler { private final BroadcastDispatcher mBroadcastDispatcher; private final ScreenshotActionsController mActionsController; - private final WindowManager mWindowManager; - private final WindowManager.LayoutParams mWindowLayoutParams; @Nullable private final ScreenshotSoundController mScreenshotSoundController; - private final PhoneWindow mWindow; + private final ScreenshotWindow mWindow; private final Display mDisplay; private final ScrollCaptureExecutor mScrollCaptureExecutor; private final ScreenshotNotificationSmartActionsProvider @@ -135,8 +126,6 @@ public class ScreenshotController implements InteractiveScreenshotHandler { private Bitmap mScreenBitmap; private SaveImageInBackgroundTask mSaveInBgTask; private boolean mScreenshotTakenInPortrait; - private boolean mAttachRequested; - private boolean mDetachRequested; private Animator mScreenshotAnimation; private RequestCallback mCurrentRequestCallback; private String mPackageName = ""; @@ -155,7 +144,7 @@ public class ScreenshotController implements InteractiveScreenshotHandler { @AssistedInject ScreenshotController( Context context, - WindowManager windowManager, + ScreenshotWindow.Factory screenshotWindowFactory, FeatureFlags flags, ScreenshotShelfViewProxy.Factory viewProxyFactory, ScreenshotSmartActions screenshotSmartActions, @@ -195,9 +184,8 @@ public class ScreenshotController implements InteractiveScreenshotHandler { mScreenshotHandler.setDefaultTimeoutMillis(SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS); mDisplay = display; - mWindowManager = windowManager; - final Context displayContext = context.createDisplayContext(display); - mContext = (WindowContext) displayContext.createWindowContext(TYPE_SCREENSHOT, null); + mWindow = screenshotWindowFactory.create(mDisplay); + mContext = mWindow.getContext(); mFlags = flags; mUserManager = userManager; mMessageContainerController = messageContainerController; @@ -213,17 +201,10 @@ public class ScreenshotController implements InteractiveScreenshotHandler { mViewProxy.requestDismissal(SCREENSHOT_INTERACTION_TIMEOUT); }); - // Setup the window that we are going to use - mWindowLayoutParams = FloatingWindowUtil.getFloatingWindowParams(); - mWindowLayoutParams.setTitle("ScreenshotAnimation"); - - mWindow = FloatingWindowUtil.getFloatingWindow(mContext); - mWindow.setWindowManager(mWindowManager, null, null); - mConfigChanges.applyNewConfig(context.getResources()); reloadAssets(); - mActionExecutor = actionExecutorFactory.create(mWindow, mViewProxy, + mActionExecutor = actionExecutorFactory.create(mWindow.getWindow(), mViewProxy, () -> { finishDismiss(); return Unit.INSTANCE; @@ -318,12 +299,12 @@ public class ScreenshotController implements InteractiveScreenshotHandler { } // The window is focusable by default - setWindowFocusable(true); + mWindow.setFocusable(true); mViewProxy.requestFocus(); enqueueScrollCaptureRequest(requestId, screenshot.getUserHandle()); - attachWindow(); + mWindow.attachWindow(); boolean showFlash; if (screenshot.getType() == WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE) { @@ -347,13 +328,10 @@ public class ScreenshotController implements InteractiveScreenshotHandler { mViewProxy.setScreenshot(screenshot); - // ignore system bar insets for the purpose of window layout - mWindow.getDecorView().setOnApplyWindowInsetsListener( - (v, insets) -> WindowInsets.CONSUMED); } void prepareViewForNewScreenshot(@NonNull ScreenshotData screenshot, String oldPackageName) { - withWindowAttached(() -> { + mWindow.whenWindowAttached(() -> { mAnnouncementResolver.getScreenshotAnnouncement( screenshot.getUserHandle().getIdentifier(), announcement -> { @@ -444,7 +422,7 @@ public class ScreenshotController implements InteractiveScreenshotHandler { @Override public void onTouchOutside() { // TODO(159460485): Remove this when focus is handled properly in the system - setWindowFocusable(false); + mWindow.setFocusable(false); } }); @@ -457,9 +435,9 @@ public class ScreenshotController implements InteractiveScreenshotHandler { private void enqueueScrollCaptureRequest(UUID requestId, UserHandle owner) { // Wait until this window is attached to request because it is // the reference used to locate the target window (below). - withWindowAttached(() -> { + mWindow.whenWindowAttached(() -> { requestScrollCapture(requestId, owner); - mWindow.peekDecorView().getViewRootImpl().setActivityConfigCallback( + mWindow.setActivityConfigCallback( new ViewRootImpl.ActivityConfigCallback() { @Override public void onConfigurationChanged(Configuration overrideConfig, @@ -472,8 +450,7 @@ public class ScreenshotController implements InteractiveScreenshotHandler { // to set up in the new orientation. mScreenshotHandler.postDelayed( () -> requestScrollCapture(requestId, owner), 150); - mViewProxy.updateInsets( - mWindowManager.getCurrentWindowMetrics().getWindowInsets()); + mViewProxy.updateInsets(mWindow.getWindowInsets()); // Screenshot animation calculations won't be valid anymore, // so just end if (mScreenshotAnimation != null @@ -489,7 +466,7 @@ public class ScreenshotController implements InteractiveScreenshotHandler { private void requestScrollCapture(UUID requestId, UserHandle owner) { mScrollCaptureExecutor.requestScrollCapture( mDisplay.getDisplayId(), - mWindow.getDecorView().getWindowToken(), + mWindow.getWindowToken(), (response) -> { mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_LONG_SCREENSHOT_IMPRESSION, 0, response.getPackageName()); @@ -528,61 +505,9 @@ public class ScreenshotController implements InteractiveScreenshotHandler { mViewProxy::startLongScreenshotTransition); } - private void withWindowAttached(Runnable action) { - View decorView = mWindow.getDecorView(); - if (decorView.isAttachedToWindow()) { - action.run(); - } else { - decorView.getViewTreeObserver().addOnWindowAttachListener( - new ViewTreeObserver.OnWindowAttachListener() { - @Override - public void onWindowAttached() { - mAttachRequested = false; - decorView.getViewTreeObserver().removeOnWindowAttachListener(this); - action.run(); - } - - @Override - public void onWindowDetached() { - } - }); - - } - } - - @MainThread - private void attachWindow() { - View decorView = mWindow.getDecorView(); - if (decorView.isAttachedToWindow() || mAttachRequested) { - return; - } - if (DEBUG_WINDOW) { - Log.d(TAG, "attachWindow"); - } - mAttachRequested = true; - mWindowManager.addView(decorView, mWindowLayoutParams); - decorView.requestApplyInsets(); - - ViewGroup layout = decorView.requireViewById(android.R.id.content); - layout.setClipChildren(false); - layout.setClipToPadding(false); - } - @Override public void removeWindow() { - final View decorView = mWindow.peekDecorView(); - if (decorView != null && decorView.isAttachedToWindow()) { - if (DEBUG_WINDOW) { - Log.d(TAG, "Removing screenshot window"); - } - mWindowManager.removeViewImmediate(decorView); - mDetachRequested = false; - } - if (mAttachRequested && !mDetachRequested) { - mDetachRequested = true; - withWindowAttached(this::removeWindow); - } - + mWindow.removeWindow(); mViewProxy.stopInputListening(); } @@ -759,33 +684,6 @@ public class ScreenshotController implements InteractiveScreenshotHandler { .getContentResolver(), SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1; } - /** - * Updates the window focusability. If the window is already showing, then it updates the - * window immediately, otherwise the layout params will be applied when the window is next - * shown. - */ - private void setWindowFocusable(boolean focusable) { - if (DEBUG_WINDOW) { - Log.d(TAG, "setWindowFocusable: " + focusable); - } - int flags = mWindowLayoutParams.flags; - if (focusable) { - mWindowLayoutParams.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; - } else { - mWindowLayoutParams.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; - } - if (mWindowLayoutParams.flags == flags) { - if (DEBUG_WINDOW) { - Log.d(TAG, "setWindowFocusable: skipping, already " + focusable); - } - return; - } - final View decorView = mWindow.peekDecorView(); - if (decorView != null && decorView.isAttachedToWindow()) { - mWindowManager.updateViewLayout(decorView, mWindowLayoutParams); - } - } - private Rect getFullScreenRect() { DisplayMetrics displayMetrics = new DisplayMetrics(); mDisplay.getRealMetrics(displayMetrics); @@ -826,6 +724,6 @@ public class ScreenshotController implements InteractiveScreenshotHandler { * * @param display display to capture */ - LegacyScreenshotController create(Display display); + ScreenshotController create(Display display); } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotWindow.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotWindow.kt new file mode 100644 index 000000000000..644e12cba6fc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotWindow.kt @@ -0,0 +1,194 @@ +/* + * 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.screenshot + +import android.R +import android.annotation.MainThread +import android.content.Context +import android.graphics.PixelFormat +import android.os.IBinder +import android.util.Log +import android.view.Display +import android.view.View +import android.view.ViewGroup +import android.view.ViewRootImpl +import android.view.ViewTreeObserver.OnWindowAttachListener +import android.view.Window +import android.view.WindowInsets +import android.view.WindowManager +import android.window.WindowContext +import com.android.internal.policy.PhoneWindow +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +/** Creates and manages the window in which the screenshot UI is displayed. */ +class ScreenshotWindow +@AssistedInject +constructor( + private val windowManager: WindowManager, + private val context: Context, + @Assisted private val display: Display, +) { + + val window: PhoneWindow = + PhoneWindow( + context + .createDisplayContext(display) + .createWindowContext(WindowManager.LayoutParams.TYPE_SCREENSHOT, null) + ) + private val params = + WindowManager.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + 0, /* xpos */ + 0, /* ypos */ + WindowManager.LayoutParams.TYPE_SCREENSHOT, + WindowManager.LayoutParams.FLAG_FULLSCREEN or + WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or + WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or + WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH or + WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or + WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM, + PixelFormat.TRANSLUCENT + ) + .apply { + layoutInDisplayCutoutMode = + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS + setFitInsetsTypes(0) + // This is needed to let touches pass through outside the touchable areas + privateFlags = + privateFlags or WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY + title = "ScreenshotUI" + } + private var attachRequested: Boolean = false + private var detachRequested: Boolean = false + + init { + window.requestFeature(Window.FEATURE_NO_TITLE) + window.requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS) + window.setBackgroundDrawableResource(R.color.transparent) + window.setWindowManager(windowManager, null, null) + } + + @MainThread + fun attachWindow() { + val decorView: View = window.getDecorView() + if (decorView.isAttachedToWindow || attachRequested) { + return + } + if (LogConfig.DEBUG_WINDOW) { + Log.d(TAG, "attachWindow") + } + attachRequested = true + windowManager.addView(decorView, params) + + decorView.requestApplyInsets() + decorView.requireViewById<ViewGroup>(R.id.content).apply { + clipChildren = false + clipToPadding = false + // ignore system bar insets for the purpose of window layout + setOnApplyWindowInsetsListener { _, _ -> WindowInsets.CONSUMED } + } + } + + fun whenWindowAttached(action: Runnable) { + val decorView: View = window.getDecorView() + if (decorView.isAttachedToWindow) { + action.run() + } else { + decorView + .getViewTreeObserver() + .addOnWindowAttachListener( + object : OnWindowAttachListener { + override fun onWindowAttached() { + attachRequested = false + decorView.getViewTreeObserver().removeOnWindowAttachListener(this) + action.run() + } + + override fun onWindowDetached() {} + } + ) + } + } + + fun removeWindow() { + val decorView: View? = window.peekDecorView() + if (decorView != null && decorView.isAttachedToWindow) { + if (LogConfig.DEBUG_WINDOW) { + Log.d(TAG, "Removing screenshot window") + } + windowManager.removeViewImmediate(decorView) + detachRequested = false + } + if (attachRequested && !detachRequested) { + detachRequested = true + whenWindowAttached { removeWindow() } + } + } + + /** + * Updates the window focusability. If the window is already showing, then it updates the window + * immediately, otherwise the layout params will be applied when the window is next shown. + */ + fun setFocusable(focusable: Boolean) { + if (LogConfig.DEBUG_WINDOW) { + Log.d(TAG, "setWindowFocusable: $focusable") + } + val flags: Int = params.flags + if (focusable) { + params.flags = params.flags and WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE.inv() + } else { + params.flags = params.flags or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + } + if (params.flags == flags) { + if (LogConfig.DEBUG_WINDOW) { + Log.d(TAG, "setWindowFocusable: skipping, already $focusable") + } + return + } + window.peekDecorView()?.also { + if (it.isAttachedToWindow) { + windowManager.updateViewLayout(it, params) + } + } + } + + fun getContext(): WindowContext = window.context as WindowContext + + fun getWindowToken(): IBinder = window.decorView.windowToken + + fun getWindowInsets(): WindowInsets = windowManager.currentWindowMetrics.windowInsets + + fun setContentView(view: View) { + window.setContentView(view) + } + + fun setActivityConfigCallback(callback: ViewRootImpl.ActivityConfigCallback) { + window.peekDecorView().viewRootImpl.setActivityConfigCallback(callback) + } + + @AssistedFactory + interface Factory { + fun create(display: Display): ScreenshotWindow + } + + companion object { + private const val TAG = "ScreenshotWindow" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/ui/viewModel/BrightnessMirrorViewModel.kt b/packages/SystemUI/src/com/android/systemui/settings/brightness/ui/viewModel/BrightnessMirrorViewModel.kt index 79e8b879288e..7f8c1463ed1f 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/brightness/ui/viewModel/BrightnessMirrorViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/ui/viewModel/BrightnessMirrorViewModel.kt @@ -19,25 +19,25 @@ package com.android.systemui.settings.brightness.ui.viewModel import android.content.res.Resources import android.util.Log import android.view.View -import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.lifecycle.SysUiViewModel import com.android.systemui.res.R import com.android.systemui.settings.brightness.BrightnessSliderController import com.android.systemui.settings.brightness.MirrorController import com.android.systemui.settings.brightness.ToggleSlider import com.android.systemui.settings.brightness.domain.interactor.BrightnessMirrorShowingInteractor -import javax.inject.Inject +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -@SysUISingleton class BrightnessMirrorViewModel -@Inject +@AssistedInject constructor( private val brightnessMirrorShowingInteractor: BrightnessMirrorShowingInteractor, @Main private val resources: Resources, val sliderControllerFactory: BrightnessSliderController.Factory, -) : MirrorController { +) : SysUiViewModel(), MirrorController { private val tempPosition = IntArray(2) @@ -99,6 +99,11 @@ constructor( override fun removeCallback(listener: MirrorController.BrightnessMirrorListener) {} + @AssistedFactory + interface Factory { + fun create(): BrightnessMirrorViewModel + } + companion object { private const val TAG = "BrightnessMirrorViewModel" } diff --git a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt index 05c50fe18c8b..15bbef02196a 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt @@ -293,7 +293,7 @@ constructor( ) containerView.systemGestureExclusionRects = - if (Flags.hubmodeFullscreenVerticalSwipe()) { + if (Flags.hubmodeFullscreenVerticalSwipeFix()) { listOf( // Disable back gestures on the left side of the screen, to avoid // conflicting with scene transitions. diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt index 684a4845144e..d64b21f2254f 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt @@ -55,7 +55,7 @@ constructor( keyguardRepository: KeyguardRepository, keyguardTransitionInteractor: KeyguardTransitionInteractor, powerInteractor: PowerInteractor, - shadeRepository: ShadeRepository, + private val shadeRepository: ShadeRepository, userSetupRepository: UserSetupRepository, userSwitcherInteractor: UserSwitcherInteractor, private val baseShadeInteractor: BaseShadeInteractor, @@ -114,11 +114,13 @@ constructor( initialValue = determineShadeMode(isShadeLayoutWide.value) ) - override val shadeAlignment: ShadeAlignment = - if (shadeRepository.isDualShadeAlignedToBottom) { - ShadeAlignment.Bottom - } else { - ShadeAlignment.Top + override val shadeAlignment: ShadeAlignment + get() { + return if (shadeRepository.isDualShadeAlignedToBottom) { + ShadeAlignment.Bottom + } else { + ShadeAlignment.Top + } } override val isExpandToQsEnabled: Flow<Boolean> = diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/BaseShadeSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/BaseShadeSceneViewModel.kt new file mode 100644 index 000000000000..068d6a74c8a7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/BaseShadeSceneViewModel.kt @@ -0,0 +1,56 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.android.systemui.shade.ui.viewmodel + +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor +import com.android.systemui.lifecycle.SysUiViewModel +import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.scene.shared.model.Scenes +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest + +/** Base class for classes that model UI state of the content of shade scenes. */ +abstract class BaseShadeSceneViewModel( + private val deviceEntryInteractor: DeviceEntryInteractor, + private val sceneInteractor: SceneInteractor, +) : SysUiViewModel() { + + private val _isEmptySpaceClickable = + MutableStateFlow(!deviceEntryInteractor.isDeviceEntered.value) + /** Whether clicking on the empty area of the shade does something */ + val isEmptySpaceClickable: StateFlow<Boolean> = _isEmptySpaceClickable.asStateFlow() + + override suspend fun onActivated() { + deviceEntryInteractor.isDeviceEntered.collectLatest { isDeviceEntered -> + _isEmptySpaceClickable.value = !isDeviceEntered + } + } + + /** Notifies that the empty space in the shade has been clicked. */ + fun onEmptySpaceClicked() { + if (!isEmptySpaceClickable.value) { + return + } + + sceneInteractor.changeScene(Scenes.Lockscreen, "Shade empty space clicked.") + } +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModel.kt index 6551854dcb36..566bc166ed40 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModel.kt @@ -17,43 +17,39 @@ package com.android.systemui.shade.ui.viewmodel import com.android.compose.animation.scene.SceneKey -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.lifecycle.SysUiViewModel import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.model.SceneFamilies import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.domain.interactor.ShadeInteractor -import javax.inject.Inject -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.SharingStarted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest /** * Models UI state and handles user input for the overlay shade UI, which shows a shade as an * overlay on top of another scene UI. */ -@SysUISingleton class OverlayShadeViewModel -@Inject -constructor( - @Application applicationScope: CoroutineScope, - private val sceneInteractor: SceneInteractor, - shadeInteractor: ShadeInteractor -) { +@AssistedInject +constructor(private val sceneInteractor: SceneInteractor, shadeInteractor: ShadeInteractor) : + SysUiViewModel() { + private val _backgroundScene = MutableStateFlow(Scenes.Lockscreen) /** The scene to show in the background when the overlay shade is open. */ - val backgroundScene: StateFlow<SceneKey> = - sceneInteractor - .resolveSceneFamily(SceneFamilies.Home) - .stateIn( - scope = applicationScope, - started = SharingStarted.WhileSubscribed(), - initialValue = Scenes.Lockscreen, - ) + val backgroundScene: StateFlow<SceneKey> = _backgroundScene.asStateFlow() /** Dictates the alignment of the overlay shade panel on the screen. */ val panelAlignment = shadeInteractor.shadeAlignment + override suspend fun onActivated() { + sceneInteractor.resolveSceneFamily(SceneFamilies.Home).collectLatest { sceneKey -> + _backgroundScene.value = sceneKey + } + } + /** Notifies that the user has clicked the semi-transparent background scrim. */ fun onScrimClicked() { sceneInteractor.changeScene( @@ -61,4 +57,9 @@ constructor( loggingReason = "Shade scrim clicked", ) } + + @AssistedFactory + interface Factory { + fun create(): OverlayShadeViewModel + } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt index b2e0cd04687c..03fdfa9aaa6c 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt @@ -24,8 +24,7 @@ import android.icu.text.DisplayContext import android.os.UserHandle import android.provider.Settings import com.android.systemui.broadcast.BroadcastDispatcher -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.lifecycle.SysUiViewModel import com.android.systemui.plugins.ActivityStarter import com.android.systemui.privacy.OngoingPrivacyChip import com.android.systemui.privacy.PrivacyItem @@ -38,44 +37,40 @@ import com.android.systemui.shade.domain.interactor.ShadeHeaderClockInteractor import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import java.util.Date import java.util.Locale -import javax.inject.Inject -import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch /** Models UI state for the shade header. */ -@SysUISingleton class ShadeHeaderViewModel -@Inject +@AssistedInject constructor( - @Application private val applicationScope: CoroutineScope, - context: Context, + private val context: Context, private val activityStarter: ActivityStarter, private val sceneInteractor: SceneInteractor, - shadeInteractor: ShadeInteractor, - mobileIconsInteractor: MobileIconsInteractor, + private val shadeInteractor: ShadeInteractor, + private val mobileIconsInteractor: MobileIconsInteractor, val mobileIconsViewModel: MobileIconsViewModel, private val privacyChipInteractor: PrivacyChipInteractor, private val clockInteractor: ShadeHeaderClockInteractor, - broadcastDispatcher: BroadcastDispatcher, -) { + private val broadcastDispatcher: BroadcastDispatcher, +) : SysUiViewModel() { /** True if there is exactly one mobile connection. */ val isSingleCarrier: StateFlow<Boolean> = mobileIconsInteractor.isSingleCarrier + private val _mobileSubIds = MutableStateFlow(emptyList<Int>()) /** The list of subscription Ids for current mobile connections. */ - val mobileSubIds = - mobileIconsInteractor.filteredSubscriptions - .map { list -> list.map { it.subscriptionId } } - .stateIn(applicationScope, SharingStarted.WhileSubscribed(), emptyList()) + val mobileSubIds: StateFlow<List<Int>> = _mobileSubIds.asStateFlow() /** The list of PrivacyItems to be displayed by the privacy chip. */ val privacyItems: StateFlow<List<PrivacyItem>> = privacyChipInteractor.privacyItems @@ -94,11 +89,9 @@ constructor( /** Whether or not the privacy chip is enabled in the device privacy config. */ val isPrivacyChipEnabled: StateFlow<Boolean> = privacyChipInteractor.isChipEnabled + private val _isDisabled = MutableStateFlow(false) /** Whether or not the Shade Header should be disabled based on disableFlags. */ - val isDisabled: StateFlow<Boolean> = - shadeInteractor.isQsEnabled - .map { !it } - .stateIn(applicationScope, SharingStarted.WhileSubscribed(), false) + val isDisabled: StateFlow<Boolean> = _isDisabled.asStateFlow() private val longerPattern = context.getString(R.string.abbrev_wday_month_day_no_year_alarm) private val shorterPattern = context.getString(R.string.abbrev_month_day_no_year) @@ -111,26 +104,40 @@ constructor( private val _longerDateText: MutableStateFlow<String> = MutableStateFlow("") val longerDateText: StateFlow<String> = _longerDateText.asStateFlow() - init { - broadcastDispatcher - .broadcastFlow( - filter = - IntentFilter().apply { - addAction(Intent.ACTION_TIME_TICK) - addAction(Intent.ACTION_TIME_CHANGED) - addAction(Intent.ACTION_TIMEZONE_CHANGED) - addAction(Intent.ACTION_LOCALE_CHANGED) - }, - user = UserHandle.SYSTEM, - map = { intent, _ -> - intent.action == Intent.ACTION_TIMEZONE_CHANGED || - intent.action == Intent.ACTION_LOCALE_CHANGED - } - ) - .onEach { invalidateFormats -> updateDateTexts(invalidateFormats) } - .launchIn(applicationScope) - - applicationScope.launch { updateDateTexts(false) } + override suspend fun onActivated() { + coroutineScope { + launch { + broadcastDispatcher + .broadcastFlow( + filter = + IntentFilter().apply { + addAction(Intent.ACTION_TIME_TICK) + addAction(Intent.ACTION_TIME_CHANGED) + addAction(Intent.ACTION_TIMEZONE_CHANGED) + addAction(Intent.ACTION_LOCALE_CHANGED) + }, + user = UserHandle.SYSTEM, + map = { intent, _ -> + intent.action == Intent.ACTION_TIMEZONE_CHANGED || + intent.action == Intent.ACTION_LOCALE_CHANGED + } + ) + .onEach { invalidateFormats -> updateDateTexts(invalidateFormats) } + .launchIn(this) + } + + launch { updateDateTexts(false) } + + launch { + mobileIconsInteractor.filteredSubscriptions + .map { list -> list.map { it.subscriptionId } } + .collectLatest { _mobileSubIds.value = it } + } + + launch { + shadeInteractor.isQsEnabled.map { !it }.collectLatest { _isDisabled.value = it } + } + } } /** Notifies that the privacy chip was clicked. */ @@ -182,4 +189,9 @@ constructor( format.setContext(DisplayContext.CAPITALIZATION_FOR_BEGINNING_OF_SENTENCE) return format } + + @AssistedFactory + interface Factory { + fun create(): ShadeHeaderViewModel + } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneActionsViewModel.kt new file mode 100644 index 000000000000..bdc0fdba1dea --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneActionsViewModel.kt @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2023 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.shade.ui.viewmodel + +import com.android.compose.animation.scene.Swipe +import com.android.compose.animation.scene.SwipeDirection +import com.android.compose.animation.scene.UserAction +import com.android.compose.animation.scene.UserActionResult +import com.android.systemui.qs.ui.adapter.QSSceneAdapter +import com.android.systemui.scene.shared.model.SceneFamilies +import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade +import com.android.systemui.scene.ui.viewmodel.SceneActionsViewModel +import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.shade.shared.model.ShadeMode +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine + +/** + * Models the UI state for the user actions that the user can perform to navigate to other scenes. + * + * Different from the [ShadeSceneContentViewModel] which models the _content_ of the scene. + */ +class ShadeSceneActionsViewModel +@AssistedInject +constructor( + private val qsSceneAdapter: QSSceneAdapter, + private val shadeInteractor: ShadeInteractor, +) : SceneActionsViewModel() { + + override suspend fun hydrateActions(setActions: (Map<UserAction, UserActionResult>) -> Unit) { + combine( + shadeInteractor.shadeMode, + qsSceneAdapter.isCustomizerShowing, + ) { shadeMode, isCustomizerShowing -> + buildMap<UserAction, UserActionResult> { + if (!isCustomizerShowing) { + set( + Swipe(SwipeDirection.Up), + UserActionResult( + SceneFamilies.Home, + ToSplitShade.takeIf { shadeMode is ShadeMode.Split } + ) + ) + } + + // TODO(b/330200163) Add an else to be able to collapse the shade while + // customizing + if (shadeMode is ShadeMode.Single) { + set(Swipe(SwipeDirection.Down), UserActionResult(Scenes.QuickSettings)) + } + } + } + .collectLatest { actions -> setActions(actions) } + } + + @AssistedFactory + interface Factory { + fun create(): ShadeSceneActionsViewModel + } +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneContentViewModel.kt new file mode 100644 index 000000000000..3cdff964e26a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneContentViewModel.kt @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2023 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. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.android.systemui.shade.ui.viewmodel + +import androidx.lifecycle.LifecycleOwner +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor +import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor +import com.android.systemui.qs.FooterActionsController +import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel +import com.android.systemui.qs.ui.adapter.QSSceneAdapter +import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.settings.brightness.ui.viewModel.BrightnessMirrorViewModel +import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.shade.shared.model.ShadeMode +import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import java.util.concurrent.atomic.AtomicBoolean +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow + +/** + * Models UI state used to render the content of the shade scene. + * + * Different from [ShadeSceneActionsViewModel], which only models user actions that can be performed + * to navigate to other scenes. + */ +class ShadeSceneContentViewModel +@AssistedInject +constructor( + val qsSceneAdapter: QSSceneAdapter, + val shadeHeaderViewModelFactory: ShadeHeaderViewModel.Factory, + val brightnessMirrorViewModelFactory: BrightnessMirrorViewModel.Factory, + val mediaCarouselInteractor: MediaCarouselInteractor, + shadeInteractor: ShadeInteractor, + private val footerActionsViewModelFactory: FooterActionsViewModel.Factory, + private val footerActionsController: FooterActionsController, + private val unfoldTransitionInteractor: UnfoldTransitionInteractor, + deviceEntryInteractor: DeviceEntryInteractor, + sceneInteractor: SceneInteractor, +) : + BaseShadeSceneViewModel( + deviceEntryInteractor, + sceneInteractor, + ) { + + val shadeMode: StateFlow<ShadeMode> = shadeInteractor.shadeMode + + val isMediaVisible: StateFlow<Boolean> = mediaCarouselInteractor.hasActiveMediaOrRecommendation + + private val footerActionsControllerInitialized = AtomicBoolean(false) + + /** + * Amount of X-axis translation to apply to various elements as the unfolded foldable is folded + * slightly, in pixels. + */ + fun unfoldTranslationX(isOnStartSide: Boolean): Flow<Float> { + return unfoldTransitionInteractor.unfoldTranslationX(isOnStartSide) + } + + fun getFooterActionsViewModel(lifecycleOwner: LifecycleOwner): FooterActionsViewModel { + if (footerActionsControllerInitialized.compareAndSet(false, true)) { + footerActionsController.init() + } + return footerActionsViewModelFactory.create(lifecycleOwner) + } + + @AssistedFactory + interface Factory { + fun create(): ShadeSceneContentViewModel + } +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt deleted file mode 100644 index 06298efc95a4..000000000000 --- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright (C) 2023 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.shade.ui.viewmodel - -import androidx.lifecycle.LifecycleOwner -import com.android.compose.animation.scene.SceneKey -import com.android.compose.animation.scene.Swipe -import com.android.compose.animation.scene.SwipeDirection -import com.android.compose.animation.scene.UserAction -import com.android.compose.animation.scene.UserActionResult -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.lifecycle.Activatable -import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor -import com.android.systemui.qs.FooterActionsController -import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel -import com.android.systemui.qs.ui.adapter.QSSceneAdapter -import com.android.systemui.scene.domain.interactor.SceneInteractor -import com.android.systemui.scene.shared.model.SceneFamilies -import com.android.systemui.scene.shared.model.Scenes -import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade -import com.android.systemui.settings.brightness.ui.viewModel.BrightnessMirrorViewModel -import com.android.systemui.shade.domain.interactor.ShadeInteractor -import com.android.systemui.shade.shared.model.ShadeMode -import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor -import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated -import java.util.concurrent.atomic.AtomicBoolean -import javax.inject.Inject -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch - -/** Models UI state and handles user input for the shade scene. */ -@SysUISingleton -class ShadeSceneViewModel -@Inject -constructor( - val qsSceneAdapter: QSSceneAdapter, - val shadeHeaderViewModel: ShadeHeaderViewModel, - val brightnessMirrorViewModel: BrightnessMirrorViewModel, - val mediaCarouselInteractor: MediaCarouselInteractor, - shadeInteractor: ShadeInteractor, - private val footerActionsViewModelFactory: FooterActionsViewModel.Factory, - private val footerActionsController: FooterActionsController, - private val sceneInteractor: SceneInteractor, - private val unfoldTransitionInteractor: UnfoldTransitionInteractor, -) : Activatable { - val destinationScenes: Flow<Map<UserAction, UserActionResult>> = - combine( - shadeInteractor.shadeMode, - qsSceneAdapter.isCustomizerShowing, - ) { shadeMode, isCustomizerShowing -> - buildMap { - if (!isCustomizerShowing) { - set( - Swipe(SwipeDirection.Up), - UserActionResult( - SceneFamilies.Home, - ToSplitShade.takeIf { shadeMode is ShadeMode.Split } - ) - ) - } - - // TODO(b/330200163) Add an else to be able to collapse the shade while customizing - if (shadeMode is ShadeMode.Single) { - set(Swipe(SwipeDirection.Down), UserActionResult(Scenes.QuickSettings)) - } - } - } - - private val upDestinationSceneKey: Flow<SceneKey?> = - destinationScenes.map { it[Swipe(SwipeDirection.Up)]?.toScene } - - private val _isClickable = MutableStateFlow(false) - /** Whether or not the shade container should be clickable. */ - val isClickable: StateFlow<Boolean> = _isClickable.asStateFlow() - - /** - * Activates the view-model. - * - * Serves as an entrypoint to kick off coroutine work that the view-model requires in order to - * keep its state fresh and/or perform side-effects. - * - * Suspends the caller forever as it will keep doing work until canceled. - * - * **Must be invoked** when the scene becomes the current scene or when it becomes visible - * during a transition (the choice is the responsibility of the parent). Similarly, the work - * must be canceled when the scene stops being visible or the current scene. - */ - override suspend fun activate() { - coroutineScope { - launch { - upDestinationSceneKey - .flatMapLatestConflated { key -> - key?.let { sceneInteractor.resolveSceneFamily(key) } ?: flowOf(null) - } - .map { it == Scenes.Lockscreen } - .collectLatest { _isClickable.value = it } - } - } - } - - val shadeMode: StateFlow<ShadeMode> = shadeInteractor.shadeMode - - val isMediaVisible: StateFlow<Boolean> = mediaCarouselInteractor.hasActiveMediaOrRecommendation - - /** - * Amount of X-axis translation to apply to various elements as the unfolded foldable is folded - * slightly, in pixels. - */ - fun unfoldTranslationX(isOnStartSide: Boolean): Flow<Float> { - return unfoldTransitionInteractor.unfoldTranslationX(isOnStartSide) - } - - /** Notifies that some content in the shade was clicked. */ - fun onContentClicked() { - if (!isClickable.value) { - return - } - - sceneInteractor.changeScene(Scenes.Lockscreen, "Shade empty content clicked") - } - - private val footerActionsControllerInitialized = AtomicBoolean(false) - - fun getFooterActionsViewModel(lifecycleOwner: LifecycleOwner): FooterActionsViewModel { - if (footerActionsControllerInitialized.compareAndSet(false, true)) { - footerActionsController.init() - } - return footerActionsViewModelFactory.create(lifecycleOwner) - } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java index cea97d602236..50be6dcaa678 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java @@ -19,7 +19,6 @@ package com.android.systemui.statusbar; import static android.app.StatusBarManager.DISABLE2_NONE; import static android.app.StatusBarManager.DISABLE_NONE; import static android.inputmethodservice.InputMethodService.BACK_DISPOSITION_DEFAULT; -import static android.inputmethodservice.InputMethodService.IME_INVISIBLE; import static android.view.Display.INVALID_DISPLAY; import android.annotation.Nullable; @@ -1219,7 +1218,7 @@ public class CommandQueue extends IStatusBar.Stub implements && mLastUpdatedImeDisplayId != INVALID_DISPLAY) { // Set previous NavBar's IME window status as invisible when IME // window switched to another display for single-session IME case. - sendImeInvisibleStatusForPrevNavBar(); + sendImeNotVisibleStatusForPrevNavBar(); } for (int i = 0; i < mCallbacks.size(); i++) { mCallbacks.get(i).setImeWindowStatus(displayId, vis, backDisposition, showImeSwitcher); @@ -1227,9 +1226,9 @@ public class CommandQueue extends IStatusBar.Stub implements mLastUpdatedImeDisplayId = displayId; } - private void sendImeInvisibleStatusForPrevNavBar() { + private void sendImeNotVisibleStatusForPrevNavBar() { for (int i = 0; i < mCallbacks.size(); i++) { - mCallbacks.get(i).setImeWindowStatus(mLastUpdatedImeDisplayId, IME_INVISIBLE, + mCallbacks.get(i).setImeWindowStatus(mLastUpdatedImeDisplayId, 0 /* vis */, BACK_DISPOSITION_DEFAULT, false /* showImeSwitcher */); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java index c1eb8bcfa493..5eef8ea1999d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java @@ -785,7 +785,8 @@ public class KeyguardIndicationController { private void updateLockScreenAdaptiveAuthMsg(int userId) { final boolean deviceLocked = mKeyguardUpdateMonitor.isDeviceLockedByAdaptiveAuth(userId); - if (deviceLocked) { + final boolean canSkipBouncer = mKeyguardUpdateMonitor.getUserCanSkipBouncer(userId); + if (deviceLocked && !canSkipBouncer) { mRotateTextViewController.updateIndication( INDICATION_TYPE_ADAPTIVE_AUTH, new KeyguardIndication.Builder() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt index 1cb59f14626d..1027bc98ef47 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt @@ -81,11 +81,15 @@ constructor( * [CallType.Ongoing]. */ val ongoingCallNotification: Flow<ActiveNotificationModel?> = - allRepresentativeNotifications.map { notifMap -> - // Once a call has started, its `whenTime` should stay the same, so we can use it as a - // stable sort value. - notifMap.values.filter { it.callType == CallType.Ongoing }.minByOrNull { it.whenTime } - } + allRepresentativeNotifications + .map { notifMap -> + // Once a call has started, its `whenTime` should stay the same, so we can use it as + // a stable sort value. + notifMap.values + .filter { it.callType == CallType.Ongoing } + .minByOrNull { it.whenTime } + } + .flowOn(backgroundDispatcher) /** Are any notifications being actively presented in the notification stack? */ val areAnyNotificationsPresent: Flow<Boolean> = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt index a30b8772c3d1..fd08e898fce3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt @@ -17,14 +17,14 @@ package com.android.systemui.statusbar.notification.stack.ui.viewbinder import android.util.Log +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle import com.android.systemui.common.ui.ConfigurationState import com.android.systemui.common.ui.view.onLayoutChanged import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager -import com.android.systemui.lifecycle.WindowLifecycleState import com.android.systemui.lifecycle.repeatWhenAttached -import com.android.systemui.lifecycle.viewModel import com.android.systemui.res.R import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationScrollViewModel @@ -33,6 +33,7 @@ import com.android.systemui.util.kotlin.launchAndDispose import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.DisposableHandle +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch @@ -45,7 +46,7 @@ constructor( dumpManager: DumpManager, @Main private val mainImmediateDispatcher: CoroutineDispatcher, private val view: NotificationScrollView, - private val viewModelFactory: NotificationScrollViewModel.Factory, + private val viewModel: NotificationScrollViewModel, private val configuration: ConfigurationState, ) : FlowDumperImpl(dumpManager) { @@ -60,42 +61,38 @@ constructor( } fun bindWhileAttached(): DisposableHandle { - return view.asView().repeatWhenAttached(mainImmediateDispatcher) { bind() } + return view.asView().repeatWhenAttached(mainImmediateDispatcher) { + repeatOnLifecycle(Lifecycle.State.CREATED) { bind() } + } } - suspend fun bind(): Nothing = - view.asView().viewModel( - minWindowLifecycleState = WindowLifecycleState.ATTACHED, - factory = viewModelFactory::create, - ) { viewModel -> - launchAndDispose { - updateViewPosition() - view.asView().onLayoutChanged { updateViewPosition() } - } + suspend fun bind() = coroutineScope { + launchAndDispose { + updateViewPosition() + view.asView().onLayoutChanged { updateViewPosition() } + } - launch { - viewModel - .shadeScrimShape(cornerRadius = scrimRadius, viewLeftOffset = viewLeftOffset) - .collect { view.setScrimClippingShape(it) } - } + launch { + viewModel + .shadeScrimShape(cornerRadius = scrimRadius, viewLeftOffset = viewLeftOffset) + .collect { view.setScrimClippingShape(it) } + } - launch { viewModel.maxAlpha.collect { view.setMaxAlpha(it) } } - launch { viewModel.scrolledToTop.collect { view.setScrolledToTop(it) } } - launch { - viewModel.expandFraction.collect { view.setExpandFraction(it.coerceIn(0f, 1f)) } - } - launch { viewModel.isScrollable.collect { view.setScrollingEnabled(it) } } - launch { viewModel.isDozing.collect { isDozing -> view.setDozing(isDozing) } } + launch { viewModel.maxAlpha.collect { view.setMaxAlpha(it) } } + launch { viewModel.scrolledToTop.collect { view.setScrolledToTop(it) } } + launch { viewModel.expandFraction.collect { view.setExpandFraction(it.coerceIn(0f, 1f)) } } + launch { viewModel.isScrollable.collect { view.setScrollingEnabled(it) } } + launch { viewModel.isDozing.collect { isDozing -> view.setDozing(isDozing) } } - launchAndDispose { - view.setSyntheticScrollConsumer(viewModel.syntheticScrollConsumer) - view.setCurrentGestureOverscrollConsumer(viewModel.currentGestureOverscrollConsumer) - DisposableHandle { - view.setSyntheticScrollConsumer(null) - view.setCurrentGestureOverscrollConsumer(null) - } + launchAndDispose { + view.setSyntheticScrollConsumer(viewModel.syntheticScrollConsumer) + view.setCurrentGestureOverscrollConsumer(viewModel.currentGestureOverscrollConsumer) + DisposableHandle { + view.setSyntheticScrollConsumer(null) + view.setCurrentGestureOverscrollConsumer(null) } } + } /** flow of the scrim clipping radius */ private val scrimRadius: Flow<Int> diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt index 428102530334..2ba79a8612bb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt @@ -19,9 +19,9 @@ package com.android.systemui.statusbar.notification.stack.ui.viewmodel import com.android.compose.animation.scene.ObservableTransitionState import com.android.compose.animation.scene.SceneKey +import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dump.DumpManager import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor -import com.android.systemui.lifecycle.SysUiViewModel import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.scene.shared.model.SceneFamilies @@ -33,11 +33,9 @@ import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrim import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimShape import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationTransitionThresholds.EXPANSION_FOR_DELAYED_STACK_FADE_IN import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationTransitionThresholds.EXPANSION_FOR_MAX_SCRIM_ALPHA -import com.android.systemui.util.kotlin.FlowDumper import com.android.systemui.util.kotlin.FlowDumperImpl import dagger.Lazy -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject +import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged @@ -45,8 +43,9 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map /** ViewModel which represents the state of the NSSL/Controller in the world of flexiglass */ +@SysUISingleton class NotificationScrollViewModel -@AssistedInject +@Inject constructor( dumpManager: DumpManager, stackAppearanceInteractor: NotificationStackAppearanceInteractor, @@ -55,9 +54,7 @@ constructor( // TODO(b/336364825) Remove Lazy when SceneContainerFlag is released - // while the flag is off, creating this object too early results in a crash keyguardInteractor: Lazy<KeyguardInteractor>, -) : FlowDumper by FlowDumperImpl(dumpManager, "NotificationScrollViewModel"), - SysUiViewModel() { - +) : FlowDumperImpl(dumpManager) { /** * The expansion fraction of the notification stack. It should go from 0 to 1 when transitioning * from Gone to Shade scenes, and remain at 1 when in Lockscreen or Shade scenes and while @@ -189,9 +186,4 @@ constructor( keyguardInteractor.get().isDozing.dumpWhileCollecting("isDozing") } } - - @AssistedFactory - interface Factory { - fun create(): NotificationScrollViewModel - } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt index db91eed68b4c..d179888b569c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt @@ -22,7 +22,6 @@ import com.android.systemui.flags.FeatureFlagsClassic import com.android.systemui.flags.Flags import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.domain.interactor.ShadeInteractor -import com.android.systemui.shade.ui.viewmodel.ShadeSceneViewModel import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimBounds @@ -30,7 +29,6 @@ import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrim import com.android.systemui.util.kotlin.FlowDumperImpl import javax.inject.Inject import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.StateFlow /** * ViewModel used by the Notification placeholders inside the scene container to update the @@ -43,7 +41,6 @@ constructor( dumpManager: DumpManager, private val interactor: NotificationStackAppearanceInteractor, shadeInteractor: ShadeInteractor, - private val shadeSceneViewModel: ShadeSceneViewModel, private val headsUpNotificationInteractor: HeadsUpNotificationInteractor, featureFlags: FeatureFlagsClassic, ) : FlowDumperImpl(dumpManager) { @@ -63,20 +60,11 @@ constructor( interactor.setConstrainedAvailableSpace(height) } - /** Notifies that empty space on the notification scrim has been clicked. */ - fun onEmptySpaceClicked() { - shadeSceneViewModel.onContentClicked() - } - /** Sets the content alpha for the current state of the brightness mirror */ fun setAlphaForBrightnessMirror(alpha: Float) { interactor.setAlphaForBrightnessMirror(alpha) } - /** Whether or not the notification scrim should be clickable. */ - val isClickable: StateFlow<Boolean> - get() = shadeSceneViewModel.isClickable - /** True when a HUN is pinned or animating away. */ val isHeadsUpOrAnimatingAway: Flow<Boolean> = headsUpNotificationInteractor.isHeadsUpOrAnimatingAway diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt index b7531b0e2e0f..44b692fcb786 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt @@ -131,7 +131,7 @@ constructor( val on = context.resources.getString(R.string.zen_mode_on) val off = context.resources.getString(R.string.zen_mode_off) - return mode.rule.triggerDescription ?: if (mode.isActive) on else off + return mode.getDynamicDescription(context) ?: if (mode.isActive) on else off } private fun makeZenModeDialog(): Dialog { diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt index 1b00ae2103f0..5980e1de85b4 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt @@ -38,8 +38,8 @@ fun BackGestureTutorialScreen( TutorialScreenConfig.Strings( titleResId = R.string.touchpad_back_gesture_action_title, bodyResId = R.string.touchpad_back_gesture_guidance, - titleSuccessResId = R.string.touchpad_tutorial_gesture_done, - bodySuccessResId = R.string.touchpad_back_gesture_finished + titleSuccessResId = R.string.touchpad_back_gesture_success_title, + bodySuccessResId = R.string.touchpad_back_gesture_success_body ), animations = TutorialScreenConfig.Animations( diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt index 416c562d212d..9ac2cba2b8d8 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt @@ -213,10 +213,14 @@ fun TutorialAnimation( transitionSpec = { if (initialState == NOT_STARTED && targetState == IN_PROGRESS) { val transitionDurationMillis = 150 - fadeIn( - animationSpec = tween(transitionDurationMillis, easing = LinearEasing) - ) togetherWith - fadeOut(animationSpec = snap(delayMillis = transitionDurationMillis)) + fadeIn(animationSpec = tween(transitionDurationMillis, easing = LinearEasing)) + .togetherWith( + fadeOut(animationSpec = snap(delayMillis = transitionDurationMillis)) + ) + // we explicitly don't want size transform because when targetState + // animation is loaded for the first time, AnimatedContent thinks target + // size is smaller and tries to shrink initial state animation + .using(sizeTransform = null) } else { // empty transition works because all remaining transitions are from IN_PROGRESS // state which shares initial animation frame with both FINISHED and NOT_STARTED diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt index 51b14c295275..ed3110c04131 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt @@ -38,8 +38,8 @@ fun HomeGestureTutorialScreen( TutorialScreenConfig.Strings( titleResId = R.string.touchpad_home_gesture_action_title, bodyResId = R.string.touchpad_home_gesture_guidance, - titleSuccessResId = R.string.touchpad_home_gesture_done, - bodySuccessResId = R.string.touchpad_home_gesture_finished + titleSuccessResId = R.string.touchpad_home_gesture_success_title, + bodySuccessResId = R.string.touchpad_home_gesture_success_body ), animations = TutorialScreenConfig.Animations( diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt index 28ac2c0e8283..055671cf32ca 100644 --- a/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt @@ -28,6 +28,7 @@ import javax.inject.Inject import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine @@ -62,7 +63,9 @@ constructor( /** * Collect information for the given [flow], calling [consumer] for each emitted event. Defaults to * [LifeCycle.State.CREATED] to better align with legacy ViewController usage of attaching listeners - * during onViewAttached() and removing during onViewRemoved() + * during onViewAttached() and removing during onViewRemoved(). + * + * @return a disposable handle in order to cancel the flow in the future. */ @JvmOverloads fun <T> collectFlow( @@ -71,8 +74,8 @@ fun <T> collectFlow( consumer: Consumer<T>, coroutineContext: CoroutineContext = EmptyCoroutineContext, state: Lifecycle.State = Lifecycle.State.CREATED, -) { - view.repeatWhenAttached(coroutineContext) { +): DisposableHandle { + return view.repeatWhenAttached(coroutineContext) { repeatOnLifecycle(state) { flow.collect { consumer.accept(it) } } } } diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/GlobalSettingsImpl.java b/packages/SystemUI/src/com/android/systemui/util/settings/GlobalSettingsImpl.java index 816f55db65a0..7fcabe4a4363 100644 --- a/packages/SystemUI/src/com/android/systemui/util/settings/GlobalSettingsImpl.java +++ b/packages/SystemUI/src/com/android/systemui/util/settings/GlobalSettingsImpl.java @@ -42,28 +42,31 @@ class GlobalSettingsImpl implements GlobalSettings { mBgDispatcher = bgDispatcher; } + @NonNull @Override public ContentResolver getContentResolver() { return mContentResolver; } + @NonNull @Override - public Uri getUriFor(String name) { + public Uri getUriFor(@NonNull String name) { return Settings.Global.getUriFor(name); } + @NonNull @Override public CoroutineDispatcher getBackgroundDispatcher() { return mBgDispatcher; } @Override - public String getString(String name) { + public String getString(@NonNull String name) { return Settings.Global.getString(mContentResolver, name); } @Override - public boolean putString(String name, String value) { + public boolean putString(@NonNull String name, String value) { return Settings.Global.putString(mContentResolver, name, value); } diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/SecureSettingsImpl.java b/packages/SystemUI/src/com/android/systemui/util/settings/SecureSettingsImpl.java index f1da27f9cce9..c29648186d54 100644 --- a/packages/SystemUI/src/com/android/systemui/util/settings/SecureSettingsImpl.java +++ b/packages/SystemUI/src/com/android/systemui/util/settings/SecureSettingsImpl.java @@ -16,12 +16,11 @@ package com.android.systemui.util.settings; +import android.annotation.NonNull; import android.content.ContentResolver; import android.net.Uri; import android.provider.Settings; -import androidx.annotation.NonNull; - import com.android.systemui.util.kotlin.SettingsSingleThreadBackground; import kotlinx.coroutines.CoroutineDispatcher; @@ -43,46 +42,50 @@ class SecureSettingsImpl implements SecureSettings { mBgDispatcher = bgDispatcher; } + @NonNull @Override public ContentResolver getContentResolver() { return mContentResolver; } + @NonNull @Override public CurrentUserIdProvider getCurrentUserProvider() { return mCurrentUserProvider; } + @NonNull @Override - public Uri getUriFor(String name) { + public Uri getUriFor(@NonNull String name) { return Settings.Secure.getUriFor(name); } + @NonNull @Override public CoroutineDispatcher getBackgroundDispatcher() { return mBgDispatcher; } @Override - public String getStringForUser(String name, int userHandle) { + public String getStringForUser(@NonNull String name, int userHandle) { return Settings.Secure.getStringForUser(mContentResolver, name, getRealUserHandle(userHandle)); } @Override - public boolean putString(String name, String value, boolean overrideableByRestore) { + public boolean putString(@NonNull String name, String value, boolean overrideableByRestore) { return Settings.Secure.putString(mContentResolver, name, value, overrideableByRestore); } @Override - public boolean putStringForUser(String name, String value, int userHandle) { + public boolean putStringForUser(@NonNull String name, String value, int userHandle) { return Settings.Secure.putStringForUser(mContentResolver, name, value, getRealUserHandle(userHandle)); } @Override - public boolean putStringForUser(String name, String value, String tag, boolean makeDefault, - int userHandle, boolean overrideableByRestore) { + public boolean putStringForUser(@NonNull String name, String value, String tag, + boolean makeDefault, int userHandle, boolean overrideableByRestore) { return Settings.Secure.putStringForUser( mContentResolver, name, value, tag, makeDefault, getRealUserHandle(userHandle), overrideableByRestore); 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 0ee997e4549d..82f41a7fd154 100644 --- a/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.kt +++ b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.kt @@ -346,7 +346,7 @@ interface SettingsProxy { * @param value to associate with the name * @return true if the value was set, false on database errors */ - fun putString(name: String, value: String): Boolean + fun putString(name: String, value: String?): Boolean /** * Store a name/value pair into the database. @@ -377,7 +377,7 @@ interface SettingsProxy { * @return true if the value was set, false on database errors. * @see .resetToDefaults */ - fun putString(name: String, value: String, tag: String, makeDefault: Boolean): Boolean + fun putString(name: String, value: String?, tag: String?, makeDefault: Boolean): Boolean /** * Convenience function for retrieving a single secure settings value as an integer. Note that diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/SystemSettingsImpl.java b/packages/SystemUI/src/com/android/systemui/util/settings/SystemSettingsImpl.java index 1e8035734a36..e670b2c2edd0 100644 --- a/packages/SystemUI/src/com/android/systemui/util/settings/SystemSettingsImpl.java +++ b/packages/SystemUI/src/com/android/systemui/util/settings/SystemSettingsImpl.java @@ -16,12 +16,11 @@ package com.android.systemui.util.settings; +import android.annotation.NonNull; import android.content.ContentResolver; import android.net.Uri; import android.provider.Settings; -import androidx.annotation.NonNull; - import com.android.systemui.util.kotlin.SettingsSingleThreadBackground; import kotlinx.coroutines.CoroutineDispatcher; @@ -42,46 +41,50 @@ class SystemSettingsImpl implements SystemSettings { mBgCoroutineDispatcher = bgDispatcher; } + @NonNull @Override public ContentResolver getContentResolver() { return mContentResolver; } + @NonNull @Override public CurrentUserIdProvider getCurrentUserProvider() { return mCurrentUserProvider; } + @NonNull @Override - public Uri getUriFor(String name) { + public Uri getUriFor(@NonNull String name) { return Settings.System.getUriFor(name); } + @NonNull @Override public CoroutineDispatcher getBackgroundDispatcher() { return mBgCoroutineDispatcher; } @Override - public String getStringForUser(String name, int userHandle) { + public String getStringForUser(@NonNull String name, int userHandle) { return Settings.System.getStringForUser(mContentResolver, name, getRealUserHandle(userHandle)); } @Override - public boolean putString(String name, String value, boolean overrideableByRestore) { + public boolean putString(@NonNull String name, String value, boolean overrideableByRestore) { return Settings.System.putString(mContentResolver, name, value, overrideableByRestore); } @Override - public boolean putStringForUser(String name, String value, int userHandle) { + public boolean putStringForUser(@NonNull String name, String value, int userHandle) { return Settings.System.putStringForUser(mContentResolver, name, value, getRealUserHandle(userHandle)); } @Override - public boolean putStringForUser(String name, String value, String tag, boolean makeDefault, - int userHandle, boolean overrideableByRestore) { + public boolean putStringForUser(@NonNull String name, String value, String tag, + boolean makeDefault, int userHandle, boolean overrideableByRestore) { throw new UnsupportedOperationException( "This method only exists publicly for Settings.Secure and Settings.Global"); } diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt b/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt index 9ae8f03479cf..8e3b813a2a82 100644 --- a/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt +++ b/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt @@ -368,19 +368,19 @@ interface UserSettingsProxy : SettingsProxy { * @param value to associate with the name * @return true if the value was set, false on database errors */ - fun putString(name: String, value: String, overrideableByRestore: Boolean): Boolean + fun putString(name: String, value: String?, overrideableByRestore: Boolean): Boolean - override fun putString(name: String, value: String): Boolean { + override fun putString(name: String, value: String?): Boolean { return putStringForUser(name, value, userId) } /** Similar implementation to [putString] for the specified [userHandle]. */ - fun putStringForUser(name: String, value: String, userHandle: Int): Boolean + fun putStringForUser(name: String, value: String?, userHandle: Int): Boolean /** Similar implementation to [putString] for the specified [userHandle]. */ fun putStringForUser( name: String, - value: String, + value: String?, tag: String?, makeDefault: Boolean, @UserIdInt userHandle: Int, diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationModeSwitchTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationModeSwitchTest.java index 5be1180d3bdb..1ceac78af1a2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationModeSwitchTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationModeSwitchTest.java @@ -73,10 +73,14 @@ import android.widget.ImageView; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.app.viewcapture.ViewCapture; +import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.internal.graphics.SfVsyncFrameCallbackProvider; import com.android.systemui.SysuiTestCase; import com.android.systemui.res.R; +import kotlin.Lazy; + import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -104,6 +108,8 @@ public class MagnificationModeSwitchTest extends SysuiTestCase { private SfVsyncFrameCallbackProvider mSfVsyncFrameProvider; @Mock private MagnificationModeSwitch.ClickListener mClickListener; + @Mock + private Lazy<ViewCapture> mLazyViewCapture; private TestableWindowManager mWindowManager; private ViewPropertyAnimator mViewPropertyAnimator; private MagnificationModeSwitch mMagnificationModeSwitch; @@ -133,8 +139,10 @@ public class MagnificationModeSwitchTest extends SysuiTestCase { return null; }).when(mSfVsyncFrameProvider).postFrameCallback( any(Choreographer.FrameCallback.class)); + ViewCaptureAwareWindowManager vwm = new ViewCaptureAwareWindowManager(mWindowManager, + mLazyViewCapture, false); mMagnificationModeSwitch = new MagnificationModeSwitch(mContext, mSpyImageView, - mSfVsyncFrameProvider, mClickListener); + mSfVsyncFrameProvider, mClickListener, vwm); assertNotNull(mTouchListener); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/ModeSwitchesControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/ModeSwitchesControllerTest.java index 6e942979e0ed..e1e515eb31f5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/ModeSwitchesControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/ModeSwitchesControllerTest.java @@ -28,6 +28,7 @@ import android.view.View; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.systemui.SysuiTestCase; import org.junit.After; @@ -50,6 +51,8 @@ public class ModeSwitchesControllerTest extends SysuiTestCase { private View mSpyView; @Mock private MagnificationModeSwitch.ClickListener mListener; + @Mock + private ViewCaptureAwareWindowManager mViewCaptureAwareWindowManager; @Before @@ -58,7 +61,8 @@ public class ModeSwitchesControllerTest extends SysuiTestCase { mSupplier = new FakeSwitchSupplier(mContext.getSystemService(DisplayManager.class)); mModeSwitchesController = new ModeSwitchesController(mSupplier); mModeSwitchesController.setClickListenerDelegate(mListener); - mModeSwitch = Mockito.spy(new MagnificationModeSwitch(mContext, mModeSwitchesController)); + mModeSwitch = Mockito.spy(new MagnificationModeSwitch(mContext, mModeSwitchesController, + mViewCaptureAwareWindowManager)); mSpyView = Mockito.spy(new View(mContext)); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java b/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java index 5600b87280ad..a18d272b8fe3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java @@ -711,6 +711,16 @@ public class TouchMonitorTest extends SysuiTestCase { } @Test + public void testDestroy_cleansUpHandler() { + final TouchHandler touchHandler = createTouchHandler(); + + final Environment environment = new Environment(Stream.of(touchHandler) + .collect(Collectors.toCollection(HashSet::new)), mKosmos); + environment.destroyMonitor(); + verify(touchHandler).onDestroy(); + } + + @Test public void testLastSessionPop_createsNewInputSession() { final TouchHandler touchHandler = createTouchHandler(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt index 82465065c1e1..74bc9282eebb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt @@ -208,8 +208,8 @@ class DeviceItemActionInteractorTest : SysuiTestCase() { with(kosmos) { testScope.runTest { whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS) - whenever(cachedBluetoothDevice.connectableProfiles) - .thenReturn(listOf(leAudioProfile)) + whenever(cachedBluetoothDevice.uiAccessibleProfiles) + .thenReturn(listOf(leAudioProfile)) whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true) whenever(localBluetoothManager.profileManager).thenReturn(profileManager) @@ -243,8 +243,8 @@ class DeviceItemActionInteractorTest : SysuiTestCase() { testScope.runTest { whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS) whenever(cachedBluetoothDevice.device).thenReturn(bluetoothDevice) - whenever(cachedBluetoothDevice.connectableProfiles) - .thenReturn(listOf(leAudioProfile)) + whenever(cachedBluetoothDevice.uiAccessibleProfiles) + .thenReturn(listOf(leAudioProfile)) whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true) whenever(localBluetoothManager.profileManager).thenReturn(profileManager) @@ -254,12 +254,12 @@ class DeviceItemActionInteractorTest : SysuiTestCase() { whenever(BluetoothUtils.isBroadcasting(ArgumentMatchers.any())).thenReturn(true) whenever( - BluetoothUtils.hasConnectedBroadcastSource( - ArgumentMatchers.any(), - ArgumentMatchers.any() + BluetoothUtils.hasConnectedBroadcastSource( + ArgumentMatchers.any(), + ArgumentMatchers.any() + ) ) - ) - .thenReturn(false) + .thenReturn(false) actionInteractorImpl.onClick(connectedMediaDeviceItem, dialog) verify(activityStarter) diff --git a/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.kt index 20cb1e129f49..0ac04b61f13d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.kt @@ -28,6 +28,7 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.FlowValue import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.coroutines.collectValues import com.android.systemui.util.mockito.kotlinArgumentCaptor import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever @@ -456,8 +457,20 @@ class DisplayRepositoryTest : SysuiTestCase() { assertThat(value?.ids()).containsExactly(DEFAULT_DISPLAY) } + @Test + fun displayFlow_emitsCorrectDisplaysAtFirst() = + testScope.runTest { + setDisplays(0, 1, 2) + + val values: List<Set<Display>> by collectValues(displayRepository.displays) + + assertThat(values.toIdSets()).containsExactly(setOf(0, 1, 2)) + } + private fun Iterable<Display>.ids(): List<Int> = map { it.displayId } + private fun Iterable<Set<Display>>.toIdSets(): List<Set<Int>> = map { it.ids().toSet() } + // Wrapper to capture the displayListener. private fun TestScope.latestDisplayFlowValue(): FlowValue<Set<Display>?> { val flowValue = collectLastValue(displayRepository.displays) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt index e44bc7b43fb1..313292a5fab8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt @@ -102,34 +102,6 @@ class AlternateBouncerViewModelTest : SysuiTestCase() { } @Test - fun forcePluginOpen() = - testScope.runTest { - val forcePluginOpen by collectLastValue(underTest.forcePluginOpen) - - transitionRepository.sendTransitionSteps( - listOf( - stepToAlternateBouncer(0f, TransitionState.STARTED), - stepToAlternateBouncer(.4f), - stepToAlternateBouncer(.6f), - stepToAlternateBouncer(1f), - ), - testScope, - ) - assertThat(forcePluginOpen).isTrue() - - transitionRepository.sendTransitionSteps( - listOf( - stepFromAlternateBouncer(0f, TransitionState.STARTED), - stepFromAlternateBouncer(.3f), - stepFromAlternateBouncer(.6f), - stepFromAlternateBouncer(1f), - ), - testScope, - ) - assertThat(forcePluginOpen).isFalse() - } - - @Test fun registerForDismissGestures() = testScope.runTest { val registerForDismissGestures by collectLastValue(underTest.registerForDismissGestures) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt index 77977f3f1115..24bea2ce51c7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt @@ -195,9 +195,7 @@ class KeyguardQuickAffordancesCombinedViewModelTest : SysuiTestCase() { mSetFlagsRule.enableFlags(AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR) val featureFlags = - FakeFeatureFlags().apply { - set(Flags.LOCK_SCREEN_LONG_PRESS_ENABLED, false) - } + FakeFeatureFlags().apply { set(Flags.LOCK_SCREEN_LONG_PRESS_ENABLED, false) } val withDeps = KeyguardInteractorFactory.create(featureFlags = featureFlags) keyguardInteractor = withDeps.keyguardInteractor @@ -289,6 +287,7 @@ class KeyguardQuickAffordancesCombinedViewModelTest : SysuiTestCase() { underTest = KeyguardQuickAffordancesCombinedViewModel( + applicationScope = testScope.backgroundScope, quickAffordanceInteractor = KeyguardQuickAffordanceInteractor( keyguardInteractor = keyguardInteractor, diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/permission/ShareToAppPermissionDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/permission/ShareToAppPermissionDialogDelegateTest.kt index c57aa369490b..04ef1be9c057 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/permission/ShareToAppPermissionDialogDelegateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/permission/ShareToAppPermissionDialogDelegateTest.kt @@ -29,6 +29,7 @@ import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger import com.android.systemui.res.R import com.android.systemui.statusbar.phone.AlertDialogWithDelegate import com.android.systemui.statusbar.phone.SystemUIDialog +import com.google.common.truth.Truth.assertThat import kotlin.test.assertEquals import org.junit.After import org.junit.Test @@ -44,8 +45,10 @@ class ShareToAppPermissionDialogDelegateTest : SysuiTestCase() { private val appName = "Test App" - private val resIdSingleApp = R.string.screen_share_permission_dialog_option_single_app - private val resIdFullScreen = R.string.screen_share_permission_dialog_option_entire_screen + private val resIdSingleApp = + R.string.media_projection_entry_app_permission_dialog_option_text_single_app + private val resIdFullScreen = + R.string.media_projection_entry_app_permission_dialog_option_text_entire_screen private val resIdSingleAppDisabled = R.string.media_projection_entry_app_permission_dialog_single_app_disabled @@ -115,6 +118,36 @@ class ShareToAppPermissionDialogDelegateTest : SysuiTestCase() { assertEquals(context.getString(resIdFullScreen), secondOptionText) } + @Test + fun startButtonText_entireScreenSelected() { + setUpAndShowDialog() + onSpinnerItemSelected(ENTIRE_SCREEN) + + val startButtonText = dialog.requireViewById<TextView>(android.R.id.button1).text + + assertThat(startButtonText) + .isEqualTo( + context.getString( + R.string.media_projection_entry_app_permission_dialog_continue_entire_screen + ) + ) + } + + @Test + fun startButtonText_singleAppSelected() { + setUpAndShowDialog() + onSpinnerItemSelected(SINGLE_APP) + + val startButtonText = dialog.requireViewById<TextView>(android.R.id.button1).text + + assertThat(startButtonText) + .isEqualTo( + context.getString( + R.string.media_projection_entry_generic_permission_dialog_continue_single_app + ) + ) + } + private fun setUpAndShowDialog( mediaProjectionConfig: MediaProjectionConfig? = null, overrideDisableSingleAppOption: Boolean = false, @@ -142,4 +175,10 @@ class ShareToAppPermissionDialogDelegateTest : SysuiTestCase() { delegate.onCreate(dialog, savedInstanceState = null) dialog.show() } + + private fun onSpinnerItemSelected(position: Int) { + val spinner = dialog.requireViewById<Spinner>(R.id.screen_share_mode_options) + checkNotNull(spinner.onItemSelectedListener) + .onItemSelected(spinner, mock(), position, /* id= */ 0) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java index a8cbbd4178bd..a52ab0c690a4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java @@ -20,7 +20,6 @@ import static android.app.StatusBarManager.NAVIGATION_HINT_BACK_ALT; import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SHOWN; import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_SHOWN; import static android.inputmethodservice.InputMethodService.BACK_DISPOSITION_DEFAULT; -import static android.inputmethodservice.InputMethodService.IME_INVISIBLE; import static android.inputmethodservice.InputMethodService.IME_VISIBLE; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS; @@ -512,7 +511,7 @@ public class NavigationBarTest extends SysuiTestCase { externalNavBar.setImeWindowStatus(EXTERNAL_DISPLAY_ID, IME_VISIBLE, BACK_DISPOSITION_DEFAULT, true); - defaultNavBar.setImeWindowStatus(DEFAULT_DISPLAY, IME_INVISIBLE, + defaultNavBar.setImeWindowStatus(DEFAULT_DISPLAY, 0 /* vis */, BACK_DISPOSITION_DEFAULT, false); // Verify IME window state will be updated in external NavBar & default NavBar state reset. assertEquals(NAVIGATION_HINT_BACK_ALT | NAVIGATION_HINT_IME_SHOWN diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java index 79c206c1a838..3f550ca27868 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java @@ -16,8 +16,13 @@ package com.android.systemui.screenrecord; +import static com.android.systemui.screenrecord.RecordingService.GROUP_KEY_ERROR_SAVING; +import static com.android.systemui.screenrecord.RecordingService.GROUP_KEY_SAVED; + import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; @@ -25,6 +30,7 @@ import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -73,8 +79,6 @@ public class RecordingServiceTest extends SysuiTestCase { @Mock private ScreenMediaRecorder mScreenMediaRecorder; @Mock - private Notification mNotification; - @Mock private Executor mExecutor; @Mock private Handler mHandler; @@ -124,10 +128,6 @@ public class RecordingServiceTest extends SysuiTestCase { // Mock notifications doNothing().when(mRecordingService).createRecordingNotification(); - doReturn(mNotification).when(mRecordingService).createProcessingNotification(); - doReturn(mNotification).when(mRecordingService).createSaveNotification(any()); - doNothing().when(mRecordingService).createErrorStartingNotification(); - doNothing().when(mRecordingService).createErrorSavingNotification(); doNothing().when(mRecordingService).showErrorToast(anyInt()); doNothing().when(mRecordingService).stopForeground(anyInt()); @@ -228,6 +228,33 @@ public class RecordingServiceTest extends SysuiTestCase { } @Test + public void testOnSystemRequestedStop_whenRecordingInProgress_showsNotifications() { + doReturn(true).when(mController).isRecording(); + + mRecordingService.onStopped(); + + // Processing notification + ArgumentCaptor<Notification> notifCaptor = ArgumentCaptor.forClass(Notification.class); + verify(mNotificationManager).notifyAsUser(any(), anyInt(), notifCaptor.capture(), any()); + assertEquals(GROUP_KEY_SAVED, notifCaptor.getValue().getGroup()); + + reset(mNotificationManager); + verify(mExecutor).execute(mRunnableCaptor.capture()); + mRunnableCaptor.getValue().run(); + + verify(mNotificationManager, times(2)) + .notifyAsUser(any(), anyInt(), notifCaptor.capture(), any()); + // Saved notification + Notification saveNotification = notifCaptor.getAllValues().get(0); + assertFalse(saveNotification.isGroupSummary()); + assertEquals(GROUP_KEY_SAVED, saveNotification.getGroup()); + // Group summary notification + Notification groupSummaryNotification = notifCaptor.getAllValues().get(1); + assertTrue(groupSummaryNotification.isGroupSummary()); + assertEquals(GROUP_KEY_SAVED, groupSummaryNotification.getGroup()); + } + + @Test public void testOnSystemRequestedStop_recorderEndThrowsRuntimeException_showsErrorNotification() throws IOException { doReturn(true).when(mController).isRecording(); @@ -235,7 +262,11 @@ public class RecordingServiceTest extends SysuiTestCase { mRecordingService.onStopped(); - verify(mRecordingService).createErrorSavingNotification(); + verify(mRecordingService).createErrorSavingNotification(any()); + ArgumentCaptor<Notification> notifCaptor = ArgumentCaptor.forClass(Notification.class); + verify(mNotificationManager).notifyAsUser(any(), anyInt(), notifCaptor.capture(), any()); + assertTrue(notifCaptor.getValue().isGroupSummary()); + assertEquals(GROUP_KEY_ERROR_SAVING, notifCaptor.getValue().getGroup()); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java index 86d21e8081e5..6916bbde0153 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java @@ -16,7 +16,6 @@ package com.android.systemui.statusbar; import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE; import static android.inputmethodservice.InputMethodService.BACK_DISPOSITION_DEFAULT; -import static android.inputmethodservice.InputMethodService.IME_INVISIBLE; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.WindowInsetsController.BEHAVIOR_DEFAULT; @@ -207,7 +206,7 @@ public class CommandQueueTest extends SysuiTestCase { mCommandQueue.setImeWindowStatus(SECONDARY_DISPLAY, 1, 2, true); waitForIdleSync(); - verify(mCallbacks).setImeWindowStatus(eq(DEFAULT_DISPLAY), eq(IME_INVISIBLE), + verify(mCallbacks).setImeWindowStatus(eq(DEFAULT_DISPLAY), eq(0), eq(BACK_DISPOSITION_DEFAULT), eq(false)); verify(mCallbacks).setImeWindowStatus(eq(SECONDARY_DISPLAY), eq(1), eq(2), eq(true)); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java index 80011dcab1cd..a75d7b23a92c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java @@ -25,6 +25,7 @@ import static android.hardware.biometrics.BiometricFaceConstants.FACE_ERROR_TIME import static com.android.keyguard.KeyguardUpdateMonitor.BIOMETRIC_HELP_FACE_NOT_AVAILABLE; import static com.android.keyguard.KeyguardUpdateMonitor.BIOMETRIC_HELP_FACE_NOT_RECOGNIZED; import static com.android.keyguard.KeyguardUpdateMonitor.BIOMETRIC_HELP_FINGERPRINT_NOT_RECOGNIZED; +import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_ADAPTIVE_AUTH; import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_ALIGNMENT; import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_BATTERY; import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_BIOMETRIC_MESSAGE; @@ -1535,6 +1536,48 @@ public class KeyguardIndicationControllerTest extends KeyguardIndicationControll trustGrantedMsg); } + @Test + public void updateAdaptiveAuthMessage_whenNotLockedByAdaptiveAuth_doesNotShowMsg() { + // When the device is not locked by adaptive auth + when(mKeyguardUpdateMonitor.isDeviceLockedByAdaptiveAuth(getCurrentUser())) + .thenReturn(false); + createController(); + mController.setVisible(true); + + // Verify that the adaptive auth message does not show + verifyNoMessage(INDICATION_TYPE_ADAPTIVE_AUTH); + } + + @Test + public void updateAdaptiveAuthMessage_whenLockedByAdaptiveAuth_cannotSkipBouncer_showsMsg() { + // When the device is locked by adaptive auth, and the user cannot skip bouncer + when(mKeyguardUpdateMonitor.isDeviceLockedByAdaptiveAuth(getCurrentUser())) + .thenReturn(true); + when(mKeyguardUpdateMonitor.getUserCanSkipBouncer(getCurrentUser())).thenReturn(false); + createController(); + mController.setVisible(true); + + // Verify that the adaptive auth message shows + String message = mContext.getString(R.string.keyguard_indication_after_adaptive_auth_lock); + verifyIndicationMessage(INDICATION_TYPE_ADAPTIVE_AUTH, message); + } + + @Test + public void updateAdaptiveAuthMessage_whenLockedByAdaptiveAuth_canSkipBouncer_doesNotShowMsg() { + createController(); + mController.setVisible(true); + + // When the device is locked by adaptive auth, but the device unlocked state changes and the + // user can skip bouncer + when(mKeyguardUpdateMonitor.isDeviceLockedByAdaptiveAuth(getCurrentUser())) + .thenReturn(true); + when(mKeyguardUpdateMonitor.getUserCanSkipBouncer(getCurrentUser())).thenReturn(true); + mKeyguardStateControllerCallback.onUnlockedChanged(); + + // Verify that the adaptive auth message does not show + verifyNoMessage(INDICATION_TYPE_ADAPTIVE_AUTH); + } + private void screenIsTurningOn() { when(mScreenLifecycle.getScreenState()).thenReturn(SCREEN_TURNING_ON); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt index 3f28164709fd..491919a16a4e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt @@ -44,6 +44,7 @@ import com.android.systemui.statusbar.notification.row.NotificationRowContentBin import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationCallback import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag import com.android.systemui.statusbar.notification.row.shared.HeadsUpStatusBarModel +import com.android.systemui.statusbar.notification.row.shared.LockscreenOtpRedaction import com.android.systemui.statusbar.notification.row.shared.NewRemoteViews import com.android.systemui.statusbar.notification.row.shared.NotificationContentModel import com.android.systemui.statusbar.notification.row.shared.NotificationRowContentBinderRefactor @@ -78,7 +79,7 @@ import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidJUnit4::class) @RunWithLooper -@EnableFlags(NotificationRowContentBinderRefactor.FLAG_NAME) +@EnableFlags(NotificationRowContentBinderRefactor.FLAG_NAME, LockscreenOtpRedaction.FLAG_NAME) class NotificationRowContentBinderImplTest : SysuiTestCase() { private lateinit var notificationInflater: NotificationRowContentBinderImpl private lateinit var builder: Notification.Builder 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 b0acd0386870..2e6d0fc847bb 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 @@ -385,7 +385,7 @@ class SettingsProxyTest : SysuiTestCase() { private class FakeSettingsProxy(val testDispatcher: CoroutineDispatcher) : SettingsProxy { private val mContentResolver = mock(ContentResolver::class.java) - private val settingToValueMap: MutableMap<String, String> = mutableMapOf() + private val settingToValueMap: MutableMap<String, String?> = mutableMapOf() override fun getContentResolver() = mContentResolver @@ -399,15 +399,15 @@ class SettingsProxyTest : SysuiTestCase() { return settingToValueMap[name] ?: "" } - override fun putString(name: String, value: String): Boolean { + override fun putString(name: String, value: String?): Boolean { settingToValueMap[name] = value return true } override fun putString( name: String, - value: String, - tag: String, + value: String?, + tag: String?, makeDefault: Boolean ): Boolean { settingToValueMap[name] = value diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/settings/UserSettingsProxyTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/settings/UserSettingsProxyTest.kt index eaeece9c293e..00b8cd04bdaf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/util/settings/UserSettingsProxyTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/util/settings/UserSettingsProxyTest.kt @@ -561,7 +561,7 @@ class UserSettingsProxyTest : SysuiTestCase() { ) : UserSettingsProxy { private val mContentResolver = mock(ContentResolver::class.java) - private val userIdToSettingsValueMap: MutableMap<Int, MutableMap<String, String>> = + private val userIdToSettingsValueMap: MutableMap<Int, MutableMap<String, String?>> = mutableMapOf() override fun getContentResolver() = mContentResolver @@ -577,7 +577,7 @@ class UserSettingsProxyTest : SysuiTestCase() { override fun putString( name: String, - value: String, + value: String?, overrideableByRestore: Boolean ): Boolean { userIdToSettingsValueMap[DEFAULT_USER_ID]?.put(name, value) @@ -586,22 +586,22 @@ class UserSettingsProxyTest : SysuiTestCase() { override fun putString( name: String, - value: String, - tag: String, + value: String?, + tag: String?, makeDefault: Boolean ): Boolean { putStringForUser(name, value, DEFAULT_USER_ID) return true } - override fun putStringForUser(name: String, value: String, userHandle: Int): Boolean { + override fun putStringForUser(name: String, value: String?, userHandle: Int): Boolean { userIdToSettingsValueMap[userHandle] = mutableMapOf(Pair(name, value)) return true } override fun putStringForUser( name: String, - value: String, + value: String?, tag: String?, makeDefault: Boolean, userHandle: Int, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryKosmos.kt index aac5e57207a7..1d2bce2f9b99 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryKosmos.kt @@ -17,10 +17,9 @@ package com.android.systemui.education.data.repository import com.android.systemui.kosmos.Kosmos -import java.time.Clock import java.time.Instant var Kosmos.contextualEducationRepository: ContextualEducationRepository by Kosmos.Fixture { FakeContextualEducationRepository() } -var Kosmos.fakeEduClock: Clock by Kosmos.Fixture { FakeEduClock(Instant.MIN) } +var Kosmos.fakeEduClock: FakeEduClock by Kosmos.Fixture { FakeEduClock(Instant.MIN) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/FakeEduClock.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/FakeEduClock.kt index 513c14381997..c9a5d4bffef2 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/FakeEduClock.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/FakeEduClock.kt @@ -19,8 +19,9 @@ package com.android.systemui.education.data.repository import java.time.Clock import java.time.Instant import java.time.ZoneId +import kotlin.time.Duration -class FakeEduClock(private val base: Instant) : Clock() { +class FakeEduClock(private var base: Instant) : Clock() { private val zone: ZoneId = ZoneId.of("UTC") override fun instant(): Instant { @@ -34,4 +35,8 @@ class FakeEduClock(private val base: Instant) : Clock() { override fun getZone(): ZoneId { return zone } + + fun offset(duration: Duration) { + base = base.plusSeconds(duration.inWholeSeconds) + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt index fb4e9012f79d..5088677161d8 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt @@ -16,6 +16,7 @@ package com.android.systemui.education.domain.interactor +import com.android.systemui.education.data.repository.fakeEduClock import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope @@ -23,7 +24,8 @@ var Kosmos.keyboardTouchpadEduInteractor by Kosmos.Fixture { KeyboardTouchpadEduInteractor( backgroundScope = testScope.backgroundScope, - contextualEducationInteractor = contextualEducationInteractor + contextualEducationInteractor = contextualEducationInteractor, + clock = fakeEduClock ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt index 727de9e95872..4571c19d101a 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt @@ -74,6 +74,8 @@ class FakeKeyguardRepository @Inject constructor() : KeyguardRepository { private val _dozeTimeTick = MutableStateFlow<Long>(0L) override val dozeTimeTick = _dozeTimeTick + override val showDismissibleKeyguard = MutableStateFlow<Long>(0L) + private val _lastDozeTapToWakePosition = MutableStateFlow<Point?>(null) override val lastDozeTapToWakePosition = _lastDozeTapToWakePosition.asStateFlow() @@ -206,6 +208,10 @@ class FakeKeyguardRepository @Inject constructor() : KeyguardRepository { _dozeTimeTick.value = millis } + override fun showDismissibleKeyguard() { + showDismissibleKeyguard.value = showDismissibleKeyguard.value + 1 + } + override fun setLastDozeTapToWakePosition(position: Point) { _lastDozeTapToWakePosition.value = position } @@ -216,6 +222,9 @@ class FakeKeyguardRepository @Inject constructor() : KeyguardRepository { override fun setDreaming(isDreaming: Boolean) { _isDreaming.value = isDreaming + // Intentionally set both for testing, to avoid races with merge() in the interactor that + // would make testing difficult + _isDreamingWithOverlay.value = isDreaming } fun setDreamingWithOverlay(isDreaming: Boolean) { diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt index 82860fc52045..b9443bcaf650 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt @@ -39,6 +39,7 @@ val Kosmos.keyguardRootViewModel by Fixture { communalInteractor = communalInteractor, keyguardTransitionInteractor = keyguardTransitionInteractor, notificationsKeyguardInteractor = notificationsKeyguardInteractor, + alternateBouncerToAodTransitionViewModel = alternateBouncerToAodTransitionViewModel, alternateBouncerToGoneTransitionViewModel = alternateBouncerToGoneTransitionViewModel, alternateBouncerToLockscreenTransitionViewModel = alternateBouncerToLockscreenTransitionViewModel, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneActionsViewModelKosmos.kt index 299b22ef963f..5d70ed6a634c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneActionsViewModelKosmos.kt @@ -18,13 +18,11 @@ package com.android.systemui.qs.ui.viewmodel import com.android.systemui.kosmos.Kosmos import com.android.systemui.shade.domain.interactor.shadeInteractor -import com.android.systemui.shade.ui.viewmodel.overlayShadeViewModel -val Kosmos.quickSettingsShadeSceneViewModel: QuickSettingsShadeSceneViewModel by +val Kosmos.quickSettingsShadeSceneActionsViewModel: QuickSettingsShadeSceneActionsViewModel by Kosmos.Fixture { - QuickSettingsShadeSceneViewModel( + QuickSettingsShadeSceneActionsViewModel( shadeInteractor = shadeInteractor, - overlayShadeViewModel = overlayShadeViewModel, quickSettingsContainerViewModel = quickSettingsContainerViewModel, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneContentViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneContentViewModelKosmos.kt new file mode 100644 index 000000000000..5ad5cb28e549 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneContentViewModelKosmos.kt @@ -0,0 +1,31 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.android.systemui.qs.ui.viewmodel + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.shade.ui.viewmodel.overlayShadeViewModelFactory +import kotlinx.coroutines.ExperimentalCoroutinesApi + +val Kosmos.quickSettingsShadeSceneContentViewModel: QuickSettingsShadeSceneContentViewModel by + Kosmos.Fixture { + QuickSettingsShadeSceneContentViewModel( + overlayShadeViewModelFactory = overlayShadeViewModelFactory, + quickSettingsContainerViewModel = quickSettingsContainerViewModel, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/brightness/ui/viewmodel/BrightnessMirrorViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/brightness/ui/viewmodel/BrightnessMirrorViewModelKosmos.kt index 8fb370caee09..32a561474b4d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/brightness/ui/viewmodel/BrightnessMirrorViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/brightness/ui/viewmodel/BrightnessMirrorViewModelKosmos.kt @@ -18,15 +18,23 @@ package com.android.systemui.settings.brightness.ui.viewmodel import android.content.res.mainResources import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.settings.brightness.domain.interactor.brightnessMirrorShowingInteractor import com.android.systemui.settings.brightness.ui.viewModel.BrightnessMirrorViewModel import com.android.systemui.settings.brightnessSliderControllerFactory -val Kosmos.brightnessMirrorViewModel by - Kosmos.Fixture { - BrightnessMirrorViewModel( - brightnessMirrorShowingInteractor, - mainResources, - brightnessSliderControllerFactory, - ) +val Kosmos.brightnessMirrorViewModel by Fixture { + BrightnessMirrorViewModel( + brightnessMirrorShowingInteractor, + mainResources, + brightnessSliderControllerFactory, + ) +} + +val Kosmos.brightnessMirrorViewModelFactory by Fixture { + object : BrightnessMirrorViewModel.Factory { + override fun create(): BrightnessMirrorViewModel { + return brightnessMirrorViewModel + } } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneActionsViewModelKosmos.kt index 72a80d480288..9bf4756f53b0 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneActionsViewModelKosmos.kt @@ -17,8 +17,13 @@ package com.android.systemui.shade.ui.viewmodel import com.android.systemui.kosmos.Kosmos -import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeSceneViewModel +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeSceneActionsViewModel import com.android.systemui.shade.domain.interactor.shadeInteractor -val Kosmos.notificationsShadeSceneViewModel: NotificationsShadeSceneViewModel by - Kosmos.Fixture { NotificationsShadeSceneViewModel(shadeInteractor) } +val Kosmos.notificationsShadeSceneActionsViewModel: + NotificationsShadeSceneActionsViewModel by Fixture { + NotificationsShadeSceneActionsViewModel( + shadeInteractor = shadeInteractor, + ) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneContentViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneContentViewModelKosmos.kt new file mode 100644 index 000000000000..92401024c91f --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneContentViewModelKosmos.kt @@ -0,0 +1,34 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.android.systemui.shade.ui.viewmodel + +import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeSceneContentViewModel +import com.android.systemui.scene.domain.interactor.sceneInteractor +import kotlinx.coroutines.ExperimentalCoroutinesApi + +val Kosmos.notificationsShadeSceneContentViewModel: + NotificationsShadeSceneContentViewModel by Fixture { + NotificationsShadeSceneContentViewModel( + deviceEntryInteractor = deviceEntryInteractor, + sceneInteractor = sceneInteractor, + ) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModelKosmos.kt index 8d4d54749086..00f1526f6cd4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModelKosmos.kt @@ -17,15 +17,22 @@ package com.android.systemui.shade.ui.viewmodel import com.android.systemui.kosmos.Kosmos -import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.shade.domain.interactor.shadeInteractor val Kosmos.overlayShadeViewModel: OverlayShadeViewModel by Kosmos.Fixture { OverlayShadeViewModel( - applicationScope = applicationCoroutineScope, sceneInteractor = sceneInteractor, shadeInteractor = shadeInteractor, ) } + +val Kosmos.overlayShadeViewModelFactory: OverlayShadeViewModel.Factory by + Kosmos.Fixture { + object : OverlayShadeViewModel.Factory { + override fun create(): OverlayShadeViewModel { + return overlayShadeViewModel + } + } + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt index 0e21698ef271..7eb9f3472482 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt @@ -19,7 +19,6 @@ package com.android.systemui.shade.ui.viewmodel import android.content.applicationContext import com.android.systemui.broadcast.broadcastDispatcher import com.android.systemui.kosmos.Kosmos -import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.plugins.activityStarter import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.shade.domain.interactor.privacyChipInteractor @@ -31,7 +30,6 @@ import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.mobileIconsVi val Kosmos.shadeHeaderViewModel: ShadeHeaderViewModel by Kosmos.Fixture { ShadeHeaderViewModel( - applicationScope = applicationCoroutineScope, context = applicationContext, activityStarter = activityStarter, sceneInteractor = sceneInteractor, @@ -43,3 +41,12 @@ val Kosmos.shadeHeaderViewModel: ShadeHeaderViewModel by broadcastDispatcher = broadcastDispatcher, ) } + +val Kosmos.shadeHeaderViewModelFactory: ShadeHeaderViewModel.Factory by + Kosmos.Fixture { + object : ShadeHeaderViewModel.Factory { + override fun create(): ShadeHeaderViewModel { + return shadeHeaderViewModel + } + } + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneActionsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneActionsViewModelKosmos.kt new file mode 100644 index 000000000000..2387aa856fe6 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneActionsViewModelKosmos.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2023 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.shade.ui.viewmodel + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.qs.ui.adapter.qsSceneAdapter +import com.android.systemui.shade.domain.interactor.shadeInteractor + +val Kosmos.shadeSceneActionsViewModel: ShadeSceneActionsViewModel by Fixture { + ShadeSceneActionsViewModel( + qsSceneAdapter = qsSceneAdapter, + shadeInteractor = shadeInteractor, + ) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneContentViewModelKosmos.kt index 2c5a0f4d31bc..7097d3130aa0 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneContentViewModelKosmos.kt @@ -16,27 +16,29 @@ package com.android.systemui.shade.ui.viewmodel +import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.media.controls.domain.pipeline.interactor.mediaCarouselInteractor import com.android.systemui.qs.footerActionsController import com.android.systemui.qs.footerActionsViewModelFactory import com.android.systemui.qs.ui.adapter.qsSceneAdapter import com.android.systemui.scene.domain.interactor.sceneInteractor -import com.android.systemui.settings.brightness.ui.viewmodel.brightnessMirrorViewModel +import com.android.systemui.settings.brightness.ui.viewmodel.brightnessMirrorViewModelFactory import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.unfold.domain.interactor.unfoldTransitionInteractor -val Kosmos.shadeSceneViewModel: ShadeSceneViewModel by - Kosmos.Fixture { - ShadeSceneViewModel( - shadeHeaderViewModel = shadeHeaderViewModel, - qsSceneAdapter = qsSceneAdapter, - brightnessMirrorViewModel = brightnessMirrorViewModel, - mediaCarouselInteractor = mediaCarouselInteractor, - shadeInteractor = shadeInteractor, - footerActionsViewModelFactory = footerActionsViewModelFactory, - footerActionsController = footerActionsController, - sceneInteractor = sceneInteractor, - unfoldTransitionInteractor = unfoldTransitionInteractor, - ) - } +val Kosmos.shadeSceneContentViewModel: ShadeSceneContentViewModel by Fixture { + ShadeSceneContentViewModel( + shadeHeaderViewModelFactory = shadeHeaderViewModelFactory, + qsSceneAdapter = qsSceneAdapter, + brightnessMirrorViewModelFactory = brightnessMirrorViewModelFactory, + mediaCarouselInteractor = mediaCarouselInteractor, + shadeInteractor = shadeInteractor, + footerActionsViewModelFactory = footerActionsViewModelFactory, + footerActionsController = footerActionsController, + unfoldTransitionInteractor = unfoldTransitionInteractor, + deviceEntryInteractor = deviceEntryInteractor, + sceneInteractor = sceneInteractor, + ) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt index afb8acb3d819..20dc668e4ff6 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt @@ -21,7 +21,6 @@ import com.android.systemui.flags.featureFlagsClassic import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.shade.domain.interactor.shadeInteractor -import com.android.systemui.shade.ui.viewmodel.shadeSceneViewModel import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackAppearanceInteractor @@ -30,7 +29,6 @@ val Kosmos.notificationsPlaceholderViewModel by Fixture { dumpManager = dumpManager, interactor = notificationStackAppearanceInteractor, shadeInteractor = shadeInteractor, - shadeSceneViewModel = shadeSceneViewModel, headsUpNotificationInteractor = headsUpNotificationInteractor, featureFlags = featureFlagsClassic, ) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettings.java b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettings.java deleted file mode 100644 index d1174667648c..000000000000 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettings.java +++ /dev/null @@ -1,198 +0,0 @@ -/* - * Copyright (C) 2020 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.util.settings; - -import static kotlinx.coroutines.test.TestCoroutineDispatchersKt.StandardTestDispatcher; - -import android.annotation.UserIdInt; -import android.content.ContentResolver; -import android.database.ContentObserver; -import android.net.Uri; -import android.os.UserHandle; -import android.util.Pair; - -import androidx.annotation.NonNull; -import androidx.annotation.VisibleForTesting; - -import kotlinx.coroutines.CoroutineDispatcher; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public class FakeSettings implements SecureSettings, SystemSettings { - private final Map<SettingsKey, String> mValues = new HashMap<>(); - private final Map<SettingsKey, List<ContentObserver>> mContentObservers = - new HashMap<>(); - private final Map<String, List<ContentObserver>> mContentObserversAllUsers = new HashMap<>(); - private final CoroutineDispatcher mDispatcher; - - public static final Uri CONTENT_URI = Uri.parse("content://settings/fake"); - @UserIdInt - private int mUserId = UserHandle.USER_CURRENT; - - private final CurrentUserIdProvider mCurrentUserProvider; - - /** - * @deprecated Please use FakeSettings(testDispatcher) to provide the same dispatcher used - * by main test scope. - */ - @Deprecated - public FakeSettings() { - mDispatcher = StandardTestDispatcher(/* scheduler = */ null, /* name = */ null); - mCurrentUserProvider = () -> mUserId; - } - - public FakeSettings(CoroutineDispatcher dispatcher) { - mDispatcher = dispatcher; - mCurrentUserProvider = () -> mUserId; - } - - public FakeSettings(CoroutineDispatcher dispatcher, CurrentUserIdProvider currentUserProvider) { - mDispatcher = dispatcher; - mCurrentUserProvider = currentUserProvider; - } - - @VisibleForTesting - FakeSettings(String initialKey, String initialValue) { - this(); - putString(initialKey, initialValue); - } - - @VisibleForTesting - FakeSettings(Map<String, String> initialValues) { - this(); - for (Map.Entry<String, String> kv : initialValues.entrySet()) { - putString(kv.getKey(), kv.getValue()); - } - } - - @Override - @NonNull - public ContentResolver getContentResolver() { - throw new UnsupportedOperationException( - "FakeSettings.getContentResolver is not implemented"); - } - - @NonNull - @Override - public CurrentUserIdProvider getCurrentUserProvider() { - return mCurrentUserProvider; - } - - @NonNull - @Override - public CoroutineDispatcher getBackgroundDispatcher() { - return mDispatcher; - } - - @Override - public void registerContentObserverForUserSync(@NonNull Uri uri, boolean notifyDescendants, - @NonNull ContentObserver settingsObserver, int userHandle) { - List<ContentObserver> observers; - if (userHandle == UserHandle.USER_ALL) { - mContentObserversAllUsers.putIfAbsent(uri.toString(), new ArrayList<>()); - observers = mContentObserversAllUsers.get(uri.toString()); - } else { - SettingsKey key = new SettingsKey(userHandle, uri.toString()); - mContentObservers.putIfAbsent(key, new ArrayList<>()); - observers = mContentObservers.get(key); - } - observers.add(settingsObserver); - } - - @Override - public void unregisterContentObserverSync(@NonNull ContentObserver settingsObserver) { - for (List<ContentObserver> observers : mContentObservers.values()) { - observers.remove(settingsObserver); - } - for (List<ContentObserver> observers : mContentObserversAllUsers.values()) { - observers.remove(settingsObserver); - } - } - - @NonNull - @Override - public Uri getUriFor(@NonNull String name) { - return Uri.withAppendedPath(CONTENT_URI, name); - } - - public void setUserId(@UserIdInt int userId) { - mUserId = userId; - } - - @Override - public int getUserId() { - return mUserId; - } - - @Override - public String getString(@NonNull String name) { - return getStringForUser(name, getUserId()); - } - - @Override - public String getStringForUser(@NonNull String name, int userHandle) { - return mValues.get(new SettingsKey(userHandle, getUriFor(name).toString())); - } - - @Override - public boolean putString(@NonNull String name, @NonNull String value, - boolean overrideableByRestore) { - return putStringForUser(name, value, null, false, getUserId(), overrideableByRestore); - } - - @Override - public boolean putString(@NonNull String name, @NonNull String value) { - return putString(name, value, false); - } - - @Override - public boolean putStringForUser(@NonNull String name, @NonNull String value, int userHandle) { - return putStringForUser(name, value, null, false, userHandle, false); - } - - @Override - public boolean putStringForUser(@NonNull String name, @NonNull String value, String tag, - boolean makeDefault, int userHandle, boolean overrideableByRestore) { - SettingsKey key = new SettingsKey(userHandle, getUriFor(name).toString()); - mValues.put(key, value); - - Uri uri = getUriFor(name); - for (ContentObserver observer : mContentObservers.getOrDefault(key, new ArrayList<>())) { - observer.dispatchChange(false, List.of(uri), 0, userHandle); - } - for (ContentObserver observer : - mContentObserversAllUsers.getOrDefault(uri.toString(), new ArrayList<>())) { - observer.dispatchChange(false, List.of(uri), 0, userHandle); - } - return true; - } - - @Override - public boolean putString(@NonNull String name, @NonNull String value, @NonNull String tag, - boolean makeDefault) { - return putString(name, value); - } - - private static class SettingsKey extends Pair<Integer, String> { - SettingsKey(Integer first, String second) { - super(first, second); - } - } -} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettings.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettings.kt new file mode 100644 index 000000000000..e5d113be7ca2 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettings.kt @@ -0,0 +1,307 @@ +/* + * Copyright (C) 2020 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.util.settings + +import android.annotation.UserIdInt +import android.content.ContentResolver +import android.database.ContentObserver +import android.net.Uri +import android.os.UserHandle +import android.util.Pair +import androidx.annotation.VisibleForTesting +import com.android.systemui.util.settings.SettingsProxy.CurrentUserIdProvider +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Job +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestDispatcher + +class FakeSettings : SecureSettings, SystemSettings, UserSettingsProxy { + private val values = mutableMapOf<SettingsKey, String?>() + private val contentObservers = mutableMapOf<SettingsKey, MutableList<ContentObserver>>() + private val contentObserversAllUsers = mutableMapOf<String, MutableList<ContentObserver>>() + + override val backgroundDispatcher: CoroutineDispatcher + + @UserIdInt override var userId = UserHandle.USER_CURRENT + override val currentUserProvider: CurrentUserIdProvider + + @Deprecated( + """Please use FakeSettings(testDispatcher) to provide the same dispatcher used + by main test scope.""" + ) + constructor() { + backgroundDispatcher = StandardTestDispatcher(scheduler = null, name = null) + currentUserProvider = CurrentUserIdProvider { userId } + } + + constructor(dispatcher: CoroutineDispatcher) { + backgroundDispatcher = dispatcher + currentUserProvider = CurrentUserIdProvider { userId } + } + + constructor(dispatcher: CoroutineDispatcher, currentUserProvider: CurrentUserIdProvider) { + backgroundDispatcher = dispatcher + this.currentUserProvider = currentUserProvider + } + + @VisibleForTesting + internal constructor(initialKey: String, initialValue: String) : this() { + putString(initialKey, initialValue) + } + + @VisibleForTesting + internal constructor(initialValues: Map<String, String>) : this() { + for ((key, value) in initialValues) { + putString(key, value) + } + } + + override fun getContentResolver(): ContentResolver { + throw UnsupportedOperationException("FakeSettings.getContentResolver is not implemented") + } + + override fun registerContentObserverForUserSync( + uri: Uri, + notifyForDescendants: Boolean, + settingsObserver: ContentObserver, + userHandle: Int + ) { + if (userHandle == UserHandle.USER_ALL) { + contentObserversAllUsers + .getOrPut(uri.toString()) { mutableListOf() } + .add(settingsObserver) + } else { + val key = SettingsKey(userHandle, uri.toString()) + contentObservers.getOrPut(key) { mutableListOf() }.add(settingsObserver) + } + } + + override fun unregisterContentObserverSync(settingsObserver: ContentObserver) { + contentObservers.values.onEach { it.remove(settingsObserver) } + contentObserversAllUsers.values.onEach { it.remove(settingsObserver) } + } + + override suspend fun registerContentObserver(uri: Uri, settingsObserver: ContentObserver) = + suspendAdvanceDispatcher { + super<UserSettingsProxy>.registerContentObserver(uri, settingsObserver) + } + + override fun registerContentObserverAsync(uri: Uri, settingsObserver: ContentObserver): Job = + advanceDispatcher { + super<UserSettingsProxy>.registerContentObserverAsync(uri, settingsObserver) + } + + override suspend fun registerContentObserver( + uri: Uri, + notifyForDescendants: Boolean, + settingsObserver: ContentObserver + ) = suspendAdvanceDispatcher { + super<UserSettingsProxy>.registerContentObserver( + uri, + notifyForDescendants, + settingsObserver + ) + } + + override fun registerContentObserverAsync( + uri: Uri, + notifyForDescendants: Boolean, + settingsObserver: ContentObserver + ): Job = advanceDispatcher { + super<UserSettingsProxy>.registerContentObserverAsync( + uri, + notifyForDescendants, + settingsObserver + ) + } + + override suspend fun registerContentObserverForUser( + name: String, + settingsObserver: ContentObserver, + userHandle: Int + ) = suspendAdvanceDispatcher { + super<UserSettingsProxy>.registerContentObserverForUser(name, settingsObserver, userHandle) + } + + override fun registerContentObserverForUserAsync( + name: String, + settingsObserver: ContentObserver, + userHandle: Int + ): Job = advanceDispatcher { + super<UserSettingsProxy>.registerContentObserverForUserAsync( + name, + settingsObserver, + userHandle + ) + } + + override fun unregisterContentObserverAsync(settingsObserver: ContentObserver): Job = + advanceDispatcher { + super<UserSettingsProxy>.unregisterContentObserverAsync(settingsObserver) + } + + override suspend fun registerContentObserverForUser( + uri: Uri, + settingsObserver: ContentObserver, + userHandle: Int + ) = suspendAdvanceDispatcher { + super<UserSettingsProxy>.registerContentObserverForUser(uri, settingsObserver, userHandle) + } + + override fun registerContentObserverForUserAsync( + uri: Uri, + settingsObserver: ContentObserver, + userHandle: Int + ): Job = advanceDispatcher { + super<UserSettingsProxy>.registerContentObserverForUserAsync( + uri, + settingsObserver, + userHandle + ) + } + + override fun registerContentObserverForUserAsync( + uri: Uri, + settingsObserver: ContentObserver, + userHandle: Int, + registered: Runnable + ): Job = advanceDispatcher { + super<UserSettingsProxy>.registerContentObserverForUserAsync( + uri, + settingsObserver, + userHandle, + registered + ) + } + + override suspend fun registerContentObserverForUser( + name: String, + notifyForDescendants: Boolean, + settingsObserver: ContentObserver, + userHandle: Int + ) = suspendAdvanceDispatcher { + super<UserSettingsProxy>.registerContentObserverForUser( + name, + notifyForDescendants, + settingsObserver, + userHandle + ) + } + + override fun registerContentObserverForUserAsync( + name: String, + notifyForDescendants: Boolean, + settingsObserver: ContentObserver, + userHandle: Int + ) = advanceDispatcher { + super<UserSettingsProxy>.registerContentObserverForUserAsync( + name, + notifyForDescendants, + settingsObserver, + userHandle + ) + } + + override fun registerContentObserverForUserAsync( + uri: Uri, + notifyForDescendants: Boolean, + settingsObserver: ContentObserver, + userHandle: Int + ): Job = advanceDispatcher { + super<UserSettingsProxy>.registerContentObserverForUserAsync( + uri, + notifyForDescendants, + settingsObserver, + userHandle + ) + } + + override fun getUriFor(name: String): Uri { + return Uri.withAppendedPath(CONTENT_URI, name) + } + + override fun getString(name: String): String? { + return getStringForUser(name, userId) + } + + override fun getStringForUser(name: String, userHandle: Int): String? { + return values[SettingsKey(userHandle, getUriFor(name).toString())] + } + + override fun putString(name: String, value: String?, overrideableByRestore: Boolean): Boolean { + return putStringForUser(name, value, null, false, userId, overrideableByRestore) + } + + override fun putString(name: String, value: String?): Boolean { + return putString(name, value, false) + } + + override fun putStringForUser(name: String, value: String?, userHandle: Int): Boolean { + return putStringForUser(name, value, null, false, userHandle, false) + } + + override fun putStringForUser( + name: String, + value: String?, + tag: String?, + makeDefault: Boolean, + userHandle: Int, + overrideableByRestore: Boolean + ): Boolean { + val key = SettingsKey(userHandle, getUriFor(name).toString()) + values[key] = value + val uri = getUriFor(name) + contentObservers[key]?.onEach { it.dispatchChange(false, listOf(uri), 0, userHandle) } + contentObserversAllUsers[uri.toString()]?.onEach { + it.dispatchChange(false, listOf(uri), 0, userHandle) + } + return true + } + + override fun putString( + name: String, + value: String?, + tag: String?, + makeDefault: Boolean + ): Boolean { + return putString(name, value) + } + + /** Runs current jobs on dispatcher after calling the method. */ + private fun <T> advanceDispatcher(f: () -> T): T { + val result = f() + testDispatcherRunCurrent() + return result + } + + private suspend fun <T> suspendAdvanceDispatcher(f: suspend () -> T): T { + val result = f() + testDispatcherRunCurrent() + return result + } + + private fun testDispatcherRunCurrent() { + val testDispatcher = backgroundDispatcher as? TestDispatcher + testDispatcher?.scheduler?.runCurrent() + } + + private data class SettingsKey(val first: Int, val second: String) : + Pair<Int, String>(first, second) + + companion object { + val CONTENT_URI = Uri.parse("content://settings/fake") + } +} diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp index 4faf03c395f3..615034338c6b 100644 --- a/ravenwood/Android.bp +++ b/ravenwood/Android.bp @@ -280,10 +280,10 @@ sh_test_host { src: "scripts/ravenwood-stats-checker.sh", test_suites: ["general-tests"], data: [ - ":hoststubgen_framework-minus-apex_stats.csv", - ":hoststubgen_framework-minus-apex_apis.csv", - ":hoststubgen_framework-minus-apex_keep_all.txt", - ":hoststubgen_framework-minus-apex_dump.txt", + ":framework-minus-apex.ravenwood-base_all{hoststubgen_framework-minus-apex_stats.csv}", + ":framework-minus-apex.ravenwood-base_all{hoststubgen_framework-minus-apex_apis.csv}", + ":framework-minus-apex.ravenwood-base_all{hoststubgen_framework-minus-apex_keep_all.txt}", + ":framework-minus-apex.ravenwood-base_all{hoststubgen_framework-minus-apex_dump.txt}", ":services.core.ravenwood-base{hoststubgen_services.core_stats.csv}", ":services.core.ravenwood-base{hoststubgen_services.core_apis.csv}", ":services.core.ravenwood-base{hoststubgen_services.core_keep_all.txt}", diff --git a/services/accessibility/java/com/android/server/accessibility/MouseKeysInterceptor.java b/services/accessibility/java/com/android/server/accessibility/MouseKeysInterceptor.java index 56da231ad31a..2ce5c2bc3790 100644 --- a/services/accessibility/java/com/android/server/accessibility/MouseKeysInterceptor.java +++ b/services/accessibility/java/com/android/server/accessibility/MouseKeysInterceptor.java @@ -78,6 +78,9 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation private final AccessibilityManagerService mAms; private final Handler mHandler; + /** Thread to wait for virtual mouse creation to complete */ + private final Thread mCreateVirtualMouseThread; + VirtualDeviceManager.VirtualDevice mVirtualDevice = null; private VirtualMouse mVirtualMouse = null; @@ -154,34 +157,47 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation mHandler = new Handler(looper, this); // 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(() -> { + // This is because the handler thread is the same as the main thread, + // and the main thread will be blocked waiting for the virtual device to be created. + mCreateVirtualMouseThread = new Thread(() -> { mVirtualMouse = createVirtualMouse(displayId); - }).start(); + }); + mCreateVirtualMouseThread.start(); + } + /** + * Wait for {@code mVirtualMouse} to be created. + * This will ensure that {@code mVirtualMouse} is always created before + * trying to send mouse events. + **/ + private void waitForVirtualMouseCreation() { + try { + // Block the current thread until the virtual mouse creation thread completes. + mCreateVirtualMouseThread.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } } @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) private void sendVirtualMouseRelativeEvent(float x, float y) { - if (mVirtualMouse != null) { - mVirtualMouse.sendRelativeEvent(new VirtualMouseRelativeEvent.Builder() - .setRelativeX(x) - .setRelativeY(y) - .build() - ); - } + waitForVirtualMouseCreation(); + mVirtualMouse.sendRelativeEvent(new VirtualMouseRelativeEvent.Builder() + .setRelativeX(x) + .setRelativeY(y) + .build() + ); } @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) private void sendVirtualMouseButtonEvent(int buttonCode, int actionCode) { - if (mVirtualMouse != null) { - mVirtualMouse.sendButtonEvent(new VirtualMouseButtonEvent.Builder() - .setAction(actionCode) - .setButtonCode(buttonCode) - .build() - ); - } + waitForVirtualMouseCreation(); + mVirtualMouse.sendButtonEvent(new VirtualMouseButtonEvent.Builder() + .setAction(actionCode) + .setButtonCode(buttonCode) + .build() + ); } /** @@ -205,12 +221,11 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation case DOWN_MOVE_OR_SCROLL -> -1.0f; default -> 0.0f; }; - if (mVirtualMouse != null) { - mVirtualMouse.sendScrollEvent(new VirtualMouseScrollEvent.Builder() - .setYAxisMovement(y) - .build() - ); - } + waitForVirtualMouseCreation(); + mVirtualMouse.sendScrollEvent(new VirtualMouseScrollEvent.Builder() + .setYAxisMovement(y) + .build() + ); if (DEBUG) { Slog.d(LOG_TAG, "Performed mouse key event: " + mouseKeyEvent.name() + " for scroll action with axis movement (y=" + y + ")"); diff --git a/services/core/java/com/android/server/hdmi/HdmiCecMessageValidator.java b/services/core/java/com/android/server/hdmi/HdmiCecMessageValidator.java index 7746276ac505..3161b770dca6 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecMessageValidator.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecMessageValidator.java @@ -523,8 +523,7 @@ public class HdmiCecMessageValidator { if ((value & 0x80) != 0x00) { return false; } - // Validate than not more than one bit is set - return (Integer.bitCount(value) <= 1); + return true; } /** diff --git a/services/core/java/com/android/server/input/InputManagerInternal.java b/services/core/java/com/android/server/input/InputManagerInternal.java index d32a5ed60094..819b9a166daa 100644 --- a/services/core/java/com/android/server/input/InputManagerInternal.java +++ b/services/core/java/com/android/server/input/InputManagerInternal.java @@ -21,6 +21,7 @@ import android.annotation.Nullable; import android.annotation.UserIdInt; import android.graphics.PointF; import android.hardware.display.DisplayViewport; +import android.hardware.input.KeyboardSystemShortcut; import android.os.IBinder; import android.view.InputChannel; import android.view.inputmethod.InputMethodSubtype; @@ -227,4 +228,20 @@ public abstract class InputManagerInternal { * since boot. */ public abstract int getLastUsedInputDeviceId(); + + /** + * Notify Keyboard system shortcut was triggered by the user and handled by the framework. + * + * NOTE: This is just to notify that a system shortcut was triggered. No further action is + * required to execute the said shortcut. This callback is meant for purposes of providing user + * hints or logging, etc. + * + * @param deviceId the device ID of the keyboard using which the shortcut was triggered + * @param keycodes the keys pressed for triggering the shortcut + * @param modifierState the modifier state of the key event that triggered the shortcut + * @param shortcut the shortcut that was triggered + * + */ + public abstract void notifyKeyboardShortcutTriggered(int deviceId, int[] keycodes, + int modifierState, @KeyboardSystemShortcut.SystemShortcut int shortcut); } diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index a06ad145100d..e555761e34e1 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -47,6 +47,7 @@ import android.hardware.input.IInputDevicesChangedListener; import android.hardware.input.IInputManager; import android.hardware.input.IInputSensorEventListener; import android.hardware.input.IKeyboardBacklightListener; +import android.hardware.input.IKeyboardSystemShortcutListener; import android.hardware.input.IStickyModifierStateListener; import android.hardware.input.ITabletModeChangedListener; import android.hardware.input.InputDeviceIdentifier; @@ -56,6 +57,7 @@ import android.hardware.input.InputSettings; import android.hardware.input.KeyGlyphMap; import android.hardware.input.KeyboardLayout; import android.hardware.input.KeyboardLayoutSelectionResult; +import android.hardware.input.KeyboardSystemShortcut; import android.hardware.input.TouchCalibration; import android.hardware.lights.Light; import android.hardware.lights.LightState; @@ -157,6 +159,7 @@ public class InputManagerService extends IInputManager.Stub private static final int MSG_DELIVER_INPUT_DEVICES_CHANGED = 1; private static final int MSG_RELOAD_DEVICE_ALIASES = 2; private static final int MSG_DELIVER_TABLET_MODE_CHANGED = 3; + private static final int MSG_KEYBOARD_SYSTEM_SHORTCUT_TRIGGERED = 4; private static final int DEFAULT_VIBRATION_MAGNITUDE = 192; private static final AdditionalDisplayInputProperties @@ -306,6 +309,9 @@ public class InputManagerService extends IInputManager.Stub // Manages Sticky modifier state private final StickyModifierStateController mStickyModifierStateController; + // Manages keyboard system shortcut callbacks + private final KeyboardShortcutCallbackHandler mKeyboardShortcutCallbackHandler; + // Manages Keyboard microphone mute led private final KeyboardLedController mKeyboardLedController; @@ -461,6 +467,7 @@ public class InputManagerService extends IInputManager.Stub injector.getLooper(), injector.getUEventManager()) : new KeyboardBacklightControllerInterface() {}; mStickyModifierStateController = new StickyModifierStateController(); + mKeyboardShortcutCallbackHandler = new KeyboardShortcutCallbackHandler(); mKeyboardLedController = new KeyboardLedController(mContext, injector.getLooper(), mNative); mKeyRemapper = new KeyRemapper(mContext, mNative, mDataStore, injector.getLooper()); @@ -2703,6 +2710,36 @@ public class InputManagerService extends IInputManager.Stub lockedModifierState); } + @Override + @EnforcePermission(Manifest.permission.MONITOR_KEYBOARD_SYSTEM_SHORTCUTS) + public void registerKeyboardSystemShortcutListener( + @NonNull IKeyboardSystemShortcutListener listener) { + super.registerKeyboardSystemShortcutListener_enforcePermission(); + Objects.requireNonNull(listener); + mKeyboardShortcutCallbackHandler.registerKeyboardSystemShortcutListener(listener, + Binder.getCallingPid()); + } + + @Override + @EnforcePermission(Manifest.permission.MONITOR_KEYBOARD_SYSTEM_SHORTCUTS) + public void unregisterKeyboardSystemShortcutListener( + @NonNull IKeyboardSystemShortcutListener listener) { + super.unregisterKeyboardSystemShortcutListener_enforcePermission(); + Objects.requireNonNull(listener); + mKeyboardShortcutCallbackHandler.unregisterKeyboardSystemShortcutListener(listener, + Binder.getCallingPid()); + } + + private void handleKeyboardSystemShortcutTriggered(int deviceId, + KeyboardSystemShortcut shortcut) { + InputDevice device = getInputDevice(deviceId); + if (device == null || device.isVirtual() || !device.isFullKeyboard()) { + return; + } + KeyboardMetricsCollector.logKeyboardSystemsEventReportedAtom(device, shortcut); + mKeyboardShortcutCallbackHandler.onKeyboardSystemShortcutTriggered(deviceId, shortcut); + } + /** * Callback interface implemented by the Window Manager. */ @@ -2871,6 +2908,10 @@ public class InputManagerService extends IInputManager.Stub boolean inTabletMode = (boolean) args.arg1; deliverTabletModeChanged(whenNanos, inTabletMode); break; + case MSG_KEYBOARD_SYSTEM_SHORTCUT_TRIGGERED: + int deviceId = msg.arg1; + KeyboardSystemShortcut shortcut = (KeyboardSystemShortcut) msg.obj; + handleKeyboardSystemShortcutTriggered(deviceId, shortcut); } } } @@ -3196,6 +3237,13 @@ public class InputManagerService extends IInputManager.Stub public int getLastUsedInputDeviceId() { return mNative.getLastUsedInputDeviceId(); } + + @Override + public void notifyKeyboardShortcutTriggered(int deviceId, int[] keycodes, int modifierState, + @KeyboardSystemShortcut.SystemShortcut int shortcut) { + mHandler.obtainMessage(MSG_KEYBOARD_SYSTEM_SHORTCUT_TRIGGERED, deviceId, 0, + new KeyboardSystemShortcut(keycodes, modifierState, shortcut)).sendToTarget(); + } } @Override diff --git a/services/core/java/com/android/server/input/KeyboardMetricsCollector.java b/services/core/java/com/android/server/input/KeyboardMetricsCollector.java index f21fd4132f0f..3d2f95105e76 100644 --- a/services/core/java/com/android/server/input/KeyboardMetricsCollector.java +++ b/services/core/java/com/android/server/input/KeyboardMetricsCollector.java @@ -24,31 +24,25 @@ import static android.hardware.input.KeyboardLayoutSelectionResult.layoutSelecti import android.annotation.NonNull; import android.annotation.Nullable; -import android.app.role.RoleManager; -import android.content.Intent; import android.hardware.input.KeyboardLayout; import android.hardware.input.KeyboardLayoutSelectionResult.LayoutSelectionCriteria; +import android.hardware.input.KeyboardSystemShortcut; import android.icu.util.ULocale; import android.text.TextUtils; import android.util.Log; import android.util.Slog; -import android.util.SparseArray; import android.util.proto.ProtoOutputStream; import android.view.InputDevice; -import android.view.KeyEvent; import android.view.inputmethod.InputMethodSubtype; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.KeyboardConfiguredProto.KeyboardLayoutConfig; import com.android.internal.os.KeyboardConfiguredProto.RepeatedKeyboardLayoutConfig; import com.android.internal.util.FrameworkStatsLog; -import com.android.server.policy.ModifierShortcutManager; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Objects; -import java.util.Set; /** * Collect Keyboard metrics @@ -66,336 +60,20 @@ public final class KeyboardMetricsCollector { @VisibleForTesting public static final String DEFAULT_LANGUAGE_TAG = "None"; - public enum KeyboardLogEvent { - UNSPECIFIED( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__UNSPECIFIED, - "INVALID_KEYBOARD_EVENT"), - HOME(FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__HOME, - "HOME"), - RECENT_APPS( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__RECENT_APPS, - "RECENT_APPS"), - BACK(FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__BACK, - "BACK"), - APP_SWITCH( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__APP_SWITCH, - "APP_SWITCH"), - LAUNCH_ASSISTANT( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_ASSISTANT, - "LAUNCH_ASSISTANT"), - LAUNCH_VOICE_ASSISTANT( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_VOICE_ASSISTANT, - "LAUNCH_VOICE_ASSISTANT"), - LAUNCH_SYSTEM_SETTINGS( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_SYSTEM_SETTINGS, - "LAUNCH_SYSTEM_SETTINGS"), - TOGGLE_NOTIFICATION_PANEL( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__TOGGLE_NOTIFICATION_PANEL, - "TOGGLE_NOTIFICATION_PANEL"), - TOGGLE_TASKBAR( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__TOGGLE_TASKBAR, - "TOGGLE_TASKBAR"), - TAKE_SCREENSHOT( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__TAKE_SCREENSHOT, - "TAKE_SCREENSHOT"), - OPEN_SHORTCUT_HELPER( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__OPEN_SHORTCUT_HELPER, - "OPEN_SHORTCUT_HELPER"), - BRIGHTNESS_UP( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__BRIGHTNESS_UP, - "BRIGHTNESS_UP"), - BRIGHTNESS_DOWN( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__BRIGHTNESS_DOWN, - "BRIGHTNESS_DOWN"), - KEYBOARD_BACKLIGHT_UP( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__KEYBOARD_BACKLIGHT_UP, - "KEYBOARD_BACKLIGHT_UP"), - KEYBOARD_BACKLIGHT_DOWN( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__KEYBOARD_BACKLIGHT_DOWN, - "KEYBOARD_BACKLIGHT_DOWN"), - KEYBOARD_BACKLIGHT_TOGGLE( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__KEYBOARD_BACKLIGHT_TOGGLE, - "KEYBOARD_BACKLIGHT_TOGGLE"), - VOLUME_UP( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__VOLUME_UP, - "VOLUME_UP"), - VOLUME_DOWN( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__VOLUME_DOWN, - "VOLUME_DOWN"), - VOLUME_MUTE( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__VOLUME_MUTE, - "VOLUME_MUTE"), - ALL_APPS( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__ALL_APPS, - "ALL_APPS"), - LAUNCH_SEARCH( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_SEARCH, - "LAUNCH_SEARCH"), - LANGUAGE_SWITCH( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LANGUAGE_SWITCH, - "LANGUAGE_SWITCH"), - ACCESSIBILITY_ALL_APPS( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__ACCESSIBILITY_ALL_APPS, - "ACCESSIBILITY_ALL_APPS"), - TOGGLE_CAPS_LOCK( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__TOGGLE_CAPS_LOCK, - "TOGGLE_CAPS_LOCK"), - SYSTEM_MUTE( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__SYSTEM_MUTE, - "SYSTEM_MUTE"), - SPLIT_SCREEN_NAVIGATION( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__SPLIT_SCREEN_NAVIGATION, - "SPLIT_SCREEN_NAVIGATION"), - - CHANGE_SPLITSCREEN_FOCUS( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__CHANGE_SPLITSCREEN_FOCUS, - "CHANGE_SPLITSCREEN_FOCUS"), - TRIGGER_BUG_REPORT( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__TRIGGER_BUG_REPORT, - "TRIGGER_BUG_REPORT"), - LOCK_SCREEN( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LOCK_SCREEN, - "LOCK_SCREEN"), - OPEN_NOTES( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__OPEN_NOTES, - "OPEN_NOTES"), - TOGGLE_POWER( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__TOGGLE_POWER, - "TOGGLE_POWER"), - SYSTEM_NAVIGATION( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__SYSTEM_NAVIGATION, - "SYSTEM_NAVIGATION"), - SLEEP(FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__SLEEP, - "SLEEP"), - WAKEUP(FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__WAKEUP, - "WAKEUP"), - MEDIA_KEY( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__MEDIA_KEY, - "MEDIA_KEY"), - LAUNCH_DEFAULT_BROWSER( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_BROWSER, - "LAUNCH_DEFAULT_BROWSER"), - LAUNCH_DEFAULT_EMAIL( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_EMAIL, - "LAUNCH_DEFAULT_EMAIL"), - LAUNCH_DEFAULT_CONTACTS( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_CONTACTS, - "LAUNCH_DEFAULT_CONTACTS"), - LAUNCH_DEFAULT_CALENDAR( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_CALENDAR, - "LAUNCH_DEFAULT_CALENDAR"), - LAUNCH_DEFAULT_CALCULATOR( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_CALCULATOR, - "LAUNCH_DEFAULT_CALCULATOR"), - LAUNCH_DEFAULT_MUSIC( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_MUSIC, - "LAUNCH_DEFAULT_MUSIC"), - LAUNCH_DEFAULT_MAPS( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_MAPS, - "LAUNCH_DEFAULT_MAPS"), - LAUNCH_DEFAULT_MESSAGING( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_MESSAGING, - "LAUNCH_DEFAULT_MESSAGING"), - LAUNCH_DEFAULT_GALLERY( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_GALLERY, - "LAUNCH_DEFAULT_GALLERY"), - LAUNCH_DEFAULT_FILES( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_FILES, - "LAUNCH_DEFAULT_FILES"), - LAUNCH_DEFAULT_WEATHER( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_WEATHER, - "LAUNCH_DEFAULT_WEATHER"), - LAUNCH_DEFAULT_FITNESS( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_FITNESS, - "LAUNCH_DEFAULT_FITNESS"), - LAUNCH_APPLICATION_BY_PACKAGE_NAME( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_APPLICATION_BY_PACKAGE_NAME, - "LAUNCH_APPLICATION_BY_PACKAGE_NAME"), - DESKTOP_MODE( - FrameworkStatsLog - .KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__DESKTOP_MODE, - "DESKTOP_MODE"), - MULTI_WINDOW_NAVIGATION(FrameworkStatsLog - .KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__MULTI_WINDOW_NAVIGATION, - "MULTIWINDOW_NAVIGATION"); - - - private final int mValue; - private final String mName; - - private static final SparseArray<KeyboardLogEvent> VALUE_TO_ENUM_MAP = new SparseArray<>(); - - static { - for (KeyboardLogEvent type : KeyboardLogEvent.values()) { - VALUE_TO_ENUM_MAP.put(type.mValue, type); - } - } - - KeyboardLogEvent(int enumValue, String enumName) { - mValue = enumValue; - mName = enumName; - } - - public int getIntValue() { - return mValue; - } - - /** - * Convert int value to corresponding KeyboardLogEvent enum. If can't find any matching - * value will return {@code null} - */ - @Nullable - public static KeyboardLogEvent from(int value) { - return VALUE_TO_ENUM_MAP.get(value); - } - - /** - * Find KeyboardLogEvent corresponding to volume up/down/mute key events. - */ - @Nullable - public static KeyboardLogEvent getVolumeEvent(int keycode) { - switch (keycode) { - case KeyEvent.KEYCODE_VOLUME_DOWN: - return VOLUME_DOWN; - case KeyEvent.KEYCODE_VOLUME_UP: - return VOLUME_UP; - case KeyEvent.KEYCODE_VOLUME_MUTE: - return VOLUME_MUTE; - default: - return null; - } - } - - /** - * Find KeyboardLogEvent corresponding to brightness up/down key events. - */ - @Nullable - public static KeyboardLogEvent getBrightnessEvent(int keycode) { - switch (keycode) { - case KeyEvent.KEYCODE_BRIGHTNESS_DOWN: - return BRIGHTNESS_DOWN; - case KeyEvent.KEYCODE_BRIGHTNESS_UP: - return BRIGHTNESS_UP; - default: - return null; - } - } - - /** - * Find KeyboardLogEvent corresponding to intent filter category. Returns - * {@code null if no matching event found} - */ - @Nullable - public static KeyboardLogEvent getLogEventFromIntent(Intent intent) { - Intent selectorIntent = intent.getSelector(); - if (selectorIntent != null) { - Set<String> selectorCategories = selectorIntent.getCategories(); - if (selectorCategories != null && !selectorCategories.isEmpty()) { - for (String intentCategory : selectorCategories) { - KeyboardLogEvent logEvent = getEventFromSelectorCategory(intentCategory); - if (logEvent == null) { - continue; - } - return logEvent; - } - } - } - - // The shortcut may be targeting a system role rather than using an intent selector, - // so check for that. - String role = intent.getStringExtra(ModifierShortcutManager.EXTRA_ROLE); - if (!TextUtils.isEmpty(role)) { - return getLogEventFromRole(role); - } - - Set<String> intentCategories = intent.getCategories(); - if (intentCategories == null || intentCategories.isEmpty() - || !intentCategories.contains(Intent.CATEGORY_LAUNCHER)) { - return null; - } - if (intent.getComponent() == null) { - return null; - } - - // TODO(b/280423320): Add new field package name associated in the - // KeyboardShortcutEvent atom and log it accordingly. - return LAUNCH_APPLICATION_BY_PACKAGE_NAME; - } - - @Nullable - private static KeyboardLogEvent getEventFromSelectorCategory(String category) { - switch (category) { - case Intent.CATEGORY_APP_BROWSER: - return LAUNCH_DEFAULT_BROWSER; - case Intent.CATEGORY_APP_EMAIL: - return LAUNCH_DEFAULT_EMAIL; - case Intent.CATEGORY_APP_CONTACTS: - return LAUNCH_DEFAULT_CONTACTS; - case Intent.CATEGORY_APP_CALENDAR: - return LAUNCH_DEFAULT_CALENDAR; - case Intent.CATEGORY_APP_CALCULATOR: - return LAUNCH_DEFAULT_CALCULATOR; - case Intent.CATEGORY_APP_MUSIC: - return LAUNCH_DEFAULT_MUSIC; - case Intent.CATEGORY_APP_MAPS: - return LAUNCH_DEFAULT_MAPS; - case Intent.CATEGORY_APP_MESSAGING: - return LAUNCH_DEFAULT_MESSAGING; - case Intent.CATEGORY_APP_GALLERY: - return LAUNCH_DEFAULT_GALLERY; - case Intent.CATEGORY_APP_FILES: - return LAUNCH_DEFAULT_FILES; - case Intent.CATEGORY_APP_WEATHER: - return LAUNCH_DEFAULT_WEATHER; - case Intent.CATEGORY_APP_FITNESS: - return LAUNCH_DEFAULT_FITNESS; - default: - return null; - } - } - - /** - * Find KeyboardLogEvent corresponding to the provide system role name. - * Returns {@code null} if no matching event found. - */ - @Nullable - private static KeyboardLogEvent getLogEventFromRole(String role) { - if (RoleManager.ROLE_BROWSER.equals(role)) { - return LAUNCH_DEFAULT_BROWSER; - } else if (RoleManager.ROLE_SMS.equals(role)) { - return LAUNCH_DEFAULT_MESSAGING; - } else { - Log.w(TAG, "Keyboard shortcut to launch " - + role + " not supported for logging"); - return null; - } - } - } - /** * Log keyboard system shortcuts for the proto * {@link com.android.os.input.KeyboardSystemsEventReported} * defined in "stats/atoms/input/input_extension_atoms.proto" */ - public static void logKeyboardSystemsEventReportedAtom(@Nullable InputDevice inputDevice, - @Nullable KeyboardLogEvent keyboardSystemEvent, int modifierState, int... keyCodes) { - // Logging Keyboard system event only for an external HW keyboard. We should not log events - // for virtual keyboards or internal Key events. - if (inputDevice == null || inputDevice.isVirtual() || !inputDevice.isFullKeyboard()) { - return; - } - if (keyboardSystemEvent == null) { - Slog.w(TAG, "Invalid keyboard event logging, keycode = " + Arrays.toString(keyCodes) - + ", modifier state = " + modifierState); - return; - } + public static void logKeyboardSystemsEventReportedAtom(@NonNull InputDevice inputDevice, + @NonNull KeyboardSystemShortcut keyboardSystemShortcut) { FrameworkStatsLog.write(FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED, inputDevice.getVendorId(), inputDevice.getProductId(), - keyboardSystemEvent.getIntValue(), keyCodes, modifierState, - inputDevice.getDeviceBus()); + keyboardSystemShortcut.getSystemShortcut(), keyboardSystemShortcut.getKeycodes(), + keyboardSystemShortcut.getModifierState(), inputDevice.getDeviceBus()); if (DEBUG) { - Slog.d(TAG, "Logging Keyboard system event: " + keyboardSystemEvent.mName); + Slog.d(TAG, "Logging Keyboard system event: " + keyboardSystemShortcut); } } diff --git a/services/core/java/com/android/server/input/KeyboardShortcutCallbackHandler.java b/services/core/java/com/android/server/input/KeyboardShortcutCallbackHandler.java new file mode 100644 index 000000000000..092058e6f7d0 --- /dev/null +++ b/services/core/java/com/android/server/input/KeyboardShortcutCallbackHandler.java @@ -0,0 +1,137 @@ +/* + * Copyright 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.server.input; + +import android.annotation.BinderThread; +import android.hardware.input.IKeyboardSystemShortcutListener; +import android.hardware.input.KeyboardSystemShortcut; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; +import android.util.Slog; +import android.util.SparseArray; + +import com.android.internal.annotations.GuardedBy; + +/** + * A thread-safe component of {@link InputManagerService} responsible for managing callbacks when a + * keyboard shortcut is triggered. + */ +final class KeyboardShortcutCallbackHandler { + + private static final String TAG = "KeyboardShortcut"; + + // To enable these logs, run: + // 'adb shell setprop log.tag.KeyboardShortcutCallbackHandler DEBUG' (requires restart) + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + // List of currently registered keyboard system shortcut listeners keyed by process pid + @GuardedBy("mKeyboardSystemShortcutListenerRecords") + private final SparseArray<KeyboardSystemShortcutListenerRecord> + mKeyboardSystemShortcutListenerRecords = new SparseArray<>(); + + public void onKeyboardSystemShortcutTriggered(int deviceId, + KeyboardSystemShortcut systemShortcut) { + if (DEBUG) { + Slog.d(TAG, "Keyboard system shortcut triggered, deviceId = " + deviceId + + ", systemShortcut = " + systemShortcut); + } + + synchronized (mKeyboardSystemShortcutListenerRecords) { + for (int i = 0; i < mKeyboardSystemShortcutListenerRecords.size(); i++) { + mKeyboardSystemShortcutListenerRecords.valueAt(i).onKeyboardSystemShortcutTriggered( + deviceId, systemShortcut); + } + } + } + + /** Register the keyboard system shortcut listener for a process. */ + @BinderThread + public void registerKeyboardSystemShortcutListener(IKeyboardSystemShortcutListener listener, + int pid) { + synchronized (mKeyboardSystemShortcutListenerRecords) { + if (mKeyboardSystemShortcutListenerRecords.get(pid) != null) { + throw new IllegalStateException("The calling process has already registered " + + "a KeyboardSystemShortcutListener."); + } + KeyboardSystemShortcutListenerRecord record = new KeyboardSystemShortcutListenerRecord( + pid, listener); + try { + listener.asBinder().linkToDeath(record, 0); + } catch (RemoteException ex) { + throw new RuntimeException(ex); + } + mKeyboardSystemShortcutListenerRecords.put(pid, record); + } + } + + /** Unregister the keyboard system shortcut listener for a process. */ + @BinderThread + public void unregisterKeyboardSystemShortcutListener(IKeyboardSystemShortcutListener listener, + int pid) { + synchronized (mKeyboardSystemShortcutListenerRecords) { + KeyboardSystemShortcutListenerRecord record = + mKeyboardSystemShortcutListenerRecords.get(pid); + if (record == null) { + throw new IllegalStateException("The calling process has no registered " + + "KeyboardSystemShortcutListener."); + } + if (record.mListener.asBinder() != listener.asBinder()) { + throw new IllegalStateException("The calling process has a different registered " + + "KeyboardSystemShortcutListener."); + } + record.mListener.asBinder().unlinkToDeath(record, 0); + mKeyboardSystemShortcutListenerRecords.remove(pid); + } + } + + private void onKeyboardSystemShortcutListenerDied(int pid) { + synchronized (mKeyboardSystemShortcutListenerRecords) { + mKeyboardSystemShortcutListenerRecords.remove(pid); + } + } + + // A record of a registered keyboard system shortcut listener from one process. + private class KeyboardSystemShortcutListenerRecord implements IBinder.DeathRecipient { + public final int mPid; + public final IKeyboardSystemShortcutListener mListener; + + KeyboardSystemShortcutListenerRecord(int pid, IKeyboardSystemShortcutListener listener) { + mPid = pid; + mListener = listener; + } + + @Override + public void binderDied() { + if (DEBUG) { + Slog.d(TAG, "Keyboard system shortcut listener for pid " + mPid + " died."); + } + onKeyboardSystemShortcutListenerDied(mPid); + } + + public void onKeyboardSystemShortcutTriggered(int deviceId, KeyboardSystemShortcut data) { + try { + mListener.onKeyboardSystemShortcutTriggered(deviceId, data.getKeycodes(), + data.getModifierState(), data.getSystemShortcut()); + } catch (RemoteException ex) { + Slog.w(TAG, "Failed to notify process " + mPid + + " that keyboard system shortcut was triggered, assuming it died.", ex); + binderDied(); + } + } + } +} diff --git a/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java b/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java index 94b14730bb07..079b7242b1f3 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java @@ -109,10 +109,6 @@ final class InputMethodBindingController { * <dd> * If this bit is ON, some of IME view, e.g. software input, candidate view, is visible. * </dd> - * <dt>{@link InputMethodService#IME_INVISIBLE}</dt> - * <dd> If this bit is ON, IME is ready with views from last EditorInfo but is - * currently invisible. - * </dd> * </dl> * <em>Do not update this value outside of {@link #setImeWindowStatus(IBinder, int, int)} and * {@link InputMethodBindingController#unbindCurrentMethod()}.</em> diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 4dcc35320b76..8afbd56728e4 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -330,7 +330,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @UserIdInt @BinderThread private int resolveImeUserIdLocked(@UserIdInt int callingProcessUserId) { - return mConcurrentMultiUserModeEnabled ? callingProcessUserId : mCurrentUserId; + return mConcurrentMultiUserModeEnabled ? callingProcessUserId : mCurrentImeUserId; } /** @@ -343,7 +343,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @UserIdInt private int resolveImeUserIdFromDisplayIdLocked(int displayId) { return mConcurrentMultiUserModeEnabled - ? mUserManagerInternal.getUserAssignedToDisplay(displayId) : mCurrentUserId; + ? mUserManagerInternal.getUserAssignedToDisplay(displayId) : mCurrentImeUserId; } /** @@ -359,7 +359,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. final int displayId = mWindowManagerInternal.getDisplayIdForWindow(windowToken); return mUserManagerInternal.getUserAssignedToDisplay(displayId); } - return mCurrentUserId; + return mCurrentImeUserId; } final Context mContext; @@ -370,10 +370,23 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @NonNull private final Handler mIoHandler; + /** + * The user ID whose IME should be used if {@link #mConcurrentMultiUserModeEnabled} is + * {@code false}, otherwise remains to be the initial value, which is obtained by + * {@link ActivityManagerInternal#getCurrentUserId()} while the device is booting up. + * + * <p>Never get confused with {@link ActivityManagerInternal#getCurrentUserId()}, which is + * in general useless when designing and implementing interactions between apps and IMEs.</p> + * + * <p>You can also not assume that the IME client process belongs to {@link #mCurrentImeUserId}. + * A most important outlier is System UI process, which always runs under + * {@link UserHandle#USER_SYSTEM} in all the known configurations including Headless System User + * Mode (HSUM).</p> + */ @MultiUserUnawareField @UserIdInt @GuardedBy("ImfLock.class") - private int mCurrentUserId; + private int mCurrentImeUserId; /** Holds all user related data */ @SharedByAllUsersField @@ -540,7 +553,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @GuardedBy("ImfLock.class") @Nullable IInputMethodInvoker getCurMethodLocked() { - return getInputMethodBindingController(mCurrentUserId).getCurMethod(); + return getInputMethodBindingController(mCurrentImeUserId).getCurMethod(); } /** @@ -587,7 +600,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. switch (key) { case Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD: { if (!Flags.imeSwitcherRevamp()) { - if (userId == mCurrentUserId) { + if (userId == mCurrentImeUserId) { mMenuController.updateKeyboardFromSettingsLocked(userId); } } @@ -649,11 +662,11 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. // sender userId can be a real user ID or USER_ALL. final int senderUserId = pendingResult.getSendingUserId(); synchronized (ImfLock.class) { - if (senderUserId != UserHandle.USER_ALL && senderUserId != mCurrentUserId) { + if (senderUserId != UserHandle.USER_ALL && senderUserId != mCurrentImeUserId) { // A background user is trying to hide the dialog. Ignore. return; } - final int userId = mCurrentUserId; + final int userId = mCurrentImeUserId; if (Flags.imeSwitcherRevamp()) { final var bindingController = getInputMethodBindingController(userId); mMenuControllerNew.hide(bindingController.getCurTokenDisplayId(), userId); @@ -1187,7 +1200,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. mShowOngoingImeSwitcherForPhones = false; - mCurrentUserId = mActivityManagerInternal.getCurrentUserId(); + mCurrentImeUserId = mActivityManagerInternal.getCurrentUserId(); final IntFunction<InputMethodBindingController> bindingControllerFactory = userId -> new InputMethodBindingController(userId, InputMethodManagerService.this); @@ -1282,7 +1295,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @GuardedBy("ImfLock.class") private void switchUserOnHandlerLocked(@UserIdInt int newUserId, IInputMethodClientInvoker clientToBeReset) { - final int prevUserId = mCurrentUserId; + final int prevUserId = mCurrentImeUserId; if (DEBUG) { Slog.d(TAG, "Switching user stage 1/3. newUserId=" + newUserId + " prevUserId=" + prevUserId); @@ -1307,7 +1320,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. // TODO(b/342027196): Double check if we need to always reset upon user switching. newUserData.mLastEnabledInputMethodsStr = ""; - mCurrentUserId = newUserId; + mCurrentImeUserId = newUserId; final String defaultImiId = SecureSettingsWrapper.getString( Settings.Secure.DEFAULT_INPUT_METHOD, null, newUserId); @@ -1407,13 +1420,13 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } if (!mSystemReady) { mSystemReady = true; - final int currentUserId = mCurrentUserId; + final int currentImeUserId = mCurrentImeUserId; mStatusBarManagerInternal = LocalServices.getService(StatusBarManagerInternal.class); - hideStatusBarIconLocked(currentUserId); - final var bindingController = getInputMethodBindingController(currentUserId); + hideStatusBarIconLocked(currentImeUserId); + final var bindingController = getInputMethodBindingController(currentImeUserId); updateSystemUiLocked(bindingController.getImeWindowVis(), - bindingController.getBackDisposition(), currentUserId); + bindingController.getBackDisposition(), currentImeUserId); mShowOngoingImeSwitcherForPhones = mRes.getBoolean( com.android.internal.R.bool.show_ongoing_ime_switcher); if (mShowOngoingImeSwitcherForPhones) { @@ -1593,7 +1606,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } // Check if selected IME of current user supports handwriting. - if (userId == mCurrentUserId) { + if (userId == mCurrentImeUserId) { final var bindingController = getInputMethodBindingController(userId); return bindingController.supportsStylusHandwriting() && (!connectionless @@ -2545,7 +2558,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. final int userId = userData.mUserId; // To minimize app compat risk, ignore background users' request for single-user mode. // TODO(b/357178609): generalize the logic and remove this special rule. - if (!mConcurrentMultiUserModeEnabled && userId != mCurrentUserId) { + if (!mConcurrentMultiUserModeEnabled && userId != mCurrentImeUserId) { return; } if (iconId == 0) { @@ -2577,7 +2590,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. private void hideStatusBarIconLocked(@UserIdInt int userId) { // To minimize app compat risk, ignore background users' request for single-user mode. // TODO(b/357178609): generalize the logic and remove this special rule. - if (!mConcurrentMultiUserModeEnabled && userId != mCurrentUserId) { + if (!mConcurrentMultiUserModeEnabled && userId != mCurrentImeUserId) { return; } if (mStatusBarManagerInternal != null) { @@ -2625,8 +2638,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. && mWindowManagerInternal.isKeyguardSecure(userId)) { return false; } - if ((visibility & InputMethodService.IME_ACTIVE) == 0 - || (visibility & InputMethodService.IME_INVISIBLE) != 0) { + if ((visibility & InputMethodService.IME_ACTIVE) == 0) { return false; } if (mWindowManagerInternal.isHardKeyboardAvailable() && !Flags.imeSwitcherRevamp()) { @@ -2778,7 +2790,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. private void updateSystemUiLocked(int vis, int backDisposition, @UserIdInt int userId) { // To minimize app compat risk, ignore background users' request for single-user mode. // TODO(b/357178609): generalize the logic and remove this special rule. - if (!mConcurrentMultiUserModeEnabled && userId != mCurrentUserId) { + if (!mConcurrentMultiUserModeEnabled && userId != mCurrentImeUserId) { return; } final var userData = getUserData(userId); @@ -2791,7 +2803,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. if (DEBUG) { Slog.d(TAG, "IME window vis: " + vis + " active: " + (vis & InputMethodService.IME_ACTIVE) - + " inv: " + (vis & InputMethodService.IME_INVISIBLE) + + " visible: " + (vis & InputMethodService.IME_VISIBLE) + " displayId: " + curTokenDisplayId); } final IBinder focusedWindowToken = userData.mImeBindingState != null @@ -2934,7 +2946,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. // TODO(b/357663774): Figure out how to better handle this scenario. userData.mSubtypeForKeyboardLayoutMapping = Pair.create(newSubtypeHandle, normalizedSubtype); - if (userId != mCurrentUserId) { + if (userId != mCurrentImeUserId) { return; } mInputManagerInternal.onInputMethodSubtypeChangedForKeyboardLayoutMapping( @@ -3704,7 +3716,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. return InputBindResult.USER_SWITCHING; } final int[] profileIdsWithDisabled = mUserManagerInternal.getProfileIds( - mCurrentUserId, false /* enabledOnly */); + mCurrentImeUserId, false /* enabledOnly */); for (int profileId : profileIdsWithDisabled) { if (profileId == userId) { scheduleSwitchUserTaskLocked(userId, cs.mClient); @@ -3751,9 +3763,9 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } // Verify if caller is a background user. - if (!mConcurrentMultiUserModeEnabled && userId != mCurrentUserId) { + if (!mConcurrentMultiUserModeEnabled && userId != mCurrentImeUserId) { if (ArrayUtils.contains( - mUserManagerInternal.getProfileIds(mCurrentUserId, false), + mUserManagerInternal.getProfileIds(mCurrentImeUserId, false), userId)) { // cross-profile access is always allowed here to allow // profile-switching. @@ -4171,29 +4183,27 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. // and the framework couldn't find the last ime, we will make the last ime be // the most applicable enabled keyboard subtype of the system imes. final List<InputMethodInfo> enabled = settings.getEnabledInputMethodList(); - if (enabled != null) { - final int enabledCount = enabled.size(); - final String locale; - if (currentSubtype != null - && !TextUtils.isEmpty(currentSubtype.getLocale())) { - locale = currentSubtype.getLocale(); - } else { - locale = SystemLocaleWrapper.get(userId).get(0).toString(); - } - for (int i = 0; i < enabledCount; ++i) { - final InputMethodInfo imi = enabled.get(i); - if (imi.getSubtypeCount() > 0 && imi.isSystem()) { - InputMethodSubtype keyboardSubtype = - SubtypeUtils.findLastResortApplicableSubtype( - SubtypeUtils.getSubtypes(imi), - SubtypeUtils.SUBTYPE_MODE_KEYBOARD, locale, true); - if (keyboardSubtype != null) { - targetLastImiId = imi.getId(); - subtypeIndex = SubtypeUtils.getSubtypeIndexFromHashCode(imi, - keyboardSubtype.hashCode()); - if (keyboardSubtype.getLocale().equals(locale)) { - break; - } + final int enabledCount = enabled.size(); + final String locale; + if (currentSubtype != null + && !TextUtils.isEmpty(currentSubtype.getLocale())) { + locale = currentSubtype.getLocale(); + } else { + locale = SystemLocaleWrapper.get(userId).get(0).toString(); + } + for (int i = 0; i < enabledCount; ++i) { + final InputMethodInfo imi = enabled.get(i); + if (imi.getSubtypeCount() > 0 && imi.isSystem()) { + InputMethodSubtype keyboardSubtype = + SubtypeUtils.findLastResortApplicableSubtype( + SubtypeUtils.getSubtypes(imi), + SubtypeUtils.SUBTYPE_MODE_KEYBOARD, locale, true); + if (keyboardSubtype != null) { + targetLastImiId = imi.getId(); + subtypeIndex = SubtypeUtils.getSubtypeIndexFromHashCode(imi, + keyboardSubtype.hashCode()); + if (keyboardSubtype.getLocale().equals(locale)) { + break; } } } @@ -4443,7 +4453,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. mStylusIds.add(deviceId); // a new Stylus is detected. If IME supports handwriting, and we don't have // handwriting initialized, lets do it now. - final var bindingController = getInputMethodBindingController(mCurrentUserId); + final var bindingController = getInputMethodBindingController(mCurrentImeUserId); if (!mHwController.getCurrentRequestId().isPresent() && bindingController.supportsStylusHandwriting()) { scheduleResetStylusHandwriting(); @@ -4632,7 +4642,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. private void dumpDebug(ProtoOutputStream proto, long fieldId) { synchronized (ImfLock.class) { - final int userId = mCurrentUserId; + final int userId = mCurrentImeUserId; final var userData = getUserData(userId); final var bindingController = userData.mBindingController; final var visibilityStateComputer = userData.mVisibilityStateComputer; @@ -4951,7 +4961,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. case MSG_REMOVE_IME_SURFACE: { synchronized (ImfLock.class) { // TODO(b/305849394): Needs to figure out what to do where for background users. - final int userId = mCurrentUserId; + final int userId = mCurrentImeUserId; final var userData = getUserData(userId); try { if (userData.mEnabledSession != null @@ -5017,7 +5027,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. case MSG_RESET_HANDWRITING: { synchronized (ImfLock.class) { - final var bindingController = getInputMethodBindingController(mCurrentUserId); + final var bindingController = + getInputMethodBindingController(mCurrentImeUserId); if (bindingController.supportsStylusHandwriting() && bindingController.getCurMethod() != null && hasSupportedStylusLocked()) { @@ -5101,7 +5112,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. private void handleSetInteractive(final boolean interactive) { synchronized (ImfLock.class) { // TODO(b/305849394): Support multiple IMEs. - final int userId = mCurrentUserId; + final int userId = mCurrentImeUserId; final var userData = getUserData(userId); final var bindingController = userData.mBindingController; mIsInteractive = interactive; @@ -6060,7 +6071,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. final Printer p = new PrintWriterPrinter(pw); synchronized (ImfLock.class) { - final int userId = mCurrentUserId; + final int userId = mCurrentImeUserId; final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); final var userData = getUserData(userId); p.println("Current Input Method Manager state:"); @@ -6092,7 +6103,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. }; mClientController.forAllClients(clientControllerDump); final var bindingController = userData.mBindingController; - p.println(" mCurrentUserId=" + userData.mUserId); + p.println(" mCurrentImeUserId=" + userData.mUserId); p.println(" mCurMethodId=" + bindingController.getSelectedMethodId()); client = userData.mCurClient; p.println(" mCurClient=" + client + " mCurSeq=" @@ -6185,7 +6196,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. p.println("No input method client."); } synchronized (ImfLock.class) { - final int userId = mCurrentUserId; + final int userId = mCurrentImeUserId; final var userData = getUserData(userId); if (userData.mImeBindingState.mFocusedWindowClient != null && client != userData.mImeBindingState.mFocusedWindowClient) { @@ -6416,7 +6427,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } final int[] userIds; synchronized (ImfLock.class) { - userIds = InputMethodUtils.resolveUserId(userIdToBeResolved, mCurrentUserId, + userIds = InputMethodUtils.resolveUserId(userIdToBeResolved, mCurrentImeUserId, shellCommand.getErrPrintWriter()); } try (PrintWriter pr = shellCommand.getOutPrintWriter()) { @@ -6462,7 +6473,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. PrintWriter error = shellCommand.getErrPrintWriter()) { synchronized (ImfLock.class) { final int[] userIds = InputMethodUtils.resolveUserId(userIdToBeResolved, - mCurrentUserId, shellCommand.getErrPrintWriter()); + mCurrentImeUserId, shellCommand.getErrPrintWriter()); for (int userId : userIds) { if (!userHasDebugPriv(userId, shellCommand)) { continue; @@ -6557,7 +6568,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. PrintWriter error = shellCommand.getErrPrintWriter()) { synchronized (ImfLock.class) { final int[] userIds = InputMethodUtils.resolveUserId(userIdToBeResolved, - mCurrentUserId, shellCommand.getErrPrintWriter()); + mCurrentImeUserId, shellCommand.getErrPrintWriter()); for (int userId : userIds) { if (!userHasDebugPriv(userId, shellCommand)) { continue; @@ -6577,6 +6588,28 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. out.print(imeId); out.print(" selected for user #"); out.println(userId); + + // Workaround for b/354782333. + final InputMethodSettings settings = + InputMethodSettingsRepository.get(userId); + final var bindingController = getInputMethodBindingController(userId); + final int deviceId = bindingController.getDeviceIdToShowIme(); + final String settingsValue; + if (deviceId == DEVICE_ID_DEFAULT) { + settingsValue = settings.getSelectedInputMethod(); + } else { + settingsValue = settings.getSelectedDefaultDeviceInputMethod(); + } + if (!TextUtils.equals(settingsValue, imeId)) { + Slog.w(TAG, "DEFAULT_INPUT_METHOD=" + settingsValue + + " is not updated. Fixing it up to " + imeId + + " See b/354782333."); + if (deviceId == DEVICE_ID_DEFAULT) { + settings.putSelectedInputMethod(imeId); + } else { + settings.putSelectedDefaultDeviceInputMethod(imeId); + } + } } hasFailed |= failedToSelectUnknownIme; } @@ -6598,7 +6631,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. synchronized (ImfLock.class) { try (PrintWriter out = shellCommand.getOutPrintWriter()) { final int[] userIds = InputMethodUtils.resolveUserId(userIdToBeResolved, - mCurrentUserId, shellCommand.getErrPrintWriter()); + mCurrentImeUserId, shellCommand.getErrPrintWriter()); for (int userId : userIds) { if (!userHasDebugPriv(userId, shellCommand)) { continue; diff --git a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java index bd551fb2ab1b..b4459cb2fe92 100644 --- a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java +++ b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java @@ -1194,9 +1194,9 @@ public final class NotificationAttentionHelper { } boolean shouldIgnoreNotification(final NotificationRecord record) { - // Ignore group summaries - return (record.getSbn().isGroup() && record.getSbn().getNotification() - .isGroupSummary()); + // Ignore auto-group summaries => don't count them as app-posted notifications + // for the cooldown budget + return (record.getSbn().isGroup() && GroupHelper.isAggregatedGroup(record)); } /** diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 45b8da5fc8ea..c7c984b40267 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -2963,8 +2963,9 @@ public class NotificationManagerService extends SystemService { }; cancelGroupChildrenLocked(userId, pkg, Binder.getCallingUid(), Binder.getCallingPid(), null, - false, childrenFlagChecker, groupKey, - REASON_APP_CANCEL, SystemClock.elapsedRealtime()); + false, childrenFlagChecker, + NotificationManagerService::wasChildOfForceRegroupedGroupChecker, + groupKey, REASON_APP_CANCEL, SystemClock.elapsedRealtime()); } } }); @@ -8667,8 +8668,8 @@ public class NotificationManagerService extends SystemService { if (r.getNotification().isGroupSummary()) { cancelGroupChildrenLocked(mUserId, mPkg, mCallingUid, mCallingPid, listenerName, mSendDelete, childrenFlagChecker, - r.getNotification().getGroup(), mReason, - mCancellationElapsedTimeMs); + NotificationManagerService::isChildOfCurrentGroupChecker, + r.getGroupKey(), mReason, mCancellationElapsedTimeMs); } mAttentionHelper.updateLightsLocked(); if (mShortcutHelper != null) { @@ -9390,8 +9391,8 @@ public class NotificationManagerService extends SystemService { if (oldIsSummary && (!isSummary || !oldGroup.equals(group))) { cancelGroupChildrenLocked(old.getUserId(), old.getSbn().getPackageName(), callingUid, callingPid, null, false /* sendDelete */, childrenFlagChecker, - old.getNotification().getGroup(), REASON_APP_CANCEL, - SystemClock.elapsedRealtime()); + NotificationManagerService::isChildOfCurrentGroupChecker, old.getGroupKey(), + REASON_APP_CANCEL, SystemClock.elapsedRealtime()); } } @@ -10372,13 +10373,45 @@ public class NotificationManagerService extends SystemService { public boolean apply(int flags); } - private static boolean isChildOfGroup(final NotificationRecord childRecord, int userId, + @FunctionalInterface + private interface GroupChildChecker { + // Returns true if the childRecord is a child of the group defined + // by the rest of the parameters + boolean apply(NotificationRecord childRecord, int userId, String pkg, String groupKey); + } + + /** + * Checks that the notification is currently a child of the group + * @param childRecord the notification to check + * @param userId userId of the group + * @param pkg package name of the group + * @param groupKey group key for a current group + * @return true if the childRecord is currently a child of the group + */ + private static boolean isChildOfCurrentGroupChecker(NotificationRecord childRecord, int userId, String pkg, String groupKey) { return (childRecord.getUser().getIdentifier() == userId && childRecord.getSbn().getPackageName().equals(pkg) && childRecord.getSbn().isGroup() && !childRecord.getNotification().isGroupSummary() - && TextUtils.equals(groupKey, childRecord.getNotification().getGroup())); + && TextUtils.equals(groupKey, childRecord.getGroupKey())); + } + + /** + * Checks that the notification was originally a child of the group + * @param childRecord the notification to check + * @param userId userId of the group + * @param pkg package name of the group + * @param groupKey original/initial group key for a group that was force grouped + * @return true if the childRecord was originally a child of the group + */ + private static boolean wasChildOfForceRegroupedGroupChecker(NotificationRecord childRecord, + int userId, String pkg, String groupKey) { + return (childRecord.getUser().getIdentifier() == userId + && childRecord.getSbn().getPackageName().equals(pkg) + && childRecord.getSbn().isGroup() + && !childRecord.getNotification().isGroupSummary() + && TextUtils.equals(groupKey, childRecord.getOriginalGroupKey())); } @GuardedBy("mNotificationLock") @@ -10539,18 +10572,19 @@ public class NotificationManagerService extends SystemService { // Warning: The caller is responsible for invoking updateLightsLocked(). @GuardedBy("mNotificationLock") private void cancelGroupChildrenLocked(int userId, String pkg, int callingUid, int callingPid, - String listenerName, boolean sendDelete, FlagChecker flagChecker, String groupKey, - int reason, @ElapsedRealtimeLong long cancellationElapsedTimeMs) { + String listenerName, boolean sendDelete, FlagChecker flagChecker, + GroupChildChecker groupChildChecker, String groupKey, int reason, + @ElapsedRealtimeLong long cancellationElapsedTimeMs) { if (pkg == null) { if (DBG) Slog.e(TAG, "No package for group summary"); return; } cancelGroupChildrenByListLocked(mNotificationList, userId, pkg, callingUid, callingPid, - listenerName, sendDelete, true, flagChecker, groupKey, + listenerName, sendDelete, true, flagChecker, groupChildChecker, groupKey, reason, cancellationElapsedTimeMs); cancelGroupChildrenByListLocked(mEnqueuedNotifications, userId, pkg, callingUid, callingPid, - listenerName, sendDelete, false, flagChecker, groupKey, + listenerName, sendDelete, false, flagChecker, groupChildChecker, groupKey, reason, cancellationElapsedTimeMs); } @@ -10558,12 +10592,13 @@ public class NotificationManagerService extends SystemService { private void cancelGroupChildrenByListLocked(ArrayList<NotificationRecord> notificationList, int userId, String pkg, int callingUid, int callingPid, String listenerName, boolean sendDelete, boolean wasPosted, FlagChecker flagChecker, - String groupKey, int reason, @ElapsedRealtimeLong long cancellationElapsedTimeMs) { + GroupChildChecker grouChildChecker, String groupKey, int reason, + @ElapsedRealtimeLong long cancellationElapsedTimeMs) { final int childReason = REASON_GROUP_SUMMARY_CANCELED; for (int i = notificationList.size() - 1; i >= 0; i--) { final NotificationRecord childR = notificationList.get(i); final StatusBarNotification childSbn = childR.getSbn(); - if (isChildOfGroup(childR, userId, pkg, groupKey) + if (grouChildChecker.apply(childR, userId, pkg, groupKey) && (flagChecker == null || flagChecker.apply(childR.getFlags())) && (!childR.getChannel().isImportantConversation() || reason != REASON_CANCEL)) { EventLogTags.writeNotificationCancel(callingUid, callingPid, pkg, childSbn.getId(), diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java index bd009010a313..1392003a13e7 100644 --- a/services/core/java/com/android/server/notification/NotificationRecord.java +++ b/services/core/java/com/android/server/notification/NotificationRecord.java @@ -1163,6 +1163,21 @@ public final class NotificationRecord { getSbn().setOverrideGroupKey(overrideGroupKey); } + /** + * Get the original group key that was set via {@link Notification.Builder#setGroup} + * + * This value is different than the value returned by {@link #getGroupKey()} as it does + * not contain any userId or package name. + * + * This value is different than the value returned + * by {@link StatusBarNotification#getGroup()} if the notification group + * was overridden: by NotificationAssistantService or by autogrouping. + */ + @Nullable + public String getOriginalGroupKey() { + return getSbn().getNotification().getGroup(); + } + public NotificationChannel getChannel() { return mChannel; } diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index a0d5ea875abf..98e3e24c36b9 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -1801,26 +1801,37 @@ final class InstallPackageHelper { oldPackageState.getRestrictUpdateHash()); } - if (oldPackage != null) { - // APK should not change its sharedUserId declarations - final var oldSharedUid = oldPackage.getSharedUserId() != null - ? oldPackage.getSharedUserId() : "<nothing>"; - final var newSharedUid = parsedPackage.getSharedUserId() != null - ? parsedPackage.getSharedUserId() : "<nothing>"; - if (!oldSharedUid.equals(newSharedUid)) { - throw new PrepareFailure(INSTALL_FAILED_UID_CHANGED, - "Package " + parsedPackage.getPackageName() - + " shared user changed from " - + oldSharedUid + " to " + newSharedUid); + // APK should not change its sharedUserId declarations + final String oldSharedUid; + if (mPm.mSettings.getSharedUserSettingLPr(oldPackageState) != null) { + oldSharedUid = mPm.mSettings.getSharedUserSettingLPr(oldPackageState).name; + } else { + oldSharedUid = "<nothing>"; + } + String newSharedUid = parsedPackage.getSharedUserId() != null + ? parsedPackage.getSharedUserId() : "<nothing>"; + // If the previously installed app version doesn't have sharedUserSetting, + // check that the new apk either doesn't have sharedUserId or it is leaving one. + // If it contains sharedUserId but it is also leaving it, it's ok to proceed. + if (oldSharedUid.equals("<nothing>")) { + if (parsedPackage.isLeavingSharedUser()) { + newSharedUid = "<nothing>"; } + } - // APK should not re-join shared UID - if (oldPackage.isLeavingSharedUser() - && !parsedPackage.isLeavingSharedUser()) { - throw new PrepareFailure(INSTALL_FAILED_UID_CHANGED, - "Package " + parsedPackage.getPackageName() - + " attempting to rejoin " + newSharedUid); - } + if (!oldSharedUid.equals(newSharedUid)) { + throw new PrepareFailure(INSTALL_FAILED_UID_CHANGED, + "Package " + parsedPackage.getPackageName() + + " shared user changed from " + + oldSharedUid + " to " + newSharedUid); + } + + // APK should not re-join shared UID + if (oldPackageState.isLeavingSharedUser() + && !parsedPackage.isLeavingSharedUser()) { + throw new PrepareFailure(INSTALL_FAILED_UID_CHANGED, + "Package " + parsedPackage.getPackageName() + + " attempting to rejoin " + newSharedUid); } // In case of rollback, remember per-user/profile install state diff --git a/services/core/java/com/android/server/pm/PackageSetting.java b/services/core/java/com/android/server/pm/PackageSetting.java index 9f10e0166120..d374142d3912 100644 --- a/services/core/java/com/android/server/pm/PackageSetting.java +++ b/services/core/java/com/android/server/pm/PackageSetting.java @@ -98,6 +98,7 @@ public class PackageSetting extends SettingBase implements PackageStateInternal SCANNED_AS_STOPPED_SYSTEM_APP, PENDING_RESTORE, DEBUGGABLE, + IS_LEAVING_SHARED_USER, }) public @interface Flags { } @@ -107,6 +108,7 @@ public class PackageSetting extends SettingBase implements PackageStateInternal private static final int SCANNED_AS_STOPPED_SYSTEM_APP = 1 << 3; private static final int PENDING_RESTORE = 1 << 4; private static final int DEBUGGABLE = 1 << 5; + private static final int IS_LEAVING_SHARED_USER = 1 << 6; } private int mBooleans; @@ -595,6 +597,20 @@ public class PackageSetting extends SettingBase implements PackageStateInternal } /** + * @see PackageState#isLeavingSharedUser + */ + public PackageSetting setLeavingSharedUser(boolean value) { + setBoolean(Booleans.IS_LEAVING_SHARED_USER, value); + onChanged(); + return this; + } + + @Override + public boolean isLeavingSharedUser() { + return getBoolean(Booleans.IS_LEAVING_SHARED_USER); + } + + /** * @see AndroidPackage#getBaseRevisionCode */ public PackageSetting setBaseRevisionCode(int value) { diff --git a/services/core/java/com/android/server/pm/RemovePackageHelper.java b/services/core/java/com/android/server/pm/RemovePackageHelper.java index 7afc35819aa7..26da84f99f5e 100644 --- a/services/core/java/com/android/server/pm/RemovePackageHelper.java +++ b/services/core/java/com/android/server/pm/RemovePackageHelper.java @@ -435,7 +435,7 @@ final class RemovePackageHelper { // Preserve split apk information for downgrade check with DELETE_KEEP_DATA and archived // app cases - if (deletedPkg.getSplitNames() != null) { + if (deletedPkg != null && deletedPkg.getSplitNames() != null) { deletedPs.setSplitNames(deletedPkg.getSplitNames()); deletedPs.setSplitRevisionCodes(deletedPkg.getSplitRevisionCodes()); } diff --git a/services/core/java/com/android/server/pm/ScanPackageUtils.java b/services/core/java/com/android/server/pm/ScanPackageUtils.java index 95561f5fe0e3..61fddbae0d22 100644 --- a/services/core/java/com/android/server/pm/ScanPackageUtils.java +++ b/services/core/java/com/android/server/pm/ScanPackageUtils.java @@ -482,6 +482,7 @@ final class ScanPackageUtils { + " to " + volumeUuid); pkgSetting.setVolumeUuid(volumeUuid); } + pkgSetting.setLeavingSharedUser(parsedPackage.isLeavingSharedUser()); SharedLibraryInfo sdkLibraryInfo = null; if (!TextUtils.isEmpty(parsedPackage.getSdkLibraryName())) { diff --git a/services/core/java/com/android/server/pm/pkg/PackageState.java b/services/core/java/com/android/server/pm/pkg/PackageState.java index 58761886ecb9..bbc17c83cfac 100644 --- a/services/core/java/com/android/server/pm/pkg/PackageState.java +++ b/services/core/java/com/android/server/pm/pkg/PackageState.java @@ -488,4 +488,10 @@ public interface PackageState { * @hide */ boolean isScannedAsStoppedSystemApp(); + + /** + * see AndroidPackage#isLeavingSharedUser() + * @hide + */ + boolean isLeavingSharedUser(); } diff --git a/services/core/java/com/android/server/policy/ModifierShortcutManager.java b/services/core/java/com/android/server/policy/ModifierShortcutManager.java index cefecbc99bd7..5a4518606ca6 100644 --- a/services/core/java/com/android/server/policy/ModifierShortcutManager.java +++ b/services/core/java/com/android/server/policy/ModifierShortcutManager.java @@ -30,6 +30,7 @@ import android.content.pm.PackageManager; import android.content.res.XmlResourceParser; import android.graphics.drawable.Icon; import android.hardware.input.InputManager; +import android.hardware.input.KeyboardSystemShortcut; import android.os.Handler; import android.os.RemoteException; import android.os.UserHandle; @@ -39,7 +40,6 @@ import android.util.Log; import android.util.LongSparseArray; import android.util.Slog; import android.util.SparseArray; -import android.view.InputDevice; import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.KeyboardShortcutGroup; @@ -49,8 +49,8 @@ import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.policy.IShortcutService; import com.android.internal.util.XmlUtils; -import com.android.server.input.KeyboardMetricsCollector; -import com.android.server.input.KeyboardMetricsCollector.KeyboardLogEvent; +import com.android.server.LocalServices; +import com.android.server.input.InputManagerInternal; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -61,6 +61,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; /** * Manages quick launch shortcuts by: @@ -123,6 +124,7 @@ public class ModifierShortcutManager { private final Context mContext; private final Handler mHandler; + private final InputManagerInternal mInputManagerInternal; private boolean mSearchKeyShortcutPending = false; private boolean mConsumeSearchKeyUp = true; private UserHandle mCurrentUser; @@ -136,6 +138,7 @@ public class ModifierShortcutManager { mRoleIntents.remove(roleName); }, UserHandle.ALL); mCurrentUser = currentUser; + mInputManagerInternal = LocalServices.getService(InputManagerInternal.class); loadShortcuts(); } @@ -473,7 +476,7 @@ public class ModifierShortcutManager { + "keyCode=" + KeyEvent.keyCodeToString(keyCode) + "," + " category=" + category + " role=" + role); } - logKeyboardShortcut(keyEvent, KeyboardLogEvent.getLogEventFromIntent(intent)); + notifyKeyboardShortcutTriggered(keyEvent, getSystemShortcutFromIntent(intent)); return true; } else { return false; @@ -494,22 +497,19 @@ public class ModifierShortcutManager { + "the activity to which it is registered was not found: " + "META+ or SEARCH" + KeyEvent.keyCodeToString(keyCode)); } - logKeyboardShortcut(keyEvent, KeyboardLogEvent.getLogEventFromIntent(shortcutIntent)); + notifyKeyboardShortcutTriggered(keyEvent, getSystemShortcutFromIntent(shortcutIntent)); return true; } return false; } - private void logKeyboardShortcut(KeyEvent event, KeyboardLogEvent logEvent) { - mHandler.post(() -> handleKeyboardLogging(event, logEvent)); - } - - private void handleKeyboardLogging(KeyEvent event, KeyboardLogEvent logEvent) { - final InputManager inputManager = mContext.getSystemService(InputManager.class); - final InputDevice inputDevice = inputManager != null - ? inputManager.getInputDevice(event.getDeviceId()) : null; - KeyboardMetricsCollector.logKeyboardSystemsEventReportedAtom(inputDevice, - logEvent, event.getMetaState(), event.getKeyCode()); + private void notifyKeyboardShortcutTriggered(KeyEvent event, + @KeyboardSystemShortcut.SystemShortcut int systemShortcut) { + if (systemShortcut == KeyboardSystemShortcut.SYSTEM_SHORTCUT_UNSPECIFIED) { + return; + } + mInputManagerInternal.notifyKeyboardShortcutTriggered(event.getDeviceId(), + new int[]{event.getKeyCode()}, event.getMetaState(), systemShortcut); } /** @@ -708,6 +708,97 @@ public class ModifierShortcutManager { return context.getString(resid); }; + + /** + * Find Keyboard shortcut event corresponding to intent filter category. Returns + * {@code SYSTEM_SHORTCUT_UNSPECIFIED if no matching event found} + */ + @KeyboardSystemShortcut.SystemShortcut + private static int getSystemShortcutFromIntent(Intent intent) { + Intent selectorIntent = intent.getSelector(); + if (selectorIntent != null) { + Set<String> selectorCategories = selectorIntent.getCategories(); + if (selectorCategories != null && !selectorCategories.isEmpty()) { + for (String intentCategory : selectorCategories) { + int systemShortcut = getEventFromSelectorCategory(intentCategory); + if (systemShortcut == KeyboardSystemShortcut.SYSTEM_SHORTCUT_UNSPECIFIED) { + continue; + } + return systemShortcut; + } + } + } + + // The shortcut may be targeting a system role rather than using an intent selector, + // so check for that. + String role = intent.getStringExtra(ModifierShortcutManager.EXTRA_ROLE); + if (!TextUtils.isEmpty(role)) { + return getLogEventFromRole(role); + } + + Set<String> intentCategories = intent.getCategories(); + if (intentCategories == null || intentCategories.isEmpty() + || !intentCategories.contains(Intent.CATEGORY_LAUNCHER)) { + return KeyboardSystemShortcut.SYSTEM_SHORTCUT_UNSPECIFIED; + } + if (intent.getComponent() == null) { + return KeyboardSystemShortcut.SYSTEM_SHORTCUT_UNSPECIFIED; + } + + // TODO(b/280423320): Add new field package name associated in the + // KeyboardShortcutEvent atom and log it accordingly. + return KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_APPLICATION_BY_PACKAGE_NAME; + } + + @KeyboardSystemShortcut.SystemShortcut + private static int getEventFromSelectorCategory(String category) { + switch (category) { + case Intent.CATEGORY_APP_BROWSER: + return KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_DEFAULT_BROWSER; + case Intent.CATEGORY_APP_EMAIL: + return KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_DEFAULT_EMAIL; + case Intent.CATEGORY_APP_CONTACTS: + return KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CONTACTS; + case Intent.CATEGORY_APP_CALENDAR: + return KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CALENDAR; + case Intent.CATEGORY_APP_CALCULATOR: + return KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CALCULATOR; + case Intent.CATEGORY_APP_MUSIC: + return KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MUSIC; + case Intent.CATEGORY_APP_MAPS: + return KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MAPS; + case Intent.CATEGORY_APP_MESSAGING: + return KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MESSAGING; + case Intent.CATEGORY_APP_GALLERY: + return KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_DEFAULT_GALLERY; + case Intent.CATEGORY_APP_FILES: + return KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_DEFAULT_FILES; + case Intent.CATEGORY_APP_WEATHER: + return KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_DEFAULT_WEATHER; + case Intent.CATEGORY_APP_FITNESS: + return KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_DEFAULT_FITNESS; + default: + return KeyboardSystemShortcut.SYSTEM_SHORTCUT_UNSPECIFIED; + } + } + + /** + * Find KeyboardLogEvent corresponding to the provide system role name. + * Returns {@code null} if no matching event found. + */ + @KeyboardSystemShortcut.SystemShortcut + private static int getLogEventFromRole(String role) { + if (RoleManager.ROLE_BROWSER.equals(role)) { + return KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_DEFAULT_BROWSER; + } else if (RoleManager.ROLE_SMS.equals(role)) { + return KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MESSAGING; + } else { + Log.w(TAG, "Keyboard shortcut to launch " + + role + " not supported for logging"); + return KeyboardSystemShortcut.SYSTEM_SHORTCUT_UNSPECIFIED; + } + } + void dump(String prefix, PrintWriter pw) { IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " ", prefix); ipw.println("ModifierShortcutManager shortcuts:"); diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 4ddc8a5700aa..720c1c201158 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -139,6 +139,7 @@ import android.hardware.hdmi.HdmiControlManager; import android.hardware.hdmi.HdmiPlaybackClient; import android.hardware.hdmi.HdmiPlaybackClient.OneTouchPlayCallback; import android.hardware.input.InputManager; +import android.hardware.input.KeyboardSystemShortcut; import android.media.AudioManager; import android.media.AudioManagerInternal; import android.media.AudioSystem; @@ -227,8 +228,6 @@ import com.android.server.LocalServices; import com.android.server.SystemServiceManager; import com.android.server.UiThread; import com.android.server.input.InputManagerInternal; -import com.android.server.input.KeyboardMetricsCollector; -import com.android.server.input.KeyboardMetricsCollector.KeyboardLogEvent; import com.android.server.inputmethod.InputMethodManagerInternal; import com.android.server.pm.UserManagerInternal; import com.android.server.policy.KeyCombinationManager.TwoKeysCombinationRule; @@ -732,7 +731,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { private static final int MSG_LAUNCH_ASSIST = 23; private static final int MSG_RINGER_TOGGLE_CHORD = 24; private static final int MSG_SWITCH_KEYBOARD_LAYOUT = 25; - private static final int MSG_LOG_KEYBOARD_SYSTEM_EVENT = 26; private static final int MSG_SET_DEFERRED_KEY_ACTIONS_EXECUTABLE = 27; private class PolicyHandler extends Handler { @@ -820,9 +818,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { handleSwitchKeyboardLayout(object.keyEvent, object.direction, object.focusedToken); break; - case MSG_LOG_KEYBOARD_SYSTEM_EVENT: - handleKeyboardSystemEvent(KeyboardLogEvent.from(msg.arg1), (KeyEvent) msg.obj); - break; case MSG_SET_DEFERRED_KEY_ACTIONS_EXECUTABLE: final int keyCode = msg.arg1; final long downTime = (Long) msg.obj; @@ -1824,7 +1819,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { } private void handleShortPressOnHome(KeyEvent event) { - logKeyboardSystemsEvent(event, KeyboardLogEvent.HOME); + notifyKeyboardShortcutTriggered(event, KeyboardSystemShortcut.SYSTEM_SHORTCUT_HOME); // Turn on the connected TV and switch HDMI input if we're a HDMI playback device. final HdmiControl hdmiControl = getHdmiControl(); @@ -2058,7 +2053,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { } switch (mDoubleTapOnHomeBehavior) { case DOUBLE_TAP_HOME_RECENT_SYSTEM_UI: - logKeyboardSystemsEvent(event, KeyboardLogEvent.APP_SWITCH); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_APP_SWITCH); mHomeConsumed = true; toggleRecentApps(); break; @@ -2086,19 +2082,23 @@ public class PhoneWindowManager implements WindowManagerPolicy { case LONG_PRESS_HOME_ALL_APPS: if (mHasFeatureLeanback) { launchAllAppsAction(); - logKeyboardSystemsEvent(event, KeyboardLogEvent.ALL_APPS); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_ALL_APPS); } else { launchAllAppsViaA11y(); - logKeyboardSystemsEvent(event, KeyboardLogEvent.ACCESSIBILITY_ALL_APPS); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_ACCESSIBILITY_ALL_APPS); } break; case LONG_PRESS_HOME_ASSIST: - logKeyboardSystemsEvent(event, KeyboardLogEvent.LAUNCH_ASSISTANT); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_ASSISTANT); launchAssistAction(null, event.getDeviceId(), event.getEventTime(), AssistUtils.INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS); break; case LONG_PRESS_HOME_NOTIFICATION_PANEL: - logKeyboardSystemsEvent(event, KeyboardLogEvent.TOGGLE_NOTIFICATION_PANEL); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_TOGGLE_NOTIFICATION_PANEL); toggleNotificationPanel(); break; default: @@ -3285,39 +3285,29 @@ public class PhoneWindowManager implements WindowManagerPolicy { WindowManager.LayoutParams.TYPE_SYSTEM_ERROR, }; - /** - * Log the keyboard shortcuts without blocking the current thread. - * - * We won't log keyboard events when the input device is null - * or when it is virtual. - */ - private void handleKeyboardSystemEvent(KeyboardLogEvent keyboardLogEvent, KeyEvent event) { - final InputDevice inputDevice = mInputManager.getInputDevice(event.getDeviceId()); - KeyboardMetricsCollector.logKeyboardSystemsEventReportedAtom(inputDevice, - keyboardLogEvent, event.getMetaState(), event.getKeyCode()); - event.recycle(); - } - - private void logKeyboardSystemsEventOnActionUp(KeyEvent event, - KeyboardLogEvent keyboardSystemEvent) { + private void notifyKeyboardShortcutTriggeredOnActionUp(KeyEvent event, + @KeyboardSystemShortcut.SystemShortcut int systemShortcut) { if (event.getAction() != KeyEvent.ACTION_UP) { return; } - logKeyboardSystemsEvent(event, keyboardSystemEvent); + notifyKeyboardShortcutTriggered(event, systemShortcut); } - private void logKeyboardSystemsEventOnActionDown(KeyEvent event, - KeyboardLogEvent keyboardSystemEvent) { + private void notifyKeyboardShortcutTriggeredOnActionDown(KeyEvent event, + @KeyboardSystemShortcut.SystemShortcut int systemShortcut) { if (event.getAction() != KeyEvent.ACTION_DOWN) { return; } - logKeyboardSystemsEvent(event, keyboardSystemEvent); + notifyKeyboardShortcutTriggered(event, systemShortcut); } - private void logKeyboardSystemsEvent(KeyEvent event, KeyboardLogEvent keyboardSystemEvent) { - KeyEvent eventToLog = KeyEvent.obtain(event); - mHandler.obtainMessage(MSG_LOG_KEYBOARD_SYSTEM_EVENT, keyboardSystemEvent.getIntValue(), 0, - eventToLog).sendToTarget(); + private void notifyKeyboardShortcutTriggered(KeyEvent event, + @KeyboardSystemShortcut.SystemShortcut int systemShortcut) { + if (systemShortcut == KeyboardSystemShortcut.SYSTEM_SHORTCUT_UNSPECIFIED) { + return; + } + mInputManagerInternal.notifyKeyboardShortcutTriggered(event.getDeviceId(), + new int[]{event.getKeyCode()}, event.getMetaState(), systemShortcut); } @Override @@ -3427,7 +3417,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { case KeyEvent.KEYCODE_RECENT_APPS: if (firstDown) { showRecentApps(false /* triggeredFromAltTab */); - logKeyboardSystemsEvent(event, KeyboardLogEvent.RECENT_APPS); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_RECENT_APPS); } return true; case KeyEvent.KEYCODE_APP_SWITCH: @@ -3436,7 +3427,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { preloadRecentApps(); } else if (!down) { toggleRecentApps(); - logKeyboardSystemsEvent(event, KeyboardLogEvent.APP_SWITCH); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_APP_SWITCH); } } return true; @@ -3445,7 +3437,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { launchAssistAction(Intent.EXTRA_ASSIST_INPUT_HINT_KEYBOARD, deviceId, event.getEventTime(), AssistUtils.INVOCATION_TYPE_UNKNOWN); - logKeyboardSystemsEvent(event, KeyboardLogEvent.LAUNCH_ASSISTANT); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_ASSISTANT); return true; } break; @@ -3458,14 +3451,16 @@ public class PhoneWindowManager implements WindowManagerPolicy { case KeyEvent.KEYCODE_I: if (firstDown && event.isMetaPressed()) { showSystemSettings(); - logKeyboardSystemsEvent(event, KeyboardLogEvent.LAUNCH_SYSTEM_SETTINGS); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_SYSTEM_SETTINGS); return true; } break; case KeyEvent.KEYCODE_L: if (firstDown && event.isMetaPressed()) { lockNow(null /* options */); - logKeyboardSystemsEvent(event, KeyboardLogEvent.LOCK_SCREEN); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_LOCK_SCREEN); return true; } break; @@ -3473,10 +3468,12 @@ public class PhoneWindowManager implements WindowManagerPolicy { if (firstDown && event.isMetaPressed()) { if (event.isCtrlPressed()) { sendSystemKeyToStatusBarAsync(event); - logKeyboardSystemsEvent(event, KeyboardLogEvent.OPEN_NOTES); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_OPEN_NOTES); } else { toggleNotificationPanel(); - logKeyboardSystemsEvent(event, KeyboardLogEvent.TOGGLE_NOTIFICATION_PANEL); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_TOGGLE_NOTIFICATION_PANEL); } return true; } @@ -3484,7 +3481,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { case KeyEvent.KEYCODE_S: if (firstDown && event.isMetaPressed() && event.isCtrlPressed()) { interceptScreenshotChord(SCREENSHOT_KEY_OTHER, 0 /*pressDelay*/); - logKeyboardSystemsEvent(event, KeyboardLogEvent.TAKE_SCREENSHOT); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_TAKE_SCREENSHOT); return true; } break; @@ -3497,14 +3495,16 @@ public class PhoneWindowManager implements WindowManagerPolicy { } catch (RemoteException e) { Slog.d(TAG, "Error taking bugreport", e); } - logKeyboardSystemsEvent(event, KeyboardLogEvent.TRIGGER_BUG_REPORT); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_TRIGGER_BUG_REPORT); return true; } } // fall through case KeyEvent.KEYCODE_ESCAPE: if (firstDown && event.isMetaPressed()) { - logKeyboardSystemsEvent(event, KeyboardLogEvent.BACK); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_BACK); injectBackGesture(event.getDownTime()); return true; } @@ -3513,7 +3513,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { StatusBarManagerInternal statusbar = getStatusBarManagerInternal(); if (statusbar != null) { statusbar.moveFocusedTaskToFullscreen(getTargetDisplayIdForKeyEvent(event)); - logKeyboardSystemsEvent(event, KeyboardLogEvent.MULTI_WINDOW_NAVIGATION); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_MULTI_WINDOW_NAVIGATION); return true; } } @@ -3523,7 +3524,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { StatusBarManagerInternal statusbar = getStatusBarManagerInternal(); if (statusbar != null) { statusbar.moveFocusedTaskToDesktop(getTargetDisplayIdForKeyEvent(event)); - logKeyboardSystemsEvent(event, KeyboardLogEvent.DESKTOP_MODE); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_DESKTOP_MODE); return true; } } @@ -3533,12 +3535,15 @@ public class PhoneWindowManager implements WindowManagerPolicy { if (event.isCtrlPressed()) { moveFocusedTaskToStageSplit(getTargetDisplayIdForKeyEvent(event), true /* leftOrTop */); - logKeyboardSystemsEvent(event, KeyboardLogEvent.SPLIT_SCREEN_NAVIGATION); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_SPLIT_SCREEN_NAVIGATION); } else if (event.isAltPressed()) { setSplitscreenFocus(true /* leftOrTop */); - logKeyboardSystemsEvent(event, KeyboardLogEvent.CHANGE_SPLITSCREEN_FOCUS); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_CHANGE_SPLITSCREEN_FOCUS); } else { - logKeyboardSystemsEvent(event, KeyboardLogEvent.BACK); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_BACK); injectBackGesture(event.getDownTime()); } return true; @@ -3549,11 +3554,13 @@ public class PhoneWindowManager implements WindowManagerPolicy { if (event.isCtrlPressed()) { moveFocusedTaskToStageSplit(getTargetDisplayIdForKeyEvent(event), false /* leftOrTop */); - logKeyboardSystemsEvent(event, KeyboardLogEvent.SPLIT_SCREEN_NAVIGATION); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_SPLIT_SCREEN_NAVIGATION); return true; } else if (event.isAltPressed()) { setSplitscreenFocus(false /* leftOrTop */); - logKeyboardSystemsEvent(event, KeyboardLogEvent.CHANGE_SPLITSCREEN_FOCUS); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_CHANGE_SPLITSCREEN_FOCUS); return true; } } @@ -3561,7 +3568,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { case KeyEvent.KEYCODE_SLASH: if (firstDown && event.isMetaPressed() && !keyguardOn) { toggleKeyboardShortcutsMenu(event.getDeviceId()); - logKeyboardSystemsEvent(event, KeyboardLogEvent.OPEN_SHORTCUT_HELPER); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_OPEN_SHORTCUT_HELPER); return true; } break; @@ -3613,25 +3621,32 @@ public class PhoneWindowManager implements WindowManagerPolicy { | Intent.FLAG_ACTIVITY_NO_USER_ACTION); intent.putExtra(EXTRA_FROM_BRIGHTNESS_KEY, true); startActivityAsUser(intent, UserHandle.CURRENT_OR_SELF); - logKeyboardSystemsEvent(event, KeyboardLogEvent.getBrightnessEvent(keyCode)); + + int systemShortcut = keyCode == KeyEvent.KEYCODE_BRIGHTNESS_DOWN + ? KeyboardSystemShortcut.SYSTEM_SHORTCUT_BRIGHTNESS_DOWN + : KeyboardSystemShortcut.SYSTEM_SHORTCUT_BRIGHTNESS_UP; + notifyKeyboardShortcutTriggered(event, systemShortcut); } return true; case KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_DOWN: if (down) { mInputManagerInternal.decrementKeyboardBacklight(event.getDeviceId()); - logKeyboardSystemsEvent(event, KeyboardLogEvent.KEYBOARD_BACKLIGHT_DOWN); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_DOWN); } return true; case KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_UP: if (down) { mInputManagerInternal.incrementKeyboardBacklight(event.getDeviceId()); - logKeyboardSystemsEvent(event, KeyboardLogEvent.KEYBOARD_BACKLIGHT_UP); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_UP); } return true; case KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_TOGGLE: // TODO: Add logic if (!down) { - logKeyboardSystemsEvent(event, KeyboardLogEvent.KEYBOARD_BACKLIGHT_TOGGLE); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_TOGGLE); } return true; case KeyEvent.KEYCODE_VOLUME_UP: @@ -3658,7 +3673,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { if (firstDown && !keyguardOn && isUserSetupComplete()) { if (event.isMetaPressed()) { showRecentApps(false); - logKeyboardSystemsEvent(event, KeyboardLogEvent.RECENT_APPS); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_RECENT_APPS); return true; } else if (mRecentAppsHeldModifiers == 0) { final int shiftlessModifiers = @@ -3667,7 +3683,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { shiftlessModifiers, KeyEvent.META_ALT_ON)) { mRecentAppsHeldModifiers = shiftlessModifiers; showRecentApps(true); - logKeyboardSystemsEvent(event, KeyboardLogEvent.RECENT_APPS); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_RECENT_APPS); return true; } } @@ -3680,17 +3697,20 @@ public class PhoneWindowManager implements WindowManagerPolicy { Message msg = mHandler.obtainMessage(MSG_HANDLE_ALL_APPS); msg.setAsynchronous(true); msg.sendToTarget(); - logKeyboardSystemsEvent(event, KeyboardLogEvent.ALL_APPS); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_ALL_APPS); } else { launchAllAppsViaA11y(); - logKeyboardSystemsEvent(event, KeyboardLogEvent.ACCESSIBILITY_ALL_APPS); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_ACCESSIBILITY_ALL_APPS); } } return true; case KeyEvent.KEYCODE_NOTIFICATION: if (!down) { toggleNotificationPanel(); - logKeyboardSystemsEvent(event, KeyboardLogEvent.TOGGLE_NOTIFICATION_PANEL); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_TOGGLE_NOTIFICATION_PANEL); } return true; case KeyEvent.KEYCODE_SEARCH: @@ -3698,7 +3718,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { switch (mSearchKeyBehavior) { case SEARCH_KEY_BEHAVIOR_TARGET_ACTIVITY: { launchTargetSearchActivity(); - logKeyboardSystemsEvent(event, KeyboardLogEvent.LAUNCH_SEARCH); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_SEARCH); return true; } case SEARCH_KEY_BEHAVIOR_DEFAULT_SEARCH: @@ -3711,7 +3732,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { if (firstDown) { int direction = (metaState & KeyEvent.META_SHIFT_MASK) != 0 ? -1 : 1; sendSwitchKeyboardLayout(event, focusedToken, direction); - logKeyboardSystemsEvent(event, KeyboardLogEvent.LANGUAGE_SWITCH); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_LANGUAGE_SWITCH); return true; } break; @@ -3730,11 +3752,13 @@ public class PhoneWindowManager implements WindowManagerPolicy { if (mPendingCapsLockToggle) { mInputManagerInternal.toggleCapsLock(event.getDeviceId()); mPendingCapsLockToggle = false; - logKeyboardSystemsEvent(event, KeyboardLogEvent.TOGGLE_CAPS_LOCK); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_TOGGLE_CAPS_LOCK); } else if (mPendingMetaAction) { if (!canceled) { launchAllAppsViaA11y(); - logKeyboardSystemsEvent(event, KeyboardLogEvent.ACCESSIBILITY_ALL_APPS); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_ACCESSIBILITY_ALL_APPS); } mPendingMetaAction = false; } @@ -3762,14 +3786,16 @@ public class PhoneWindowManager implements WindowManagerPolicy { if (mPendingCapsLockToggle) { mInputManagerInternal.toggleCapsLock(event.getDeviceId()); mPendingCapsLockToggle = false; - logKeyboardSystemsEvent(event, KeyboardLogEvent.TOGGLE_CAPS_LOCK); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_TOGGLE_CAPS_LOCK); return true; } } break; case KeyEvent.KEYCODE_CAPS_LOCK: if (!down) { - logKeyboardSystemsEvent(event, KeyboardLogEvent.TOGGLE_CAPS_LOCK); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_TOGGLE_CAPS_LOCK); } break; case KeyEvent.KEYCODE_STYLUS_BUTTON_PRIMARY: @@ -3783,10 +3809,12 @@ public class PhoneWindowManager implements WindowManagerPolicy { if (firstDown) { if (mSettingsKeyBehavior == SETTINGS_KEY_BEHAVIOR_NOTIFICATION_PANEL) { toggleNotificationPanel(); - logKeyboardSystemsEvent(event, KeyboardLogEvent.TOGGLE_NOTIFICATION_PANEL); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_TOGGLE_NOTIFICATION_PANEL); } else if (mSettingsKeyBehavior == SETTINGS_KEY_BEHAVIOR_SETTINGS_ACTIVITY) { showSystemSettings(); - logKeyboardSystemsEvent(event, KeyboardLogEvent.LAUNCH_SYSTEM_SETTINGS); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_SYSTEM_SETTINGS); } } return true; @@ -4732,7 +4760,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { // Handle special keys. switch (keyCode) { case KeyEvent.KEYCODE_BACK: { - logKeyboardSystemsEventOnActionUp(event, KeyboardLogEvent.BACK); + notifyKeyboardShortcutTriggeredOnActionUp(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_BACK); if (down) { // There may have other embedded activities on the same Task. Try to move the // focus before processing the back event. @@ -4753,8 +4782,12 @@ public class PhoneWindowManager implements WindowManagerPolicy { case KeyEvent.KEYCODE_VOLUME_DOWN: case KeyEvent.KEYCODE_VOLUME_UP: case KeyEvent.KEYCODE_VOLUME_MUTE: { - logKeyboardSystemsEventOnActionDown(event, - KeyboardLogEvent.getVolumeEvent(keyCode)); + int systemShortcut = keyCode == KEYCODE_VOLUME_DOWN + ? KeyboardSystemShortcut.SYSTEM_SHORTCUT_VOLUME_DOWN + : keyCode == KEYCODE_VOLUME_UP + ? KeyboardSystemShortcut.SYSTEM_SHORTCUT_VOLUME_UP + : KeyboardSystemShortcut.SYSTEM_SHORTCUT_VOLUME_MUTE; + notifyKeyboardShortcutTriggeredOnActionDown(event, systemShortcut); if (down) { sendSystemKeyToStatusBarAsync(event); @@ -4855,7 +4888,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { } case KeyEvent.KEYCODE_TV_POWER: { - logKeyboardSystemsEventOnActionUp(event, KeyboardLogEvent.TOGGLE_POWER); + notifyKeyboardShortcutTriggeredOnActionUp(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_TOGGLE_POWER); result &= ~ACTION_PASS_TO_USER; isWakeKey = false; // wake-up will be handled separately if (down && hdmiControlManager != null) { @@ -4865,7 +4899,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { } case KeyEvent.KEYCODE_POWER: { - logKeyboardSystemsEventOnActionUp(event, KeyboardLogEvent.TOGGLE_POWER); + notifyKeyboardShortcutTriggeredOnActionUp(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_TOGGLE_POWER); EventLogTags.writeInterceptPower( KeyEvent.actionToString(event.getAction()), mPowerKeyHandled ? 1 : 0, @@ -4888,14 +4923,16 @@ public class PhoneWindowManager implements WindowManagerPolicy { case KeyEvent.KEYCODE_SYSTEM_NAVIGATION_LEFT: // fall through case KeyEvent.KEYCODE_SYSTEM_NAVIGATION_RIGHT: { - logKeyboardSystemsEventOnActionUp(event, KeyboardLogEvent.SYSTEM_NAVIGATION); + notifyKeyboardShortcutTriggeredOnActionUp(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_SYSTEM_NAVIGATION); result &= ~ACTION_PASS_TO_USER; interceptSystemNavigationKey(event); break; } case KeyEvent.KEYCODE_SLEEP: { - logKeyboardSystemsEventOnActionUp(event, KeyboardLogEvent.SLEEP); + notifyKeyboardShortcutTriggeredOnActionUp(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_SLEEP); result &= ~ACTION_PASS_TO_USER; isWakeKey = false; if (!mPowerManager.isInteractive()) { @@ -4911,7 +4948,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { } case KeyEvent.KEYCODE_SOFT_SLEEP: { - logKeyboardSystemsEventOnActionUp(event, KeyboardLogEvent.SLEEP); + notifyKeyboardShortcutTriggeredOnActionUp(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_SLEEP); result &= ~ACTION_PASS_TO_USER; isWakeKey = false; if (!down) { @@ -4922,7 +4960,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { } case KeyEvent.KEYCODE_WAKEUP: { - logKeyboardSystemsEventOnActionUp(event, KeyboardLogEvent.WAKEUP); + notifyKeyboardShortcutTriggeredOnActionUp(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_WAKEUP); result &= ~ACTION_PASS_TO_USER; isWakeKey = true; break; @@ -4931,7 +4970,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { case KeyEvent.KEYCODE_MUTE: result &= ~ACTION_PASS_TO_USER; if (down && event.getRepeatCount() == 0) { - logKeyboardSystemsEvent(event, KeyboardLogEvent.SYSTEM_MUTE); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_SYSTEM_MUTE); toggleMicrophoneMuteFromKey(); } break; @@ -4946,7 +4986,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { case KeyEvent.KEYCODE_MEDIA_RECORD: case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: case KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK: { - logKeyboardSystemsEventOnActionUp(event, KeyboardLogEvent.MEDIA_KEY); + notifyKeyboardShortcutTriggeredOnActionUp(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_MEDIA_KEY); if (MediaSessionLegacyHelper.getHelper(mContext).isGlobalPriorityActive()) { // If the global session is active pass all media keys to it // instead of the active window. @@ -4991,7 +5032,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { 0 /* unused */, event.getEventTime() /* eventTime */); msg.setAsynchronous(true); msg.sendToTarget(); - logKeyboardSystemsEvent(event, KeyboardLogEvent.LAUNCH_ASSISTANT); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_ASSISTANT); } result &= ~ACTION_PASS_TO_USER; break; @@ -5002,7 +5044,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { Message msg = mHandler.obtainMessage(MSG_LAUNCH_VOICE_ASSIST_WITH_WAKE_LOCK); msg.setAsynchronous(true); msg.sendToTarget(); - logKeyboardSystemsEvent(event, KeyboardLogEvent.LAUNCH_VOICE_ASSISTANT); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_VOICE_ASSISTANT); } result &= ~ACTION_PASS_TO_USER; break; diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 7210098d8daf..5c096ecbba04 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -2524,8 +2524,11 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // trampoline that will be always created and finished immediately. Then give a chance to // see if the snapshot is usable for the current running activity so the transition will // look smoother, instead of showing a splash screen on the second launch. - if (!newTask && taskSwitch && processRunning && !activityCreated && task.intent != null - && mActivityComponent.equals(task.intent.getComponent())) { + if (!newTask && taskSwitch && !activityCreated && task.intent != null + // Another case where snapshot is allowed to be used is if this activity has not yet + // been created && is translucent or floating. + // The component isn't necessary to be matched in this case. + && (!mOccludesParent || mActivityComponent.equals(task.intent.getComponent()))) { final ActivityRecord topAttached = task.getActivity(ActivityRecord::attachedToProcess); if (topAttached != null) { if (topAttached.isSnapshotCompatible(snapshot) @@ -5463,6 +5466,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } } + mAtmService.mBackNavigationController.onAppVisibilityChanged(this, visible); onChildVisibilityRequested(visible); final DisplayContent displayContent = getDisplayContent(); diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java index 509a060c096d..8ef2693ec327 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java +++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java @@ -2846,6 +2846,11 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { } finally { SaferIntentUtils.DISABLE_ENFORCE_INTENTS_TO_MATCH_INTENT_FILTERS.set(false); synchronized (mService.mGlobalLock) { + // Remove the empty task in case the activity was failed to be launched on the + // task that was restored from Recents. + if (!task.hasChild() && task.shouldRemoveSelfOnLastChildRemoval()) { + task.removeIfPossible("start-from-recents"); + } mService.continueWindowLayout(); } } diff --git a/services/core/java/com/android/server/wm/AppCompatReachabilityOverrides.java b/services/core/java/com/android/server/wm/AppCompatReachabilityOverrides.java index b9bdc325cf98..caff96ba4a9f 100644 --- a/services/core/java/com/android/server/wm/AppCompatReachabilityOverrides.java +++ b/services/core/java/com/android/server/wm/AppCompatReachabilityOverrides.java @@ -35,7 +35,6 @@ import android.annotation.NonNull; import android.content.res.Configuration; import android.graphics.Rect; -import com.android.internal.annotations.VisibleForTesting; import com.android.window.flags.Flags; /** @@ -112,12 +111,10 @@ class AppCompatReachabilityOverrides { : mAppCompatConfiguration.getLetterboxVerticalPositionMultiplier(tabletopMode); } - @VisibleForTesting boolean isHorizontalReachabilityEnabled() { return isHorizontalReachabilityEnabled(mActivityRecord.getParent().getConfiguration()); } - @VisibleForTesting boolean isVerticalReachabilityEnabled() { return isVerticalReachabilityEnabled(mActivityRecord.getParent().getConfiguration()); } diff --git a/services/core/java/com/android/server/wm/AppCompatReachabilityPolicy.java b/services/core/java/com/android/server/wm/AppCompatReachabilityPolicy.java index 90bfddb2095f..c3bf116e227d 100644 --- a/services/core/java/com/android/server/wm/AppCompatReachabilityPolicy.java +++ b/services/core/java/com/android/server/wm/AppCompatReachabilityPolicy.java @@ -31,6 +31,8 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.graphics.Rect; +import com.android.internal.annotations.VisibleForTesting; + import java.util.function.Supplier; /** @@ -43,7 +45,8 @@ class AppCompatReachabilityPolicy { @NonNull private final AppCompatConfiguration mAppCompatConfiguration; @Nullable - private Supplier<Rect> mLetterboxInnerBoundsSupplier; + @VisibleForTesting + Supplier<Rect> mLetterboxInnerBoundsSupplier; AppCompatReachabilityPolicy(@NonNull ActivityRecord activityRecord, @NonNull AppCompatConfiguration appCompatConfiguration) { diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java index b342bb48482a..b4c75572d68f 100644 --- a/services/core/java/com/android/server/wm/BackNavigationController.java +++ b/services/core/java/com/android/server/wm/BackNavigationController.java @@ -768,6 +768,48 @@ class BackNavigationController { } } + void onAppVisibilityChanged(@NonNull ActivityRecord ar, boolean visible) { + if (!mAnimationHandler.mComposed) { + return; + } + + final boolean openingTransition = mAnimationHandler.mOpenAnimAdaptor + .mPreparedOpenTransition != null; + // Detect if another transition is collecting during predictive back animation. + if (openingTransition && !visible && mAnimationHandler.isTarget(ar, false /* open */) + && ar.mTransitionController.isCollecting(ar)) { + final TransitionController controller = ar.mTransitionController; + boolean collectTask = false; + ActivityRecord changedActivity = null; + for (int i = mAnimationHandler.mOpenActivities.length - 1; i >= 0; --i) { + final ActivityRecord next = mAnimationHandler.mOpenActivities[i]; + if (next.mLaunchTaskBehind) { + // collect previous activity, so shell side can handle the transition. + controller.collect(next); + collectTask = true; + restoreLaunchBehind(next, true /* cancel */, false /* finishTransition */); + changedActivity = next; + } + } + if (collectTask && mAnimationHandler.mOpenAnimAdaptor.mAdaptors[0].mSwitchType + == AnimationHandler.TASK_SWITCH) { + final Task topTask = mAnimationHandler.mOpenAnimAdaptor.mAdaptors[0].getTopTask(); + if (topTask != null) { + WindowContainer parent = mAnimationHandler.mOpenActivities[0].getParent(); + while (parent != topTask && parent.isDescendantOf(topTask)) { + controller.collect(parent); + parent = parent.getParent(); + } + controller.collect(topTask); + } + } + if (changedActivity != null) { + changedActivity.getDisplayContent().ensureActivitiesVisible(null /* starting */, + true /* notifyClients */); + } + } + } + // For shell transition /** * Check whether the transition targets was animated by back gesture animation. @@ -784,7 +826,13 @@ class BackNavigationController { mAnimationHandler.markStartingSurfaceMatch(startTransaction); return; } - if (!isMonitoringFinishTransition() || targets.isEmpty()) { + if (targets.isEmpty()) { + return; + } + final boolean migratePredictToTransition = Flags.migratePredictiveBackTransition(); + if (migratePredictToTransition && !mAnimationHandler.mComposed) { + return; + } else if (!isMonitoringFinishTransition()) { return; } if (mAnimationHandler.hasTargetDetached()) { @@ -808,20 +856,27 @@ class BackNavigationController { mTmpCloseApps.add(wc); } } - final boolean matchAnimationTargets = isWaitBackTransition() + final boolean matchAnimationTargets; + if (migratePredictToTransition) { + matchAnimationTargets = + mAnimationHandler.containsBackAnimationTargets(mTmpOpenApps, mTmpCloseApps); + } else { + matchAnimationTargets = isWaitBackTransition() && (transition.mType == TRANSIT_CLOSE || transition.mType == TRANSIT_TO_BACK) && mAnimationHandler.containsBackAnimationTargets(mTmpOpenApps, mTmpCloseApps); + } ProtoLog.d(WM_DEBUG_BACK_PREVIEW, "onTransactionReady, opening: %s, closing: %s, animating: %s, match: %b", mTmpOpenApps, mTmpCloseApps, mAnimationHandler, matchAnimationTargets); - if (!matchAnimationTargets) { + // Don't cancel transition, let transition handler to handle it + if (!matchAnimationTargets && !migratePredictToTransition) { mNavigationMonitor.onTransitionReadyWhileNavigate(mTmpOpenApps, mTmpCloseApps); } else { if (mAnimationHandler.mPrepareCloseTransition != null) { Slog.e(TAG, "Gesture animation is applied on another transition?"); } mAnimationHandler.mPrepareCloseTransition = transition; - if (!Flags.migratePredictiveBackTransition()) { + if (!migratePredictToTransition) { // Because the target will reparent to transition root, so it cannot be controlled // by animation leash. Hide the close target when transition starts. startTransaction.hide(mAnimationHandler.mCloseAdaptor.mTarget.getSurfaceControl()); @@ -839,7 +894,19 @@ class BackNavigationController { } boolean isMonitorTransitionTarget(WindowContainer wc) { - if ((isWaitBackTransition() && mAnimationHandler.mPrepareCloseTransition != null) + if (Flags.migratePredictiveBackTransition()) { + if (!mAnimationHandler.mComposed) { + return false; + } + if (mAnimationHandler.mSwitchType == AnimationHandler.TASK_SWITCH + && wc.asActivityRecord() != null + || (mAnimationHandler.mSwitchType == AnimationHandler.ACTIVITY_SWITCH + && wc.asTask() != null)) { + return false; + } + return (mAnimationHandler.isTarget(wc, true /* open */) + || mAnimationHandler.isTarget(wc, false /* open */)); + } else if ((isWaitBackTransition() && mAnimationHandler.mPrepareCloseTransition != null) || (mAnimationHandler.mOpenAnimAdaptor != null && mAnimationHandler.mOpenAnimAdaptor.mPreparedOpenTransition != null)) { return mAnimationHandler.isTarget(wc, wc.isVisibleRequested() /* open */); @@ -1840,6 +1907,43 @@ class BackNavigationController { return openActivities; } + boolean restoreBackNavigation() { + if (!mAnimationHandler.mComposed) { + return false; + } + ActivityRecord[] penActivities = mAnimationHandler.mOpenActivities; + boolean changed = false; + if (penActivities != null) { + for (int i = penActivities.length - 1; i >= 0; --i) { + ActivityRecord resetActivity = penActivities[i]; + if (resetActivity.mLaunchTaskBehind) { + resetActivity.mTransitionController.collect(resetActivity); + restoreLaunchBehind(resetActivity, true, false); + changed = true; + } + } + } + return changed; + } + + boolean restoreBackNavigationSetTransitionReady(Transition transition) { + if (!mAnimationHandler.mComposed) { + return false; + } + ActivityRecord[] penActivities = mAnimationHandler.mOpenActivities; + if (penActivities != null) { + for (int i = penActivities.length - 1; i >= 0; --i) { + ActivityRecord resetActivity = penActivities[i]; + if (transition.isInTransition(resetActivity)) { + resetActivity.mTransitionController.setReady( + resetActivity.getDisplayContent(), true); + return true; + } + } + } + return false; + } + private static Transition setLaunchBehind(@NonNull ActivityRecord[] activities) { final boolean migrateBackTransition = Flags.migratePredictiveBackTransition(); final ArrayList<ActivityRecord> affects = new ArrayList<>(); @@ -1919,9 +2023,12 @@ class BackNavigationController { activity); if (cancel) { final boolean migrateBackTransition = Flags.migratePredictiveBackTransition(); - if (migrateBackTransition && finishTransition) { - activity.commitVisibility(false /* visible */, false /* performLayout */, - true /* fromTransition */); + // could be visible if transition is canceled due to top activity is finishing. + if (migrateBackTransition) { + if (finishTransition && !activity.shouldBeVisible()) { + activity.commitVisibility(false /* visible */, false /* performLayout */, + true /* fromTransition */); + } } else { // Restore the launch-behind state // TODO b/347168362 Change status directly during collecting for a transition. diff --git a/services/core/java/com/android/server/wm/DesktopModeBoundsCalculator.java b/services/core/java/com/android/server/wm/DesktopModeBoundsCalculator.java index f566df5fd147..8f1828d741c5 100644 --- a/services/core/java/com/android/server/wm/DesktopModeBoundsCalculator.java +++ b/services/core/java/com/android/server/wm/DesktopModeBoundsCalculator.java @@ -109,6 +109,13 @@ public final class DesktopModeBoundsCalculator { if (!DesktopModeFlagsUtil.DYNAMIC_INITIAL_BOUNDS.isEnabled(activity.mWmService.mContext)) { return centerInScreen(idealSize, screenBounds); } + if (activity.mAppCompatController.getAppCompatAspectRatioOverrides() + .hasFullscreenOverride()) { + // If the activity has a fullscreen override applied, it should be treated as + // resizeable and match the device orientation. Thus the ideal size can be + // applied. + return centerInScreen(idealSize, screenBounds); + } // TODO(b/353457301): Replace with app compat aspect ratio method when refactoring complete. float appAspectRatio = calculateAspectRatio(task, activity); final float tdaWidth = stableBounds.width(); diff --git a/services/core/java/com/android/server/wm/KeyguardController.java b/services/core/java/com/android/server/wm/KeyguardController.java index 7a0fd3e34fdd..e18ca8552e72 100644 --- a/services/core/java/com/android/server/wm/KeyguardController.java +++ b/services/core/java/com/android/server/wm/KeyguardController.java @@ -39,6 +39,7 @@ import static android.view.WindowManagerPolicyConstants.KEYGUARD_GOING_AWAY_FLAG import static android.view.WindowManagerPolicyConstants.KEYGUARD_GOING_AWAY_FLAG_TO_SHADE; import static android.view.WindowManagerPolicyConstants.KEYGUARD_GOING_AWAY_FLAG_WITH_WALLPAPER; +import static com.android.window.flags.Flags.reduceKeyguardTransitions; import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER; import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM; import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME; @@ -77,6 +78,8 @@ class KeyguardController { private static final int DEFER_WAKE_TRANSITION_TIMEOUT_MS = 5000; + private static final int GOING_AWAY_TIMEOUT_MS = 10500; + private final ActivityTaskSupervisor mTaskSupervisor; private WindowManagerService mWindowManager; @@ -232,6 +235,7 @@ class KeyguardController { dc.mWallpaperController.adjustWallpaperWindows(); dc.executeAppTransition(); } + scheduleGoingAwayTimeout(displayId); } // Update the sleep token first such that ensureActivitiesVisible has correct sleep token @@ -286,6 +290,8 @@ class KeyguardController { mRootWindowContainer.ensureActivitiesVisible(); mRootWindowContainer.addStartingWindowsForVisibleActivities(); mWindowManager.executeAppTransition(); + + scheduleGoingAwayTimeout(displayId); } finally { mService.continueWindowLayout(); Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); @@ -417,31 +423,42 @@ class KeyguardController { final TransitionController tc = mRootWindowContainer.mTransitionController; final KeyguardDisplayState state = getDisplayState(displayId); + final DisplayContent dc = mRootWindowContainer.getDisplayContent(displayId); - final boolean occluded = state.mOccluded; - final boolean performTransition = isKeyguardLocked(displayId); - final boolean executeTransition = performTransition && !tc.isCollecting(); + final boolean locked = isKeyguardLocked(displayId); + final boolean executeTransition = !tc.isShellTransitionsEnabled() + || (locked && !tc.isCollecting() && !reduceKeyguardTransitions()); + + final int transitType, transitFlags, notFlags; + if (state.mOccluded) { + transitType = TRANSIT_KEYGUARD_OCCLUDE; + transitFlags = TRANSIT_FLAG_KEYGUARD_OCCLUDING; + notFlags = TRANSIT_FLAG_KEYGUARD_UNOCCLUDING; + } else { + transitType = TRANSIT_KEYGUARD_UNOCCLUDE; + transitFlags = TRANSIT_FLAG_KEYGUARD_UNOCCLUDING; + notFlags = TRANSIT_FLAG_KEYGUARD_OCCLUDING; + } - mWindowManager.mPolicy.onKeyguardOccludedChangedLw(occluded); + mWindowManager.mPolicy.onKeyguardOccludedChangedLw(state.mOccluded); mService.deferWindowLayout(); try { - if (isKeyguardLocked(displayId)) { - final int type = occluded ? TRANSIT_KEYGUARD_OCCLUDE : TRANSIT_KEYGUARD_UNOCCLUDE; - final int flag = occluded ? TRANSIT_FLAG_KEYGUARD_OCCLUDING - : TRANSIT_FLAG_KEYGUARD_UNOCCLUDING; + if (locked) { if (tc.isShellTransitionsEnabled()) { - final Task trigger = (occluded && topActivity != null) + final Task trigger = (state.mOccluded && topActivity != null) ? topActivity.getRootTask() : null; - Transition transition = tc.requestTransitionIfNeeded(type, flag, trigger, - mRootWindowContainer.getDefaultDisplay()); + tc.requestTransitionIfNeeded(transitType, transitFlags, trigger, dc); + final Transition transition = tc.getCollectingTransition(); + if ((transition.getFlags() & notFlags) != 0 && reduceKeyguardTransitions()) { + transition.removeFlag(notFlags); + } else { + transition.addFlag(transitFlags); + } if (trigger != null) { - if (transition == null) { - transition = tc.getCollectingTransition(); - } transition.collect(trigger); } } else { - mRootWindowContainer.getDefaultDisplay().prepareAppTransition(type, flag); + dc.prepareAppTransition(transitType, transitFlags); } } else { if (tc.inTransition()) { @@ -451,8 +468,8 @@ class KeyguardController { } } updateKeyguardSleepToken(displayId); - if (performTransition && executeTransition) { - mWindowManager.executeAppTransition(); + if (executeTransition) { + dc.executeAppTransition(); } } finally { mService.continueWindowLayout(); @@ -590,6 +607,34 @@ class KeyguardController { } } + /** + * Called when the default display's mKeyguardGoingAway has been left as {@code true} for too + * long. Send an explicit message to the KeyguardService asking it to wrap up. + */ + private final Runnable mGoingAwayTimeout = () -> { + synchronized (mWindowManager.mGlobalLock) { + KeyguardDisplayState state = getDisplayState(DEFAULT_DISPLAY); + if (!state.mKeyguardGoingAway) { + return; + } + state.mKeyguardGoingAway = false; + state.writeEventLog("goingAwayTimeout"); + mWindowManager.mPolicy.startKeyguardExitAnimation(0); + } + }; + + private void scheduleGoingAwayTimeout(int displayId) { + if (displayId != DEFAULT_DISPLAY) { + return; + } + if (getDisplayState(displayId).mKeyguardGoingAway) { + if (!mWindowManager.mH.hasCallbacks(mGoingAwayTimeout)) { + mWindowManager.mH.postDelayed(mGoingAwayTimeout, GOING_AWAY_TIMEOUT_MS); + } + } else { + mWindowManager.mH.removeCallbacks(mGoingAwayTimeout); + } + } /** Represents Keyguard state per individual display. */ private static class KeyguardDisplayState { @@ -709,6 +754,7 @@ class KeyguardController { if (!lastKeyguardGoingAway && mKeyguardGoingAway) { writeEventLog("dismissIfInsecure"); controller.handleDismissInsecureKeyguard(display); + controller.scheduleGoingAwayTimeout(mDisplayId); hasChange = true; } else if (lastOccluded != mOccluded) { controller.handleOccludedChanged(mDisplayId, mTopOccludesActivity); diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java index 38df1b0e0511..4740fc45c6ba 100644 --- a/services/core/java/com/android/server/wm/LetterboxUiController.java +++ b/services/core/java/com/android/server/wm/LetterboxUiController.java @@ -492,6 +492,9 @@ final class LetterboxUiController { return; } + pw.println(prefix + "isTransparentPolicyRunning=" + + mActivityRecord.mAppCompatController.getTransparentPolicy().isRunning()); + boolean areBoundsLetterboxed = mainWin.areAppWindowBoundsLetterboxed(); pw.println(prefix + "areBoundsLetterboxed=" + areBoundsLetterboxed); if (!areBoundsLetterboxed) { diff --git a/services/core/java/com/android/server/wm/SnapshotController.java b/services/core/java/com/android/server/wm/SnapshotController.java index 99e1e8b1a5c6..0f9c001dffa8 100644 --- a/services/core/java/com/android/server/wm/SnapshotController.java +++ b/services/core/java/com/android/server/wm/SnapshotController.java @@ -19,8 +19,10 @@ package com.android.server.wm; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; import static android.view.WindowManager.TRANSIT_CLOSE; +import static android.view.WindowManager.TRANSIT_CLOSE_PREPARE_BACK_NAVIGATION; import static android.view.WindowManager.TRANSIT_FIRST_CUSTOM; import static android.view.WindowManager.TRANSIT_OPEN; +import static android.view.WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; @@ -202,10 +204,12 @@ class SnapshotController { } private static boolean isTransitionOpen(int type) { - return type == TRANSIT_OPEN || type == TRANSIT_TO_FRONT; + return type == TRANSIT_OPEN || type == TRANSIT_TO_FRONT + || type == TRANSIT_PREPARE_BACK_NAVIGATION; } private static boolean isTransitionClose(int type) { - return type == TRANSIT_CLOSE || type == TRANSIT_TO_BACK; + return type == TRANSIT_CLOSE || type == TRANSIT_TO_BACK + || type == TRANSIT_CLOSE_PREPARE_BACK_NAVIGATION; } void dump(PrintWriter pw, String prefix) { diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java index eaf3012a3b11..dba1c364b89b 100644 --- a/services/core/java/com/android/server/wm/TaskDisplayArea.java +++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java @@ -1079,8 +1079,11 @@ final class TaskDisplayArea extends DisplayArea<WindowContainer> { // Use launch-adjacent-flag-root if launching with launch-adjacent flag. if ((launchFlags & FLAG_ACTIVITY_LAUNCH_ADJACENT) != 0 && mLaunchAdjacentFlagRootTask != null) { - if (sourceTask != null && sourceTask == candidateTask) { - // Do nothing when task that is getting opened is same as the source. + if (sourceTask != null && (sourceTask == candidateTask + || sourceTask.topRunningActivity() == null)) { + // Do nothing when task that is getting opened is same as the source or when + // the source is no-longer valid. + Slog.w(TAG_WM, "Ignoring LAUNCH_ADJACENT because adjacent source is gone."); } else if (sourceTask != null && mLaunchAdjacentFlagRootTask.getAdjacentTask() != null && (sourceTask == mLaunchAdjacentFlagRootTask diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index 5698750170f4..3139e1816fbf 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -358,8 +358,12 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { return mToken; } - void addFlag(int flag) { - mFlags |= flag; + void addFlag(@TransitionFlags int flags) { + mFlags |= flags; + } + + void removeFlag(@TransitionFlags int flags) { + mFlags &= ~flags; } void calcParallelCollectType(WindowContainerTransaction wct) { diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index 0093e9d0788b..58c48ad3e9ac 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -22,6 +22,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.app.WindowConfiguration.WINDOW_CONFIG_BOUNDS; import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.WindowManager.TRANSIT_CLOSE_PREPARE_BACK_NAVIGATION; import static android.window.TaskFragmentOperation.OP_TYPE_CLEAR_ADJACENT_TASK_FRAGMENTS; import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE; import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_TASK_FRAGMENT; @@ -59,6 +60,7 @@ import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REMOVE_TASK; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REPARENT; +import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_RESTORE_BACK_NAVIGATION; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_RESTORE_TRANSIENT_ORDER; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_ADJACENT_ROOTS; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_ALWAYS_ON_TOP; @@ -436,6 +438,11 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub // the same transition instead of relying on this possible racing condition. return; } + if (transition.mType == TRANSIT_CLOSE_PREPARE_BACK_NAVIGATION + && mService.mBackNavigationController.restoreBackNavigationSetTransitionReady( + transition)) { + return; + } transition.setAllReady(); } @@ -1386,6 +1393,12 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub task.setTrimmableFromRecents(hop.isTrimmableFromRecents()); break; } + case HIERARCHY_OP_TYPE_RESTORE_BACK_NAVIGATION: { + if (mService.mBackNavigationController.restoreBackNavigation()) { + effects |= TRANSACT_EFFECTS_LIFECYCLE; + } + break; + } } return effects; } diff --git a/services/core/jni/com_android_server_utils_AnrTimer.cpp b/services/core/jni/com_android_server_utils_AnrTimer.cpp index 2edf129929cc..cf9611468fab 100644 --- a/services/core/jni/com_android_server_utils_AnrTimer.cpp +++ b/services/core/jni/com_android_server_utils_AnrTimer.cpp @@ -934,7 +934,6 @@ void AnrTimerService::scrubExpiredLocked() { } // Hold the lock in order to manage the running list. -// the listener. void AnrTimerService::expire(timer_id_t timerId) { // Save the timer attributes for the notification int pid = 0; @@ -967,7 +966,6 @@ void AnrTimerService::expire(timer_id_t timerId) { // Deliver the notification outside of the lock. if (expired) { if (!notifier_(timerId, pid, uid, elapsed, notifierCookie_, notifierObject_)) { - AutoMutex _l(lock_); // Notification failed, which means the listener will never call accept() or // discard(). Do not reinsert the timer. discard(timerId); diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java index 8e3248eaa6bf..f86d307d97bd 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java @@ -708,17 +708,15 @@ final class PolicyDefinition<V> { } @Nullable - static <V> PolicyKey readPolicyKeyFromXml(TypedXmlPullParser parser) + static PolicyKey readPolicyKeyFromXml(TypedXmlPullParser parser) throws XmlPullParserException, IOException { - // TODO: can we avoid casting? PolicyKey policyKey = PolicyKey.readGenericPolicyKeyFromXml(parser); if (policyKey == null) { Slogf.wtf(TAG, "Error parsing PolicyKey, GenericPolicyKey is null"); return null; } - PolicyDefinition<PolicyValue<V>> genericPolicyDefinition = - (PolicyDefinition<PolicyValue<V>>) POLICY_DEFINITIONS.get( - policyKey.getIdentifier()); + PolicyDefinition<?> genericPolicyDefinition = + POLICY_DEFINITIONS.get(policyKey.getIdentifier()); if (genericPolicyDefinition == null) { Slogf.wtf(TAG, "Error parsing PolicyKey, Unknown generic policy key: " + policyKey); return null; 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 0def51691efa..8753b251ac98 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/MouseKeysInterceptorTest.kt +++ b/services/tests/servicestests/src/com/android/server/accessibility/MouseKeysInterceptorTest.kt @@ -45,7 +45,6 @@ import org.mockito.ArgumentCaptor import org.mockito.Mock import org.mockito.Mockito import org.mockito.MockitoAnnotations -import java.util.concurrent.TimeUnit import java.util.LinkedList import java.util.Queue import android.util.ArraySet @@ -117,9 +116,6 @@ class MouseKeysInterceptorTest { Mockito.`when`(mockAms.traceManager).thenReturn(mockTraceManager) 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) mouseKeysInterceptor.next = nextInterceptor } diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecMessageValidatorTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecMessageValidatorTest.java index 473d1dc22d7a..1074f7b4aa0a 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecMessageValidatorTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecMessageValidatorTest.java @@ -240,6 +240,9 @@ public class HdmiCecMessageValidatorTest { public void isValid_setAnalogueTimer_clearAnalogueTimer() { assertMessageValidity("04:33:0C:08:10:1E:04:30:08:00:13:AD:06").isEqualTo(OK); assertMessageValidity("04:34:04:0C:16:0F:08:37:00:02:EA:60:03:34").isEqualTo(OK); + // Allow [Recording Sequence] set multiple days of the week. + // e.g. Monday (0x02) | Tuesday (0x04) -> Monday or Tuesday (0x06) + assertMessageValidity("04:34:04:0C:16:0F:08:37:06:02:EA:60:03:34").isEqualTo(OK); assertMessageValidity("0F:33:0C:08:10:1E:04:30:08:00:13:AD:06") .isEqualTo(ERROR_DESTINATION); @@ -308,7 +311,7 @@ public class HdmiCecMessageValidatorTest { // Invalid Recording Sequence assertMessageValidity("04:99:12:06:0C:2D:5A:19:90:91:04:00:B1").isEqualTo(ERROR_PARAMETER); // Invalid Recording Sequence - assertMessageValidity("04:97:0C:08:15:05:04:1E:21:00:C4:C2:11:D8:75:30") + assertMessageValidity("04:97:0C:08:15:05:04:1E:A1:00:C4:C2:11:D8:75:30") .isEqualTo(ERROR_PARAMETER); // Invalid Digital Broadcast System @@ -354,7 +357,7 @@ public class HdmiCecMessageValidatorTest { // Invalid Recording Sequence assertMessageValidity("40:A2:14:09:12:28:4B:19:84:05:10:00").isEqualTo(ERROR_PARAMETER); // Invalid Recording Sequence - assertMessageValidity("40:A1:0C:08:15:05:04:1E:14:04:20").isEqualTo(ERROR_PARAMETER); + assertMessageValidity("40:A1:0C:08:15:05:04:1E:94:04:20").isEqualTo(ERROR_PARAMETER); // Invalid external source specifier assertMessageValidity("40:A2:14:09:12:28:4B:19:10:08:10:00").isEqualTo(ERROR_PARAMETER); // Invalid External PLug diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java index de70280ee0a9..14ad15e23791 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java @@ -15,7 +15,9 @@ */ package com.android.server.notification; +import static android.app.Notification.FLAG_AUTOGROUP_SUMMARY; import static android.app.Notification.FLAG_BUBBLE; +import static android.app.Notification.FLAG_GROUP_SUMMARY; import static android.app.Notification.GROUP_ALERT_ALL; import static android.app.Notification.GROUP_ALERT_CHILDREN; import static android.app.Notification.GROUP_ALERT_SUMMARY; @@ -539,6 +541,36 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { return r; } + private NotificationRecord getAutogroupSummaryNotificationRecord(int id, String groupKey, + int groupAlertBehavior, UserHandle userHandle, String packageName) { + final Builder builder = new Builder(getContext()) + .setContentTitle("foo") + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setPriority(Notification.PRIORITY_HIGH) + .setFlag(FLAG_GROUP_SUMMARY | FLAG_AUTOGROUP_SUMMARY, true); + + int defaults = 0; + defaults |= Notification.DEFAULT_SOUND; + mChannel.setSound(Settings.System.DEFAULT_NOTIFICATION_URI, + Notification.AUDIO_ATTRIBUTES_DEFAULT); + + builder.setDefaults(defaults); + builder.setGroup(groupKey); + builder.setGroupAlertBehavior(groupAlertBehavior); + Notification n = builder.build(); + + Context context = spy(getContext()); + PackageManager packageManager = spy(context.getPackageManager()); + when(context.getPackageManager()).thenReturn(packageManager); + when(packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)).thenReturn(false); + + StatusBarNotification sbn = new StatusBarNotification(packageName, packageName, id, mTag, + mUid, mPid, n, userHandle, null, System.currentTimeMillis()); + NotificationRecord r = new NotificationRecord(context, sbn, mChannel); + mService.addNotification(r); + return r; + } + // // Convenience functions for interacting with mocks // @@ -2603,6 +2635,79 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { } @Test + public void testBeepVolume_politeNotif_justSummaries() throws Exception { + mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS); + mSetFlagsRule.disableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS); + TestableFlagResolver flagResolver = new TestableFlagResolver(); + flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME1, 50); + flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME2, 0); + // NOTIFICATION_COOLDOWN_ALL setting is enabled + Settings.System.putInt(getContext().getContentResolver(), + Settings.System.NOTIFICATION_COOLDOWN_ALL, 1); + initAttentionHelper(flagResolver); + + NotificationRecord summary = getBeepyNotificationRecord("a", GROUP_ALERT_ALL); + summary.getNotification().flags |= Notification.FLAG_GROUP_SUMMARY; + + // first update at 100% volume + mAttentionHelper.buzzBeepBlinkLocked(summary, DEFAULT_SIGNALS); + assertNotEquals(-1, summary.getLastAudiblyAlertedMs()); + verifyBeepVolume(1.0f); + Mockito.reset(mRingtonePlayer); + + // update should beep at 50% volume + summary = getBeepyNotificationRecord("a", GROUP_ALERT_ALL); + summary.getNotification().flags |= Notification.FLAG_GROUP_SUMMARY; + mAttentionHelper.buzzBeepBlinkLocked(summary, DEFAULT_SIGNALS); + assertNotEquals(-1, summary.getLastAudiblyAlertedMs()); + verifyBeepVolume(0.5f); + Mockito.reset(mRingtonePlayer); + + // next update at 0% volume + mAttentionHelper.buzzBeepBlinkLocked(summary, DEFAULT_SIGNALS); + assertEquals(-1, summary.getLastAudiblyAlertedMs()); + verifyBeepVolume(0.0f); + + verify(mAccessibilityService, times(3)).sendAccessibilityEvent(any(), anyInt()); + } + + @Test + public void testBeepVolume_politeNotif_autogroupSummary() throws Exception { + mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS); + mSetFlagsRule.disableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS); + TestableFlagResolver flagResolver = new TestableFlagResolver(); + flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME1, 50); + flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME2, 0); + // NOTIFICATION_COOLDOWN_ALL setting is enabled + Settings.System.putInt(getContext().getContentResolver(), + Settings.System.NOTIFICATION_COOLDOWN_ALL, 1); + initAttentionHelper(flagResolver); + + // child should beep at 100% volume + NotificationRecord child = getBeepyNotificationRecord("a", GROUP_ALERT_ALL); + mAttentionHelper.buzzBeepBlinkLocked(child, DEFAULT_SIGNALS); + assertNotEquals(-1, child.getLastAudiblyAlertedMs()); + verifyBeepVolume(1.0f); + Mockito.reset(mRingtonePlayer); + + // summary 0% volume (GROUP_ALERT_CHILDREN) + NotificationRecord summary = getAutogroupSummaryNotificationRecord(mId, "a", + GROUP_ALERT_CHILDREN, mUser, mPkg); + mAttentionHelper.buzzBeepBlinkLocked(summary, DEFAULT_SIGNALS); + verifyNeverBeep(); + assertFalse(summary.isInterruptive()); + assertEquals(-1, summary.getLastAudiblyAlertedMs()); + Mockito.reset(mRingtonePlayer); + + // next update at 50% volume because autogroup summary was ignored + mAttentionHelper.buzzBeepBlinkLocked(child, DEFAULT_SIGNALS); + assertNotEquals(-1, child.getLastAudiblyAlertedMs()); + verifyBeepVolume(0.5f); + + verify(mAccessibilityService, times(3)).sendAccessibilityEvent(any(), anyInt()); + } + + @Test public void testBeepVolume_politeNotif_applyPerApp() throws Exception { mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS); mSetFlagsRule.disableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index 4bb80170f9fa..5a8de58c14ae 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -13064,6 +13064,100 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test @DisableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) + public void testCancelAutogroupSummary_cancelsAllChildren() throws Exception { + final String originalGroupName = "originalGroup"; + final String aggregateGroupName = "Aggregate_Test"; + final int summaryId = Integer.MAX_VALUE; + // Add 2 group notifications without a summary + NotificationRecord nr0 = + generateNotificationRecord(mTestNotificationChannel, 0, originalGroupName, false); + NotificationRecord nr1 = + generateNotificationRecord(mTestNotificationChannel, 1, originalGroupName, false); + mService.addNotification(nr0); + mService.addNotification(nr1); + mService.mSummaryByGroupKey.remove(nr0.getGroupKey()); + + // GroupHelper is a mock, so make the calls it would make + // Add aggregate group summary + NotificationAttributes attr = new NotificationAttributes(GroupHelper.BASE_FLAGS, + mock(Icon.class), 0, VISIBILITY_PRIVATE, GROUP_ALERT_CHILDREN, + nr0.getChannel().getId()); + NotificationRecord aggregateSummary = mService.createAutoGroupSummary(nr0.getUserId(), + nr0.getSbn().getPackageName(), nr0.getKey(), aggregateGroupName, summaryId, attr); + mService.addNotification(aggregateSummary); + nr0.setOverrideGroupKey(aggregateGroupName); + nr1.setOverrideGroupKey(aggregateGroupName); + final String fullAggregateGroupKey = nr0.getGroupKey(); + + // Check that the aggregate group summary was created + assertThat(aggregateSummary.getNotification().getGroup()).isEqualTo(aggregateGroupName); + assertThat(aggregateSummary.getNotification().getChannelId()).isEqualTo( + nr0.getChannel().getId()); + assertThat(mService.mSummaryByGroupKey.containsKey(fullAggregateGroupKey)).isTrue(); + + // Cancel aggregate group summary + mBinderService.cancelNotificationWithTag(mPkg, mPkg, aggregateSummary.getSbn().getTag(), + aggregateSummary.getSbn().getId(), aggregateSummary.getSbn().getUserId()); + waitForIdle(); + + // Check that child notifications are also removed + verify(mGroupHelper, times(1)).onNotificationRemoved(eq(aggregateSummary)); + verify(mGroupHelper, times(1)).onNotificationRemoved(eq(nr0)); + verify(mGroupHelper, times(1)).onNotificationRemoved(eq(nr1)); + + // Make sure the summary was removed and not re-posted + assertThat(mService.getNotificationRecordCount()).isEqualTo(0); + } + + @Test + @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) + public void testCancelAutogroupSummary_forceGrouping_cancelsAllChildren() throws Exception { + final String originalGroupName = "originalGroup"; + final String aggregateGroupName = "Aggregate_Test"; + final int summaryId = Integer.MAX_VALUE; + // Add 2 group notifications without a summary + NotificationRecord nr0 = + generateNotificationRecord(mTestNotificationChannel, 0, originalGroupName, false); + NotificationRecord nr1 = + generateNotificationRecord(mTestNotificationChannel, 1, originalGroupName, false); + mService.addNotification(nr0); + mService.addNotification(nr1); + mService.mSummaryByGroupKey.remove(nr0.getGroupKey()); + + // GroupHelper is a mock, so make the calls it would make + // Add aggregate group summary + NotificationAttributes attr = new NotificationAttributes(GroupHelper.BASE_FLAGS, + mock(Icon.class), 0, VISIBILITY_PRIVATE, GROUP_ALERT_CHILDREN, + nr0.getChannel().getId()); + NotificationRecord aggregateSummary = mService.createAutoGroupSummary(nr0.getUserId(), + nr0.getSbn().getPackageName(), nr0.getKey(), aggregateGroupName, summaryId, attr); + mService.addNotification(aggregateSummary); + nr0.setOverrideGroupKey(aggregateGroupName); + nr1.setOverrideGroupKey(aggregateGroupName); + final String fullAggregateGroupKey = nr0.getGroupKey(); + + // Check that the aggregate group summary was created + assertThat(aggregateSummary.getNotification().getGroup()).isEqualTo(aggregateGroupName); + assertThat(aggregateSummary.getNotification().getChannelId()).isEqualTo( + nr0.getChannel().getId()); + assertThat(mService.mSummaryByGroupKey.containsKey(fullAggregateGroupKey)).isTrue(); + + // Cancel aggregate group summary + mBinderService.cancelNotificationWithTag(mPkg, mPkg, aggregateSummary.getSbn().getTag(), + aggregateSummary.getSbn().getId(), aggregateSummary.getSbn().getUserId()); + waitForIdle(); + + // Check that child notifications are also removed + verify(mGroupHelper, times(1)).onNotificationRemoved(eq(aggregateSummary), any()); + verify(mGroupHelper, times(1)).onNotificationRemoved(eq(nr0), any()); + verify(mGroupHelper, times(1)).onNotificationRemoved(eq(nr1), any()); + + // Make sure the summary was removed and not re-posted + assertThat(mService.getNotificationRecordCount()).isEqualTo(0); + } + + @Test + @DisableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) public void testUngroupingOngoingAutoSummary() throws Exception { NotificationRecord nr0 = generateNotificationRecord(mTestNotificationChannel, 0); diff --git a/services/tests/wmtests/src/com/android/server/policy/ShortcutLoggingTests.java b/services/tests/wmtests/src/com/android/server/policy/KeyboardSystemShortcutTests.java index aa28147a3973..e26f3e0f699a 100644 --- a/services/tests/wmtests/src/com/android/server/policy/ShortcutLoggingTests.java +++ b/services/tests/wmtests/src/com/android/server/policy/KeyboardSystemShortcutTests.java @@ -22,6 +22,7 @@ import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_HOME_ASSIS import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_HOME_NOTIFICATION_PANEL; import static com.android.server.policy.PhoneWindowManager.SETTINGS_KEY_BEHAVIOR_NOTIFICATION_PANEL; +import android.hardware.input.KeyboardSystemShortcut; import android.platform.test.annotations.Presubmit; import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; @@ -31,7 +32,6 @@ import android.view.KeyEvent; import androidx.test.filters.MediumTest; import com.android.internal.annotations.Keep; -import com.android.server.input.KeyboardMetricsCollector.KeyboardLogEvent; import junitparams.JUnitParamsRunner; import junitparams.Parameters; @@ -44,15 +44,12 @@ import org.junit.runner.RunWith; @Presubmit @MediumTest @RunWith(JUnitParamsRunner.class) -public class ShortcutLoggingTests extends ShortcutKeyTestBase { +public class KeyboardSystemShortcutTests extends ShortcutKeyTestBase { @Rule public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); - private static final int VENDOR_ID = 0x123; - private static final int PRODUCT_ID = 0x456; - private static final int DEVICE_BUS = 0x789; private static final int META_KEY = KeyEvent.KEYCODE_META_LEFT; private static final int META_ON = MODIFIER.get(KeyEvent.KEYCODE_META_LEFT); private static final int ALT_KEY = KeyEvent.KEYCODE_ALT_LEFT; @@ -64,245 +61,316 @@ public class ShortcutLoggingTests extends ShortcutKeyTestBase { @Keep private static Object[][] shortcutTestArguments() { - // testName, testKeys, expectedLogEvent, expectedKey, expectedModifierState + // testName, testKeys, expectedSystemShortcut, expectedKey, expectedModifierState return new Object[][]{ {"Meta + H -> Open Home", new int[]{META_KEY, KeyEvent.KEYCODE_H}, - KeyboardLogEvent.HOME, KeyEvent.KEYCODE_H, META_ON}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_HOME, KeyEvent.KEYCODE_H, META_ON}, {"Meta + Enter -> Open Home", new int[]{META_KEY, KeyEvent.KEYCODE_ENTER}, - KeyboardLogEvent.HOME, KeyEvent.KEYCODE_ENTER, META_ON}, - {"HOME key -> Open Home", new int[]{KeyEvent.KEYCODE_HOME}, KeyboardLogEvent.HOME, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_HOME, KeyEvent.KEYCODE_ENTER, + META_ON}, + {"HOME key -> Open Home", new int[]{KeyEvent.KEYCODE_HOME}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_HOME, KeyEvent.KEYCODE_HOME, 0}, {"RECENT_APPS key -> Open Overview", new int[]{KeyEvent.KEYCODE_RECENT_APPS}, - KeyboardLogEvent.RECENT_APPS, KeyEvent.KEYCODE_RECENT_APPS, 0}, - {"Meta + Tab -> Open OVerview", new int[]{META_KEY, KeyEvent.KEYCODE_TAB}, - KeyboardLogEvent.RECENT_APPS, KeyEvent.KEYCODE_TAB, META_ON}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_RECENT_APPS, + KeyEvent.KEYCODE_RECENT_APPS, 0}, + {"Meta + Tab -> Open Overview", new int[]{META_KEY, KeyEvent.KEYCODE_TAB}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_RECENT_APPS, KeyEvent.KEYCODE_TAB, + META_ON}, {"Alt + Tab -> Open Overview", new int[]{ALT_KEY, KeyEvent.KEYCODE_TAB}, - KeyboardLogEvent.RECENT_APPS, KeyEvent.KEYCODE_TAB, ALT_ON}, - {"BACK key -> Go back", new int[]{KeyEvent.KEYCODE_BACK}, KeyboardLogEvent.BACK, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_RECENT_APPS, KeyEvent.KEYCODE_TAB, + ALT_ON}, + {"BACK key -> Go back", new int[]{KeyEvent.KEYCODE_BACK}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_BACK, KeyEvent.KEYCODE_BACK, 0}, {"Meta + Escape -> Go back", new int[]{META_KEY, KeyEvent.KEYCODE_ESCAPE}, - KeyboardLogEvent.BACK, KeyEvent.KEYCODE_ESCAPE, META_ON}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_BACK, KeyEvent.KEYCODE_ESCAPE, + META_ON}, {"Meta + Left arrow -> Go back", new int[]{META_KEY, KeyEvent.KEYCODE_DPAD_LEFT}, - KeyboardLogEvent.BACK, KeyEvent.KEYCODE_DPAD_LEFT, META_ON}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_BACK, KeyEvent.KEYCODE_DPAD_LEFT, + META_ON}, {"Meta + Del -> Go back", new int[]{META_KEY, KeyEvent.KEYCODE_DEL}, - KeyboardLogEvent.BACK, KeyEvent.KEYCODE_DEL, META_ON}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_BACK, KeyEvent.KEYCODE_DEL, META_ON}, {"APP_SWITCH key -> Open App switcher", new int[]{KeyEvent.KEYCODE_APP_SWITCH}, - KeyboardLogEvent.APP_SWITCH, KeyEvent.KEYCODE_APP_SWITCH, 0}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_APP_SWITCH, + KeyEvent.KEYCODE_APP_SWITCH, 0}, {"ASSIST key -> Launch assistant", new int[]{KeyEvent.KEYCODE_ASSIST}, - KeyboardLogEvent.LAUNCH_ASSISTANT, KeyEvent.KEYCODE_ASSIST, 0}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_ASSISTANT, + KeyEvent.KEYCODE_ASSIST, 0}, {"Meta + A -> Launch assistant", new int[]{META_KEY, KeyEvent.KEYCODE_A}, - KeyboardLogEvent.LAUNCH_ASSISTANT, KeyEvent.KEYCODE_A, META_ON}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_ASSISTANT, KeyEvent.KEYCODE_A, + META_ON}, {"VOICE_ASSIST key -> Launch Voice Assistant", new int[]{KeyEvent.KEYCODE_VOICE_ASSIST}, - KeyboardLogEvent.LAUNCH_VOICE_ASSISTANT, KeyEvent.KEYCODE_VOICE_ASSIST, 0}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_VOICE_ASSISTANT, + KeyEvent.KEYCODE_VOICE_ASSIST, 0}, {"Meta + I -> Launch System Settings", new int[]{META_KEY, KeyEvent.KEYCODE_I}, - KeyboardLogEvent.LAUNCH_SYSTEM_SETTINGS, KeyEvent.KEYCODE_I, META_ON}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_SYSTEM_SETTINGS, + KeyEvent.KEYCODE_I, META_ON}, {"Meta + N -> Toggle Notification panel", new int[]{META_KEY, KeyEvent.KEYCODE_N}, - KeyboardLogEvent.TOGGLE_NOTIFICATION_PANEL, KeyEvent.KEYCODE_N, META_ON}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_TOGGLE_NOTIFICATION_PANEL, + KeyEvent.KEYCODE_N, META_ON}, {"NOTIFICATION key -> Toggle Notification Panel", new int[]{KeyEvent.KEYCODE_NOTIFICATION}, - KeyboardLogEvent.TOGGLE_NOTIFICATION_PANEL, KeyEvent.KEYCODE_NOTIFICATION, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_TOGGLE_NOTIFICATION_PANEL, + KeyEvent.KEYCODE_NOTIFICATION, 0}, {"Meta + Ctrl + S -> Take Screenshot", new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_S}, - KeyboardLogEvent.TAKE_SCREENSHOT, KeyEvent.KEYCODE_S, META_ON | CTRL_ON}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_TAKE_SCREENSHOT, KeyEvent.KEYCODE_S, + META_ON | CTRL_ON}, {"Meta + / -> Open Shortcut Helper", new int[]{META_KEY, KeyEvent.KEYCODE_SLASH}, - KeyboardLogEvent.OPEN_SHORTCUT_HELPER, KeyEvent.KEYCODE_SLASH, META_ON}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_OPEN_SHORTCUT_HELPER, + KeyEvent.KEYCODE_SLASH, META_ON}, {"BRIGHTNESS_UP key -> Increase Brightness", - new int[]{KeyEvent.KEYCODE_BRIGHTNESS_UP}, KeyboardLogEvent.BRIGHTNESS_UP, + new int[]{KeyEvent.KEYCODE_BRIGHTNESS_UP}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_BRIGHTNESS_UP, KeyEvent.KEYCODE_BRIGHTNESS_UP, 0}, {"BRIGHTNESS_DOWN key -> Decrease Brightness", new int[]{KeyEvent.KEYCODE_BRIGHTNESS_DOWN}, - KeyboardLogEvent.BRIGHTNESS_DOWN, KeyEvent.KEYCODE_BRIGHTNESS_DOWN, 0}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_BRIGHTNESS_DOWN, + KeyEvent.KEYCODE_BRIGHTNESS_DOWN, 0}, {"KEYBOARD_BACKLIGHT_UP key -> Increase Keyboard Backlight", new int[]{KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_UP}, - KeyboardLogEvent.KEYBOARD_BACKLIGHT_UP, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_UP, KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_UP, 0}, {"KEYBOARD_BACKLIGHT_DOWN key -> Decrease Keyboard Backlight", new int[]{KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_DOWN}, - KeyboardLogEvent.KEYBOARD_BACKLIGHT_DOWN, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_DOWN, KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_DOWN, 0}, {"KEYBOARD_BACKLIGHT_TOGGLE key -> Toggle Keyboard Backlight", new int[]{KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_TOGGLE}, - KeyboardLogEvent.KEYBOARD_BACKLIGHT_TOGGLE, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_TOGGLE, KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_TOGGLE, 0}, {"VOLUME_UP key -> Increase Volume", new int[]{KeyEvent.KEYCODE_VOLUME_UP}, - KeyboardLogEvent.VOLUME_UP, KeyEvent.KEYCODE_VOLUME_UP, 0}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_VOLUME_UP, + KeyEvent.KEYCODE_VOLUME_UP, 0}, {"VOLUME_DOWN key -> Decrease Volume", new int[]{KeyEvent.KEYCODE_VOLUME_DOWN}, - KeyboardLogEvent.VOLUME_DOWN, KeyEvent.KEYCODE_VOLUME_DOWN, 0}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_VOLUME_DOWN, + KeyEvent.KEYCODE_VOLUME_DOWN, 0}, {"VOLUME_MUTE key -> Mute Volume", new int[]{KeyEvent.KEYCODE_VOLUME_MUTE}, - KeyboardLogEvent.VOLUME_MUTE, KeyEvent.KEYCODE_VOLUME_MUTE, 0}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_VOLUME_MUTE, + KeyEvent.KEYCODE_VOLUME_MUTE, 0}, {"ALL_APPS key -> Open App Drawer in Accessibility mode", new int[]{KeyEvent.KEYCODE_ALL_APPS}, - KeyboardLogEvent.ACCESSIBILITY_ALL_APPS, KeyEvent.KEYCODE_ALL_APPS, 0}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_ACCESSIBILITY_ALL_APPS, + KeyEvent.KEYCODE_ALL_APPS, 0}, {"SEARCH key -> Launch Search Activity", new int[]{KeyEvent.KEYCODE_SEARCH}, - KeyboardLogEvent.LAUNCH_SEARCH, KeyEvent.KEYCODE_SEARCH, 0}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_SEARCH, + KeyEvent.KEYCODE_SEARCH, 0}, {"LANGUAGE_SWITCH key -> Switch Keyboard Language", new int[]{KeyEvent.KEYCODE_LANGUAGE_SWITCH}, - KeyboardLogEvent.LANGUAGE_SWITCH, KeyEvent.KEYCODE_LANGUAGE_SWITCH, 0}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_LANGUAGE_SWITCH, + KeyEvent.KEYCODE_LANGUAGE_SWITCH, 0}, {"META key -> Open App Drawer in Accessibility mode", new int[]{META_KEY}, - KeyboardLogEvent.ACCESSIBILITY_ALL_APPS, META_KEY, META_ON}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_ACCESSIBILITY_ALL_APPS, META_KEY, + META_ON}, {"Meta + Alt -> Toggle CapsLock", new int[]{META_KEY, ALT_KEY}, - KeyboardLogEvent.TOGGLE_CAPS_LOCK, ALT_KEY, META_ON | ALT_ON}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_TOGGLE_CAPS_LOCK, ALT_KEY, + META_ON | ALT_ON}, {"Alt + Meta -> Toggle CapsLock", new int[]{ALT_KEY, META_KEY}, - KeyboardLogEvent.TOGGLE_CAPS_LOCK, META_KEY, META_ON | ALT_ON}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_TOGGLE_CAPS_LOCK, META_KEY, + META_ON | ALT_ON}, {"CAPS_LOCK key -> Toggle CapsLock", new int[]{KeyEvent.KEYCODE_CAPS_LOCK}, - KeyboardLogEvent.TOGGLE_CAPS_LOCK, KeyEvent.KEYCODE_CAPS_LOCK, 0}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_TOGGLE_CAPS_LOCK, + KeyEvent.KEYCODE_CAPS_LOCK, 0}, {"MUTE key -> Mute System Microphone", new int[]{KeyEvent.KEYCODE_MUTE}, - KeyboardLogEvent.SYSTEM_MUTE, KeyEvent.KEYCODE_MUTE, 0}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_SYSTEM_MUTE, KeyEvent.KEYCODE_MUTE, + 0}, {"Meta + Ctrl + DPAD_UP -> Split screen navigation", new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_DPAD_UP}, - KeyboardLogEvent.MULTI_WINDOW_NAVIGATION, KeyEvent.KEYCODE_DPAD_UP, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_MULTI_WINDOW_NAVIGATION, + KeyEvent.KEYCODE_DPAD_UP, META_ON | CTRL_ON}, {"Meta + Ctrl + DPAD_LEFT -> Split screen navigation", new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_DPAD_LEFT}, - KeyboardLogEvent.SPLIT_SCREEN_NAVIGATION, KeyEvent.KEYCODE_DPAD_LEFT, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_SPLIT_SCREEN_NAVIGATION, + KeyEvent.KEYCODE_DPAD_LEFT, META_ON | CTRL_ON}, {"Meta + Ctrl + DPAD_RIGHT -> Split screen navigation", new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_DPAD_RIGHT}, - KeyboardLogEvent.SPLIT_SCREEN_NAVIGATION, KeyEvent.KEYCODE_DPAD_RIGHT, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_SPLIT_SCREEN_NAVIGATION, + KeyEvent.KEYCODE_DPAD_RIGHT, META_ON | CTRL_ON}, {"Meta + L -> Lock Homescreen", new int[]{META_KEY, KeyEvent.KEYCODE_L}, - KeyboardLogEvent.LOCK_SCREEN, KeyEvent.KEYCODE_L, META_ON}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_LOCK_SCREEN, KeyEvent.KEYCODE_L, + META_ON}, {"Meta + Ctrl + N -> Open Notes", new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_N}, - KeyboardLogEvent.OPEN_NOTES, KeyEvent.KEYCODE_N, META_ON | CTRL_ON}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_OPEN_NOTES, KeyEvent.KEYCODE_N, + META_ON | CTRL_ON}, {"POWER key -> Toggle Power", new int[]{KeyEvent.KEYCODE_POWER}, - KeyboardLogEvent.TOGGLE_POWER, KeyEvent.KEYCODE_POWER, 0}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_TOGGLE_POWER, KeyEvent.KEYCODE_POWER, + 0}, {"TV_POWER key -> Toggle Power", new int[]{KeyEvent.KEYCODE_TV_POWER}, - KeyboardLogEvent.TOGGLE_POWER, KeyEvent.KEYCODE_TV_POWER, 0}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_TOGGLE_POWER, + KeyEvent.KEYCODE_TV_POWER, 0}, {"SYSTEM_NAVIGATION_DOWN key -> System Navigation", new int[]{KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN}, - KeyboardLogEvent.SYSTEM_NAVIGATION, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_SYSTEM_NAVIGATION, + KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN, 0}, {"SYSTEM_NAVIGATION_UP key -> System Navigation", new int[]{KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP}, - KeyboardLogEvent.SYSTEM_NAVIGATION, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_SYSTEM_NAVIGATION, + KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP, 0}, {"SYSTEM_NAVIGATION_LEFT key -> System Navigation", new int[]{KeyEvent.KEYCODE_SYSTEM_NAVIGATION_LEFT}, - KeyboardLogEvent.SYSTEM_NAVIGATION, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_LEFT, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_SYSTEM_NAVIGATION, + KeyEvent.KEYCODE_SYSTEM_NAVIGATION_LEFT, 0}, {"SYSTEM_NAVIGATION_RIGHT key -> System Navigation", new int[]{KeyEvent.KEYCODE_SYSTEM_NAVIGATION_RIGHT}, - KeyboardLogEvent.SYSTEM_NAVIGATION, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_SYSTEM_NAVIGATION, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_RIGHT, 0}, {"SLEEP key -> System Sleep", new int[]{KeyEvent.KEYCODE_SLEEP}, - KeyboardLogEvent.SLEEP, KeyEvent.KEYCODE_SLEEP, 0}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_SLEEP, KeyEvent.KEYCODE_SLEEP, 0}, {"SOFT_SLEEP key -> System Sleep", new int[]{KeyEvent.KEYCODE_SOFT_SLEEP}, - KeyboardLogEvent.SLEEP, KeyEvent.KEYCODE_SOFT_SLEEP, 0}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_SLEEP, KeyEvent.KEYCODE_SOFT_SLEEP, + 0}, {"WAKEUP key -> System Wakeup", new int[]{KeyEvent.KEYCODE_WAKEUP}, - KeyboardLogEvent.WAKEUP, KeyEvent.KEYCODE_WAKEUP, 0}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_WAKEUP, KeyEvent.KEYCODE_WAKEUP, 0}, {"MEDIA_PLAY key -> Media Control", new int[]{KeyEvent.KEYCODE_MEDIA_PLAY}, - KeyboardLogEvent.MEDIA_KEY, KeyEvent.KEYCODE_MEDIA_PLAY, 0}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_MEDIA_KEY, + KeyEvent.KEYCODE_MEDIA_PLAY, 0}, {"MEDIA_PAUSE key -> Media Control", new int[]{KeyEvent.KEYCODE_MEDIA_PAUSE}, - KeyboardLogEvent.MEDIA_KEY, KeyEvent.KEYCODE_MEDIA_PAUSE, 0}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_MEDIA_KEY, + KeyEvent.KEYCODE_MEDIA_PAUSE, 0}, {"MEDIA_PLAY_PAUSE key -> Media Control", - new int[]{KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE}, KeyboardLogEvent.MEDIA_KEY, + new int[]{KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_MEDIA_KEY, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, 0}, {"Meta + B -> Launch Default Browser", new int[]{META_KEY, KeyEvent.KEYCODE_B}, - KeyboardLogEvent.LAUNCH_DEFAULT_BROWSER, KeyEvent.KEYCODE_B, META_ON}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_DEFAULT_BROWSER, + KeyEvent.KEYCODE_B, META_ON}, {"EXPLORER key -> Launch Default Browser", new int[]{KeyEvent.KEYCODE_EXPLORER}, - KeyboardLogEvent.LAUNCH_DEFAULT_BROWSER, KeyEvent.KEYCODE_EXPLORER, 0}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_DEFAULT_BROWSER, + KeyEvent.KEYCODE_EXPLORER, 0}, {"Meta + C -> Launch Default Contacts", new int[]{META_KEY, KeyEvent.KEYCODE_C}, - KeyboardLogEvent.LAUNCH_DEFAULT_CONTACTS, KeyEvent.KEYCODE_C, META_ON}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CONTACTS, + KeyEvent.KEYCODE_C, META_ON}, {"CONTACTS key -> Launch Default Contacts", new int[]{KeyEvent.KEYCODE_CONTACTS}, - KeyboardLogEvent.LAUNCH_DEFAULT_CONTACTS, KeyEvent.KEYCODE_CONTACTS, 0}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CONTACTS, + KeyEvent.KEYCODE_CONTACTS, 0}, {"Meta + E -> Launch Default Email", new int[]{META_KEY, KeyEvent.KEYCODE_E}, - KeyboardLogEvent.LAUNCH_DEFAULT_EMAIL, KeyEvent.KEYCODE_E, META_ON}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_DEFAULT_EMAIL, + KeyEvent.KEYCODE_E, META_ON}, {"ENVELOPE key -> Launch Default Email", new int[]{KeyEvent.KEYCODE_ENVELOPE}, - KeyboardLogEvent.LAUNCH_DEFAULT_EMAIL, KeyEvent.KEYCODE_ENVELOPE, 0}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_DEFAULT_EMAIL, + KeyEvent.KEYCODE_ENVELOPE, 0}, {"Meta + K -> Launch Default Calendar", new int[]{META_KEY, KeyEvent.KEYCODE_K}, - KeyboardLogEvent.LAUNCH_DEFAULT_CALENDAR, KeyEvent.KEYCODE_K, META_ON}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CALENDAR, + KeyEvent.KEYCODE_K, META_ON}, {"CALENDAR key -> Launch Default Calendar", new int[]{KeyEvent.KEYCODE_CALENDAR}, - KeyboardLogEvent.LAUNCH_DEFAULT_CALENDAR, KeyEvent.KEYCODE_CALENDAR, 0}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CALENDAR, + KeyEvent.KEYCODE_CALENDAR, 0}, {"Meta + P -> Launch Default Music", new int[]{META_KEY, KeyEvent.KEYCODE_P}, - KeyboardLogEvent.LAUNCH_DEFAULT_MUSIC, KeyEvent.KEYCODE_P, META_ON}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MUSIC, + KeyEvent.KEYCODE_P, META_ON}, {"MUSIC key -> Launch Default Music", new int[]{KeyEvent.KEYCODE_MUSIC}, - KeyboardLogEvent.LAUNCH_DEFAULT_MUSIC, KeyEvent.KEYCODE_MUSIC, 0}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MUSIC, + KeyEvent.KEYCODE_MUSIC, 0}, {"Meta + U -> Launch Default Calculator", new int[]{META_KEY, KeyEvent.KEYCODE_U}, - KeyboardLogEvent.LAUNCH_DEFAULT_CALCULATOR, KeyEvent.KEYCODE_U, META_ON}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CALCULATOR, + KeyEvent.KEYCODE_U, META_ON}, {"CALCULATOR key -> Launch Default Calculator", new int[]{KeyEvent.KEYCODE_CALCULATOR}, - KeyboardLogEvent.LAUNCH_DEFAULT_CALCULATOR, KeyEvent.KEYCODE_CALCULATOR, 0}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CALCULATOR, + KeyEvent.KEYCODE_CALCULATOR, 0}, {"Meta + M -> Launch Default Maps", new int[]{META_KEY, KeyEvent.KEYCODE_M}, - KeyboardLogEvent.LAUNCH_DEFAULT_MAPS, KeyEvent.KEYCODE_M, META_ON}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MAPS, + KeyEvent.KEYCODE_M, META_ON}, {"Meta + S -> Launch Default Messaging App", new int[]{META_KEY, KeyEvent.KEYCODE_S}, - KeyboardLogEvent.LAUNCH_DEFAULT_MESSAGING, KeyEvent.KEYCODE_S, META_ON}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MESSAGING, + KeyEvent.KEYCODE_S, META_ON}, {"Meta + Ctrl + DPAD_DOWN -> Enter desktop mode", new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_DPAD_DOWN}, - KeyboardLogEvent.DESKTOP_MODE, KeyEvent.KEYCODE_DPAD_DOWN, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_DESKTOP_MODE, + KeyEvent.KEYCODE_DPAD_DOWN, META_ON | CTRL_ON}}; } @Keep private static Object[][] longPressOnHomeTestArguments() { - // testName, testKeys, longPressOnHomeBehavior, expectedLogEvent, expectedKey, + // testName, testKeys, longPressOnHomeBehavior, expectedSystemShortcut, expectedKey, // expectedModifierState return new Object[][]{ {"Long press HOME key -> Toggle Notification panel", new int[]{KeyEvent.KEYCODE_HOME}, LONG_PRESS_HOME_NOTIFICATION_PANEL, - KeyboardLogEvent.TOGGLE_NOTIFICATION_PANEL, KeyEvent.KEYCODE_HOME, 0}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_TOGGLE_NOTIFICATION_PANEL, + KeyEvent.KEYCODE_HOME, 0}, {"Long press META + ENTER -> Toggle Notification panel", new int[]{META_KEY, KeyEvent.KEYCODE_ENTER}, LONG_PRESS_HOME_NOTIFICATION_PANEL, - KeyboardLogEvent.TOGGLE_NOTIFICATION_PANEL, KeyEvent.KEYCODE_ENTER, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_TOGGLE_NOTIFICATION_PANEL, + KeyEvent.KEYCODE_ENTER, META_ON}, {"Long press META + H -> Toggle Notification panel", new int[]{META_KEY, KeyEvent.KEYCODE_H}, LONG_PRESS_HOME_NOTIFICATION_PANEL, - KeyboardLogEvent.TOGGLE_NOTIFICATION_PANEL, KeyEvent.KEYCODE_H, META_ON}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_TOGGLE_NOTIFICATION_PANEL, + KeyEvent.KEYCODE_H, META_ON}, {"Long press HOME key -> Launch assistant", new int[]{KeyEvent.KEYCODE_HOME}, LONG_PRESS_HOME_ASSIST, - KeyboardLogEvent.LAUNCH_ASSISTANT, KeyEvent.KEYCODE_HOME, 0}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_ASSISTANT, + KeyEvent.KEYCODE_HOME, 0}, {"Long press META + ENTER -> Launch assistant", new int[]{META_KEY, KeyEvent.KEYCODE_ENTER}, LONG_PRESS_HOME_ASSIST, - KeyboardLogEvent.LAUNCH_ASSISTANT, KeyEvent.KEYCODE_ENTER, META_ON}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_ASSISTANT, + KeyEvent.KEYCODE_ENTER, META_ON}, {"Long press META + H -> Launch assistant", new int[]{META_KEY, KeyEvent.KEYCODE_H}, LONG_PRESS_HOME_ASSIST, - KeyboardLogEvent.LAUNCH_ASSISTANT, KeyEvent.KEYCODE_H, META_ON}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_ASSISTANT, KeyEvent.KEYCODE_H, + META_ON}, {"Long press HOME key -> Open App Drawer in Accessibility mode", new int[]{KeyEvent.KEYCODE_HOME}, LONG_PRESS_HOME_ALL_APPS, - KeyboardLogEvent.ACCESSIBILITY_ALL_APPS, KeyEvent.KEYCODE_HOME, 0}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_ACCESSIBILITY_ALL_APPS, + KeyEvent.KEYCODE_HOME, 0}, {"Long press META + ENTER -> Open App Drawer in Accessibility mode", new int[]{META_KEY, KeyEvent.KEYCODE_ENTER}, LONG_PRESS_HOME_ALL_APPS, - KeyboardLogEvent.ACCESSIBILITY_ALL_APPS, KeyEvent.KEYCODE_ENTER, META_ON}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_ACCESSIBILITY_ALL_APPS, + KeyEvent.KEYCODE_ENTER, META_ON}, {"Long press META + H -> Open App Drawer in Accessibility mode", new int[]{META_KEY, KeyEvent.KEYCODE_H}, - LONG_PRESS_HOME_ALL_APPS, KeyboardLogEvent.ACCESSIBILITY_ALL_APPS, + LONG_PRESS_HOME_ALL_APPS, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_ACCESSIBILITY_ALL_APPS, KeyEvent.KEYCODE_H, META_ON}}; } @Keep private static Object[][] doubleTapOnHomeTestArguments() { - // testName, testKeys, doubleTapOnHomeBehavior, expectedLogEvent, expectedKey, + // testName, testKeys, doubleTapOnHomeBehavior, expectedSystemShortcut, expectedKey, // expectedModifierState return new Object[][]{ {"Double tap HOME -> Open App switcher", new int[]{KeyEvent.KEYCODE_HOME}, DOUBLE_TAP_HOME_RECENT_SYSTEM_UI, - KeyboardLogEvent.APP_SWITCH, KeyEvent.KEYCODE_HOME, 0}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_APP_SWITCH, KeyEvent.KEYCODE_HOME, + 0}, {"Double tap META + ENTER -> Open App switcher", new int[]{META_KEY, KeyEvent.KEYCODE_ENTER}, - DOUBLE_TAP_HOME_RECENT_SYSTEM_UI, KeyboardLogEvent.APP_SWITCH, + DOUBLE_TAP_HOME_RECENT_SYSTEM_UI, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_APP_SWITCH, KeyEvent.KEYCODE_ENTER, META_ON}, {"Double tap META + H -> Open App switcher", new int[]{META_KEY, KeyEvent.KEYCODE_H}, DOUBLE_TAP_HOME_RECENT_SYSTEM_UI, - KeyboardLogEvent.APP_SWITCH, KeyEvent.KEYCODE_H, META_ON}}; + KeyboardSystemShortcut.SYSTEM_SHORTCUT_APP_SWITCH, KeyEvent.KEYCODE_H, + META_ON}}; } @Keep private static Object[][] settingsKeyTestArguments() { - // testName, testKeys, settingsKeyBehavior, expectedLogEvent, expectedKey, + // testName, testKeys, settingsKeyBehavior, expectedSystemShortcut, expectedKey, // expectedModifierState return new Object[][]{ {"SETTINGS key -> Toggle Notification panel", new int[]{KeyEvent.KEYCODE_SETTINGS}, SETTINGS_KEY_BEHAVIOR_NOTIFICATION_PANEL, - KeyboardLogEvent.TOGGLE_NOTIFICATION_PANEL, KeyEvent.KEYCODE_SETTINGS, 0}}; + KeyboardSystemShortcut.SYSTEM_SHORTCUT_TOGGLE_NOTIFICATION_PANEL, + KeyEvent.KEYCODE_SETTINGS, 0}}; } @Before public void setUp() { setUpPhoneWindowManager(/*supportSettingsUpdate*/ true); - mPhoneWindowManager.overrideKeyEventSource(VENDOR_ID, PRODUCT_ID, DEVICE_BUS); mPhoneWindowManager.overrideLaunchHome(); mPhoneWindowManager.overrideSearchKeyBehavior( PhoneWindowManager.SEARCH_KEY_BEHAVIOR_TARGET_ACTIVITY); @@ -318,56 +386,64 @@ public class ShortcutLoggingTests extends ShortcutKeyTestBase { @Test @Parameters(method = "shortcutTestArguments") - public void testShortcuts(String testName, int[] testKeys, KeyboardLogEvent expectedLogEvent, - int expectedKey, int expectedModifierState) { - sendKeyCombination(testKeys, 0 /* duration */); - mPhoneWindowManager.assertShortcutLogged(VENDOR_ID, PRODUCT_ID, expectedLogEvent, - expectedKey, expectedModifierState, DEVICE_BUS, - "Failed while executing " + testName); + public void testShortcut(String testName, int[] testKeys, + @KeyboardSystemShortcut.SystemShortcut int expectedSystemShortcut, int expectedKey, + int expectedModifierState) { + testShortcutInternal(testName, testKeys, expectedSystemShortcut, expectedKey, + expectedModifierState); } @Test @Parameters(method = "longPressOnHomeTestArguments") public void testLongPressOnHome(String testName, int[] testKeys, int longPressOnHomeBehavior, - KeyboardLogEvent expectedLogEvent, int expectedKey, int expectedModifierState) { + @KeyboardSystemShortcut.SystemShortcut int expectedSystemShortcut, int expectedKey, + int expectedModifierState) { mPhoneWindowManager.overrideLongPressOnHomeBehavior(longPressOnHomeBehavior); sendLongPressKeyCombination(testKeys); - mPhoneWindowManager.assertShortcutLogged(VENDOR_ID, PRODUCT_ID, expectedLogEvent, - expectedKey, expectedModifierState, DEVICE_BUS, + mPhoneWindowManager.assertKeyboardShortcutTriggered( + new int[]{expectedKey}, expectedModifierState, expectedSystemShortcut, "Failed while executing " + testName); } @Test @Parameters(method = "doubleTapOnHomeTestArguments") public void testDoubleTapOnHomeBehavior(String testName, int[] testKeys, - int doubleTapOnHomeBehavior, KeyboardLogEvent expectedLogEvent, int expectedKey, + int doubleTapOnHomeBehavior, + @KeyboardSystemShortcut.SystemShortcut int expectedSystemShortcut, int expectedKey, int expectedModifierState) { mPhoneWindowManager.overriderDoubleTapOnHomeBehavior(doubleTapOnHomeBehavior); sendKeyCombination(testKeys, 0 /* duration */); sendKeyCombination(testKeys, 0 /* duration */); - mPhoneWindowManager.assertShortcutLogged(VENDOR_ID, PRODUCT_ID, expectedLogEvent, - expectedKey, expectedModifierState, DEVICE_BUS, + mPhoneWindowManager.assertKeyboardShortcutTriggered( + new int[]{expectedKey}, expectedModifierState, expectedSystemShortcut, "Failed while executing " + testName); } @Test @Parameters(method = "settingsKeyTestArguments") - public void testSettingsKey(String testName, int[] testKeys, - int settingsKeyBehavior, KeyboardLogEvent expectedLogEvent, int expectedKey, + public void testSettingsKey(String testName, int[] testKeys, int settingsKeyBehavior, + @KeyboardSystemShortcut.SystemShortcut int expectedSystemShortcut, int expectedKey, int expectedModifierState) { mPhoneWindowManager.overrideSettingsKeyBehavior(settingsKeyBehavior); - sendKeyCombination(testKeys, 0 /* duration */); - mPhoneWindowManager.assertShortcutLogged(VENDOR_ID, PRODUCT_ID, expectedLogEvent, - expectedKey, expectedModifierState, DEVICE_BUS, - "Failed while executing " + testName); + testShortcutInternal(testName, testKeys, expectedSystemShortcut, expectedKey, + expectedModifierState); } @Test @RequiresFlagsEnabled(com.android.server.flags.Flags.FLAG_NEW_BUGREPORT_KEYBOARD_SHORTCUT) public void testBugreportShortcutPress() { - sendKeyCombination(new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_DEL}, 0); - mPhoneWindowManager.assertShortcutLogged(VENDOR_ID, PRODUCT_ID, - KeyboardLogEvent.TRIGGER_BUG_REPORT, KeyEvent.KEYCODE_DEL, META_ON | CTRL_ON, - DEVICE_BUS, "Failed to log bugreport shortcut."); + testShortcutInternal("Meta + Ctrl + Del -> Trigger bug report", + new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_DEL}, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_TRIGGER_BUG_REPORT, KeyEvent.KEYCODE_DEL, + META_ON | CTRL_ON); + } + + private void testShortcutInternal(String testName, int[] testKeys, + @KeyboardSystemShortcut.SystemShortcut int expectedSystemShortcut, int expectedKey, + int expectedModifierState) { + sendKeyCombination(testKeys, 0 /* duration */); + mPhoneWindowManager.assertKeyboardShortcutTriggered( + new int[]{expectedKey}, expectedModifierState, expectedSystemShortcut, + "Failed while executing " + testName); } } diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java index 6f8c91c97af4..f9b5c2a6c77f 100644 --- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java +++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java @@ -26,7 +26,6 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.any; import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyInt; import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyLong; import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyString; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.description; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; @@ -50,6 +49,7 @@ import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.CALLS_REAL_METHODS; import static org.mockito.Mockito.after; +import static org.mockito.Mockito.description; import static org.mockito.Mockito.mockingDetails; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.withSettings; @@ -70,6 +70,7 @@ import android.hardware.SensorPrivacyManager; import android.hardware.display.DisplayManager; import android.hardware.display.DisplayManagerInternal; import android.hardware.input.InputManager; +import android.hardware.input.KeyboardSystemShortcut; import android.media.AudioManagerInternal; import android.os.Handler; import android.os.HandlerThread; @@ -85,7 +86,6 @@ import android.os.test.TestLooper; import android.service.dreams.DreamManagerInternal; import android.telecom.TelecomManager; import android.view.Display; -import android.view.InputDevice; import android.view.KeyEvent; import android.view.accessibility.AccessibilityManager; import android.view.autofill.AutofillManagerInternal; @@ -93,11 +93,9 @@ import android.view.autofill.AutofillManagerInternal; import com.android.dx.mockito.inline.extended.StaticMockitoSession; import com.android.internal.accessibility.AccessibilityShortcutController; import com.android.internal.policy.KeyInterceptionInfo; -import com.android.internal.util.FrameworkStatsLog; import com.android.server.GestureLauncherService; import com.android.server.LocalServices; import com.android.server.input.InputManagerInternal; -import com.android.server.input.KeyboardMetricsCollector.KeyboardLogEvent; import com.android.server.inputmethod.InputMethodManagerInternal; import com.android.server.pm.UserManagerInternal; import com.android.server.policy.keyguard.KeyguardServiceDelegate; @@ -269,7 +267,6 @@ class TestPhoneWindowManager { // Return mocked services: LocalServices.getService mMockitoSession = mockitoSession() .mockStatic(LocalServices.class, spyStubOnly) - .mockStatic(FrameworkStatsLog.class) .strictness(Strictness.LENIENT) .startMocking(); @@ -583,19 +580,6 @@ class TestPhoneWindowManager { doReturn(mPackageManager).when(mContext).getPackageManager(); } - void overrideKeyEventSource(int vendorId, int productId, int deviceBus) { - InputDevice device = new InputDevice.Builder() - .setId(1) - .setVendorId(vendorId) - .setProductId(productId) - .setDeviceBus(deviceBus) - .setSources(InputDevice.SOURCE_KEYBOARD) - .setKeyboardType(InputDevice.KEYBOARD_TYPE_ALPHABETIC) - .build(); - doReturn(mInputManager).when(mContext).getSystemService(eq(InputManager.class)); - doReturn(device).when(mInputManager).getInputDevice(anyInt()); - } - void overrideInjectKeyEvent() { doReturn(true).when(mInputManager).injectInputEvent(any(KeyEvent.class), anyInt()); } @@ -820,12 +804,11 @@ class TestPhoneWindowManager { Assert.assertEquals(targetActivity, intentCaptor.getValue().getComponent()); } - void assertShortcutLogged(int vendorId, int productId, KeyboardLogEvent logEvent, - int expectedKey, int expectedModifierState, int deviceBus, String errorMsg) { + void assertKeyboardShortcutTriggered(int[] keycodes, int modifierState, int systemShortcut, + String errorMsg) { mTestLooper.dispatchAll(); - verify(() -> FrameworkStatsLog.write(FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED, - vendorId, productId, logEvent.getIntValue(), new int[]{expectedKey}, - expectedModifierState, deviceBus), description(errorMsg)); + verify(mInputManagerInternal, description(errorMsg)).notifyKeyboardShortcutTriggered( + anyInt(), eq(keycodes), eq(modifierState), eq(systemShortcut)); } void assertSwitchToTask(int persistentId) throws RemoteException { diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java index f8cf97e71274..a74572431d6b 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java @@ -36,9 +36,12 @@ import android.content.ComponentName; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.res.Configuration; +import android.graphics.Rect; import android.view.Surface; +import androidx.annotation.CallSuper; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.android.server.wm.utils.TestComponentStack; @@ -74,19 +77,36 @@ class AppCompatActivityRobot { private final int mDisplayHeight; private DisplayContent mDisplayContent; + @Nullable + private Consumer<ActivityRecord> mOnPostActivityCreation; + + @Nullable + private Consumer<DisplayContent> mOnPostDisplayContentCreation; + AppCompatActivityRobot(@NonNull WindowManagerService wm, @NonNull ActivityTaskManagerService atm, @NonNull ActivityTaskSupervisor supervisor, - int displayWidth, int displayHeight) { + int displayWidth, int displayHeight, + @Nullable Consumer<ActivityRecord> onPostActivityCreation, + @Nullable Consumer<DisplayContent> onPostDisplayContentCreation) { mAtm = atm; mSupervisor = supervisor; mDisplayWidth = displayWidth; mDisplayHeight = displayHeight; mActivityStack = new TestComponentStack<>(); mTaskStack = new TestComponentStack<>(); + mOnPostActivityCreation = onPostActivityCreation; + mOnPostDisplayContentCreation = onPostDisplayContentCreation; createNewDisplay(); } AppCompatActivityRobot(@NonNull WindowManagerService wm, + @NonNull ActivityTaskManagerService atm, @NonNull ActivityTaskSupervisor supervisor, + int displayWidth, int displayHeight) { + this(wm, atm, supervisor, displayWidth, displayHeight, /* onPostActivityCreation */ null, + /* onPostDisplayContentCreation */ null); + } + + AppCompatActivityRobot(@NonNull WindowManagerService wm, @NonNull ActivityTaskManagerService atm, @NonNull ActivityTaskSupervisor supervisor) { this(wm, atm, supervisor, DEFAULT_DISPLAY_WIDTH, DEFAULT_DISPLAY_HEIGHT); } @@ -96,6 +116,10 @@ class AppCompatActivityRobot { /* inNewDisplay */ false); } + void createActivityWithComponentWithoutTask() { + createActivityWithComponentInNewTask(/* inNewTask */ false, /* inNewDisplay */ false); + } + void createActivityWithComponentInNewTask() { createActivityWithComponentInNewTask(/* inNewTask */ true, /* inNewDisplay */ false); } @@ -104,7 +128,6 @@ class AppCompatActivityRobot { createActivityWithComponentInNewTask(/* inNewTask */ true, /* inNewDisplay */ true); } - void configureTopActivity(float minAspect, float maxAspect, int screenOrientation, boolean isUnresizable) { prepareLimitedBounds(mActivityStack.top(), minAspect, maxAspect, screenOrientation, @@ -130,6 +153,14 @@ class AppCompatActivityRobot { doReturn(naturalOrientation).when(mDisplayContent).getNaturalOrientation(); } + void configureTaskBounds(@NonNull Rect taskBounds) { + doReturn(taskBounds).when(mTaskStack.top()).getBounds(); + } + + void configureTopActivityBounds(@NonNull Rect activityBounds) { + doReturn(activityBounds).when(mActivityStack.top()).getBounds(); + } + @NonNull ActivityRecord top() { return mActivityStack.top(); @@ -169,6 +200,10 @@ class AppCompatActivityRobot { .isActivityEligibleForOrientationOverride(eq(mActivityStack.top())); } + void setTopActivityInTransition(boolean inTransition) { + doReturn(inTransition).when(mActivityStack.top()).isInTransition(); + } + void setShouldApplyUserMinAspectRatioOverride(boolean enabled) { doReturn(enabled).when(mActivityStack.top().mAppCompatController .getAppCompatAspectRatioOverrides()).shouldApplyUserMinAspectRatioOverride(); @@ -238,21 +273,20 @@ class AppCompatActivityRobot { void createNewDisplay() { mDisplayContent = new TestDisplayContent.Builder(mAtm, mDisplayWidth, mDisplayHeight) .build(); - spyOn(mDisplayContent); - spyOnAppCompatCameraPolicy(); + onPostDisplayContentCreation(mDisplayContent); } void createNewTask() { final Task newTask = new WindowTestsBase.TaskBuilder(mSupervisor) .setDisplay(mDisplayContent).build(); - pushTask(newTask); + mTaskStack.push(newTask); } void createNewTaskWithBaseActivity() { final Task newTask = new WindowTestsBase.TaskBuilder(mSupervisor) .setCreateActivity(true) .setDisplay(mDisplayContent).build(); - pushTask(newTask); + mTaskStack.push(newTask); pushActivity(newTask.getTopNonFinishingActivity()); } @@ -378,6 +412,34 @@ class AppCompatActivityRobot { pushActivity(newActivity); } + /** + * Specific Robots can override this method to add operation to run on a newly created + * {@link ActivityRecord}. Common case is to invoke spyOn(). + * + * @param activity The newly created {@link ActivityRecord}. + */ + @CallSuper + void onPostActivityCreation(@NonNull ActivityRecord activity) { + spyOn(activity.mLetterboxUiController); + if (mOnPostActivityCreation != null) { + mOnPostActivityCreation.accept(activity); + } + } + + /** + * Specific Robots can override this method to add operation to run on a newly created + * {@link DisplayContent}. Common case is to invoke spyOn(). + * + * @param displayContent The newly created {@link DisplayContent}. + */ + @CallSuper + void onPostDisplayContentCreation(@NonNull DisplayContent displayContent) { + spyOn(mDisplayContent); + if (mOnPostDisplayContentCreation != null) { + mOnPostDisplayContentCreation.accept(mDisplayContent); + } + } + private void createActivityWithComponentInNewTask(boolean inNewTask, boolean inNewDisplay) { if (inNewDisplay) { createNewDisplay(); @@ -385,14 +447,16 @@ class AppCompatActivityRobot { if (inNewTask) { createNewTask(); } - final ActivityRecord activity = new WindowTestsBase.ActivityBuilder(mAtm) - .setOnTop(true) - .setTask(mTaskStack.top()) + final WindowTestsBase.ActivityBuilder activityBuilder = + new WindowTestsBase.ActivityBuilder(mAtm).setOnTop(true) // Set the component to be that of the test class in order // to enable compat changes - .setComponent(ComponentName.createRelative(mAtm.mContext, TEST_COMPONENT_NAME)) - .build(); - pushActivity(activity); + .setComponent(ComponentName.createRelative(mAtm.mContext, TEST_COMPONENT_NAME)); + if (!mTaskStack.isEmpty()) { + // We put the Activity in the current task if any. + activityBuilder.setTask(mTaskStack.top()); + } + pushActivity(activityBuilder.build()); } /** @@ -438,28 +502,6 @@ class AppCompatActivityRobot { // We add the activity to the stack and spyOn() on its properties. private void pushActivity(@NonNull ActivityRecord activity) { mActivityStack.push(activity); - spyOn(activity); - // TODO (b/351763164): Use these spyOn calls only when necessary. - spyOn(activity.mAppCompatController.getTransparentPolicy()); - spyOn(activity.mAppCompatController.getAppCompatAspectRatioOverrides()); - spyOn(activity.mAppCompatController.getAppCompatAspectRatioPolicy()); - spyOn(activity.mAppCompatController.getAppCompatFocusOverrides()); - spyOn(activity.mAppCompatController.getAppCompatResizeOverrides()); - spyOn(activity.mLetterboxUiController); - } - - private void pushTask(@NonNull Task task) { - spyOn(task); - mTaskStack.push(task); - } - - private void spyOnAppCompatCameraPolicy() { - spyOn(mDisplayContent.mAppCompatCameraPolicy); - if (mDisplayContent.mAppCompatCameraPolicy.hasDisplayRotationCompatPolicy()) { - spyOn(mDisplayContent.mAppCompatCameraPolicy.mDisplayRotationCompatPolicy); - } - if (mDisplayContent.mAppCompatCameraPolicy.hasCameraCompatFreeformPolicy()) { - spyOn(mDisplayContent.mAppCompatCameraPolicy.mCameraCompatFreeformPolicy); - } + onPostActivityCreation(activity); } } diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatAspectRatioOverridesTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatAspectRatioOverridesTest.java index a6fd11210307..1e40aa0c8da8 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatAspectRatioOverridesTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatAspectRatioOverridesTest.java @@ -291,7 +291,6 @@ public class AppCompatAspectRatioOverridesTest extends WindowTestsBase { * Runs a test scenario providing a Robot. */ void runTestScenario(@NonNull Consumer<AspectRatioOverridesRobotTest> consumer) { - spyOn(mWm.mAppCompatConfiguration); final AspectRatioOverridesRobotTest robot = new AspectRatioOverridesRobotTest(mWm, mAtm, mSupervisor); consumer.accept(robot); @@ -305,6 +304,18 @@ public class AppCompatAspectRatioOverridesTest extends WindowTestsBase { super(wm, atm, supervisor); } + @Override + void onPostDisplayContentCreation(@NonNull DisplayContent displayContent) { + super.onPostDisplayContentCreation(displayContent); + spyOn(displayContent.mAppCompatCameraPolicy); + } + + @Override + void onPostActivityCreation(@NonNull ActivityRecord activity) { + super.onPostActivityCreation(activity); + spyOn(activity.mAppCompatController.getAppCompatAspectRatioOverrides()); + } + void checkShouldApplyUserFullscreenOverride(boolean expected) { assertEquals(expected, getTopActivityAppCompatAspectRatioOverrides() .shouldApplyUserFullscreenOverride()); diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraOverridesTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraOverridesTest.java index de99f546ab07..84ffcb8956a9 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraOverridesTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraOverridesTest.java @@ -387,6 +387,12 @@ public class AppCompatCameraOverridesTest extends WindowTestsBase { super(wm, atm, supervisor); } + @Override + void onPostDisplayContentCreation(@NonNull DisplayContent displayContent) { + super.onPostDisplayContentCreation(displayContent); + spyOn(displayContent.mAppCompatCameraPolicy); + } + void checkShouldRefreshActivityForCameraCompat(boolean expected) { Assert.assertEquals(getAppCompatCameraOverrides() .shouldRefreshActivityForCameraCompat(), expected); diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java index 0b1bb0f75a09..c42228dcc6ba 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java @@ -150,6 +150,12 @@ public class AppCompatCameraPolicyTest extends WindowTestsBase { super(wm, atm, supervisor); } + @Override + void onPostDisplayContentCreation(@NonNull DisplayContent displayContent) { + super.onPostDisplayContentCreation(displayContent); + spyOn(displayContent.mAppCompatCameraPolicy); + } + void checkTopActivityHasDisplayRotationCompatPolicy(boolean exists) { Assert.assertEquals(exists, activity().top().mDisplayContent .mAppCompatCameraPolicy.hasDisplayRotationCompatPolicy()); diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationRobot.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationRobot.java index 6592f2625ab6..40a53479e9ab 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationRobot.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationRobot.java @@ -19,6 +19,9 @@ package com.android.server.wm; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import androidx.annotation.NonNull; @@ -80,4 +83,34 @@ class AppCompatConfigurationRobot { doReturn(aspectRatio).when(mAppCompatConfiguration) .getFixedOrientationLetterboxAspectRatio(); } + + void setThinLetterboxWidthPx(int thinWidthPx) { + doReturn(thinWidthPx).when(mAppCompatConfiguration) + .getThinLetterboxWidthPx(); + } + + void setThinLetterboxHeightPx(int thinHeightPx) { + doReturn(thinHeightPx).when(mAppCompatConfiguration) + .getThinLetterboxHeightPx(); + } + + void checkToNextLeftStop(boolean invoked) { + verify(mAppCompatConfiguration, times(invoked ? 1 : 0)) + .movePositionForHorizontalReachabilityToNextLeftStop(anyBoolean()); + } + + void checkToNextRightStop(boolean invoked) { + verify(mAppCompatConfiguration, times(invoked ? 1 : 0)) + .movePositionForHorizontalReachabilityToNextRightStop(anyBoolean()); + } + + void checkToNextBottomStop(boolean invoked) { + verify(mAppCompatConfiguration, times(invoked ? 1 : 0)) + .movePositionForVerticalReachabilityToNextBottomStop(anyBoolean()); + } + + void checkToNextTopStop(boolean invoked) { + verify(mAppCompatConfiguration, times(invoked ? 1 : 0)) + .movePositionForVerticalReachabilityToNextTopStop(anyBoolean()); + } } diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationOverridesTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationOverridesTest.java index 6c0d8c4269af..d9b5f37be86c 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationOverridesTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationOverridesTest.java @@ -250,6 +250,12 @@ public class AppCompatOrientationOverridesTest extends WindowTestsBase { mTestCurrentTimeMillisSupplier = new CurrentTimeMillisSupplierFake(); } + @Override + void onPostActivityCreation(@NonNull ActivityRecord activity) { + super.onPostActivityCreation(activity); + spyOn(activity.mAppCompatController.getAppCompatAspectRatioPolicy()); + } + // Useful to reduce timeout during tests void prepareMockedTime() { getTopOrientationOverrides().mOrientationOverridesState.mCurrentTimeMillisSupplier = diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java index ad34a6b0fc87..f6d0744a10c4 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java @@ -536,6 +536,25 @@ public class AppCompatOrientationPolicyTest extends WindowTestsBase { } } + @Override + void onPostActivityCreation(@NonNull ActivityRecord activity) { + super.onPostActivityCreation(activity); + spyOn(activity.mAppCompatController.getAppCompatAspectRatioOverrides()); + spyOn(activity.mAppCompatController.getAppCompatAspectRatioPolicy()); + } + + @Override + void onPostDisplayContentCreation(@NonNull DisplayContent displayContent) { + super.onPostDisplayContentCreation(displayContent); + spyOn(displayContent.mAppCompatCameraPolicy); + if (displayContent.mAppCompatCameraPolicy.hasDisplayRotationCompatPolicy()) { + spyOn(displayContent.mAppCompatCameraPolicy.mDisplayRotationCompatPolicy); + } + if (displayContent.mAppCompatCameraPolicy.hasCameraCompatFreeformPolicy()) { + spyOn(displayContent.mAppCompatCameraPolicy.mCameraCompatFreeformPolicy); + } + } + void prepareRelaunchingAfterRequestedOrientationChanged(boolean enabled) { getTopOrientationOverrides().setRelaunchingAfterRequestedOrientationChanged(enabled); } diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityOverridesTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityOverridesTest.java new file mode 100644 index 000000000000..5ff8f0200fa3 --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityOverridesTest.java @@ -0,0 +1,228 @@ +/* + * 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.server.wm; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; + +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; + +import android.compat.testing.PlatformCompatChangeRule; +import android.graphics.Rect; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.annotations.Presubmit; + +import androidx.annotation.NonNull; + +import com.android.window.flags.Flags; + +import junit.framework.Assert; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; +import org.junit.runner.RunWith; + +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * Test class for {@link AppCompatReachabilityOverrides}. + * <p> + * Build/Install/Run: + * atest WmTests:AppCompatReachabilityOverridesTest + */ +@Presubmit +@RunWith(WindowTestRunner.class) +public class AppCompatReachabilityOverridesTest extends WindowTestsBase { + + @Rule + public TestRule compatChangeRule = new PlatformCompatChangeRule(); + + @Test + public void testIsThinLetterboxed_NegativePx_returnsFalse() { + runTestScenario((robot) -> { + robot.activity().createActivityWithComponentWithoutTask(); + robot.conf().setThinLetterboxHeightPx(/* thinHeightPx */ -1); + robot.checkIsVerticalThinLetterboxed(/* expected */ false); + + robot.conf().setThinLetterboxWidthPx(/* thinHeightPx */ -1); + robot.checkIsHorizontalThinLetterboxed(/* expected */ false); + }); + } + + @Test + public void testIsThinLetterboxed_noTask_returnsFalse() { + runTestScenario((robot) -> { + robot.activity().createActivityWithComponentWithoutTask(); + robot.conf().setThinLetterboxHeightPx(/* thinHeightPx */ 10); + robot.checkIsVerticalThinLetterboxed(/* expected */ false); + + robot.conf().setThinLetterboxWidthPx(/* thinHeightPx */ 10); + robot.checkIsHorizontalThinLetterboxed(/* expected */ false); + }); + } + + @Test + public void testIsVerticalThinLetterboxed() { + runTestScenario((robot) -> { + robot.conf().setThinLetterboxHeightPx(/* thinHeightPx */ 10); + robot.applyOnActivity((a) -> { + a.createActivityWithComponent(); + a.configureTaskBounds(new Rect(0, 0, 100, 100)); + + // (task.width() - act.width()) / 2 = 5 < 10 + a.configureTopActivityBounds(new Rect(5, 5, 95, 95)); + robot.checkIsVerticalThinLetterboxed(/* expected */ true); + + // (task.width() - act.width()) / 2 = 10 = 10 + a.configureTopActivityBounds(new Rect(10, 10, 90, 90)); + robot.checkIsVerticalThinLetterboxed(/* expected */ true); + + // (task.width() - act.width()) / 2 = 11 > 10 + a.configureTopActivityBounds(new Rect(11, 11, 89, 89)); + robot.checkIsVerticalThinLetterboxed(/* expected */ false); + }); + }); + } + + @Test + public void testIsHorizontalThinLetterboxed() { + runTestScenario((robot) -> { + robot.conf().setThinLetterboxWidthPx(/* thinHeightPx */ 10); + robot.applyOnActivity((a) -> { + a.createActivityWithComponent(); + a.configureTaskBounds(new Rect(0, 0, 100, 100)); + + // (task.height() - act.height()) / 2 = 5 < 10 + a.configureTopActivityBounds(new Rect(5, 5, 95, 95)); + robot.checkIsHorizontalThinLetterboxed(/* expected */ true); + + // (task.height() - act.height()) / 2 = 10 = 10 + a.configureTopActivityBounds(new Rect(10, 10, 90, 90)); + robot.checkIsHorizontalThinLetterboxed(/* expected */ true); + + // (task.height() - act.height()) / 2 = 11 > 10 + a.configureTopActivityBounds(new Rect(11, 11, 89, 89)); + robot.checkIsHorizontalThinLetterboxed(/* expected */ false); + }); + }); + } + + @Test + @EnableFlags(Flags.FLAG_DISABLE_THIN_LETTERBOXING_POLICY) + public void testAllowReachabilityForThinLetterboxWithFlagEnabled() { + runTestScenario((robot) -> { + robot.activity().createActivityWithComponent(); + + robot.configureIsVerticalThinLetterboxed(/* isThin */ true); + robot.checkAllowVerticalReachabilityForThinLetterbox(/* expected */ false); + robot.configureIsHorizontalThinLetterboxed(/* isThin */ true); + robot.checkAllowHorizontalReachabilityForThinLetterbox(/* expected */ false); + + robot.configureIsVerticalThinLetterboxed(/* isThin */ false); + robot.checkAllowVerticalReachabilityForThinLetterbox(/* expected */ true); + robot.configureIsHorizontalThinLetterboxed(/* isThin */ false); + robot.checkAllowHorizontalReachabilityForThinLetterbox(/* expected */ true); + }); + } + + @Test + @DisableFlags(Flags.FLAG_DISABLE_THIN_LETTERBOXING_POLICY) + public void testAllowReachabilityForThinLetterboxWithFlagDisabled() { + runTestScenario((robot) -> { + robot.activity().createActivityWithComponent(); + + robot.configureIsVerticalThinLetterboxed(/* isThin */ true); + robot.checkAllowVerticalReachabilityForThinLetterbox(/* expected */ true); + robot.configureIsHorizontalThinLetterboxed(/* isThin */ true); + robot.checkAllowHorizontalReachabilityForThinLetterbox(/* expected */ true); + + robot.configureIsVerticalThinLetterboxed(/* isThin */ false); + robot.checkAllowVerticalReachabilityForThinLetterbox(/* expected */ true); + robot.configureIsHorizontalThinLetterboxed(/* isThin */ false); + robot.checkAllowHorizontalReachabilityForThinLetterbox(/* expected */ true); + }); + } + + /** + * Runs a test scenario providing a Robot. + */ + void runTestScenario(@NonNull Consumer<ReachabilityOverridesRobotTest> consumer) { + spyOn(mWm.mAppCompatConfiguration); + final ReachabilityOverridesRobotTest robot = + new ReachabilityOverridesRobotTest(mWm, mAtm, mSupervisor); + consumer.accept(robot); + } + + private static class ReachabilityOverridesRobotTest extends AppCompatRobotBase { + + private final Supplier<Rect> mLetterboxInnerBoundsSupplier = spy(Rect::new); + + ReachabilityOverridesRobotTest(@NonNull WindowManagerService wm, + @NonNull ActivityTaskManagerService atm, + @NonNull ActivityTaskSupervisor supervisor) { + super(wm, atm, supervisor); + } + + @Override + void onPostActivityCreation(@NonNull ActivityRecord activity) { + super.onPostActivityCreation(activity); + spyOn(activity.mAppCompatController.getAppCompatReachabilityOverrides()); + activity.mAppCompatController.getAppCompatReachabilityPolicy() + .setLetterboxInnerBoundsSupplier(mLetterboxInnerBoundsSupplier); + } + + void configureIsVerticalThinLetterboxed(boolean isThin) { + doReturn(isThin).when(getAppCompatReachabilityOverrides()) + .isVerticalThinLetterboxed(); + } + + void configureIsHorizontalThinLetterboxed(boolean isThin) { + doReturn(isThin).when(getAppCompatReachabilityOverrides()) + .isHorizontalThinLetterboxed(); + } + + void checkIsVerticalThinLetterboxed(boolean expected) { + Assert.assertEquals(expected, + getAppCompatReachabilityOverrides().isVerticalThinLetterboxed()); + } + + void checkIsHorizontalThinLetterboxed(boolean expected) { + Assert.assertEquals(expected, + getAppCompatReachabilityOverrides().isHorizontalThinLetterboxed()); + } + + void checkAllowVerticalReachabilityForThinLetterbox(boolean expected) { + Assert.assertEquals(expected, getAppCompatReachabilityOverrides() + .allowVerticalReachabilityForThinLetterbox()); + } + + void checkAllowHorizontalReachabilityForThinLetterbox(boolean expected) { + Assert.assertEquals(expected, getAppCompatReachabilityOverrides() + .allowHorizontalReachabilityForThinLetterbox()); + } + + @NonNull + private AppCompatReachabilityOverrides getAppCompatReachabilityOverrides() { + return activity().top().mAppCompatController.getAppCompatReachabilityOverrides(); + } + + } + +} diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityPolicyTest.java new file mode 100644 index 000000000000..96734b389947 --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityPolicyTest.java @@ -0,0 +1,295 @@ +/* + * 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.server.wm; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; + +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.graphics.Rect; +import android.platform.test.annotations.Presubmit; + +import androidx.annotation.NonNull; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * Test class for {@link AppCompatReachabilityPolicy}. + * <p/> + * Build/Install/Run: + * atest WmTests:AppCompatReachabilityPolicyTest + */ +@Presubmit +@RunWith(WindowTestRunner.class) +public class AppCompatReachabilityPolicyTest extends WindowTestsBase { + + @Test + public void handleHorizontalDoubleTap_reachabilityDisabled_nothingHappen() { + runTestScenario((robot) -> { + robot.activity().createActivityWithComponent(); + robot.enableHorizontalReachability(/* enabled */ false); + robot.activity().setTopActivityInTransition(/* inTransition */ true); + robot.doubleTapAt(100, 100); + + robot.checkLetterboxInnerFrameProvidedInvoked(/* invoked */ false); + }); + } + + @Test + public void handleHorizontalDoubleTap_reachabilityEnabledInTransition_nothingHappen() { + runTestScenario((robot) -> { + robot.activity().createActivityWithComponent(); + robot.enableHorizontalReachability(/* enabled */ true); + robot.activity().setTopActivityInTransition(/* inTransition */ true); + robot.doubleTapAt(100, 100); + + robot.checkLetterboxInnerFrameProvidedInvoked(/* invoked */ false); + }); + } + + @Test + public void handleHorizontalDoubleTap_reachabilityDisabledNotInTransition_nothingHappen() { + runTestScenario((robot) -> { + robot.activity().createActivityWithComponent(); + robot.enableHorizontalReachability(/* enabled */ false); + robot.activity().setTopActivityInTransition(/* inTransition */ false); + robot.doubleTapAt(100, 100); + + robot.checkLetterboxInnerFrameProvidedInvoked(/* invoked */ false); + }); + } + + @Test + public void handleHorizontalDoubleTap_leftInnerFrame_moveToLeft() { + runTestScenario((robot) -> { + robot.activity().createActivityWithComponent(); + robot.enableHorizontalReachability(/* enabled */ true); + robot.activity().setTopActivityInTransition(/* inTransition */ false); + + robot.configureLetterboxInnerFrameWidth(/* left */ 100, /* right */ 200); + robot.doubleTapAt(99, 100); + + robot.checkLetterboxInnerFrameProvidedInvoked(/* invoked */ true); + robot.applyOnConf((c) -> { + c.checkToNextLeftStop(/* invoked */ true); + c.checkToNextRightStop(/* invoked */ false); + }); + }); + } + + @Test + public void handleHorizontalDoubleTap_rightInnerFrame_moveToRight() { + runTestScenario((robot) -> { + robot.activity().createActivityWithComponent(); + robot.enableHorizontalReachability(/* enabled */ true); + robot.activity().setTopActivityInTransition(/* inTransition */ false); + + robot.configureLetterboxInnerFrameWidth(/* left */ 100, /* right */ 200); + robot.doubleTapAt(201, 100); + + robot.checkLetterboxInnerFrameProvidedInvoked(/* invoked */ true); + robot.applyOnConf((c) -> { + c.checkToNextLeftStop(/* invoked */ false); + c.checkToNextRightStop(/* invoked */ true); + }); + }); + } + + @Test + public void handleHorizontalDoubleTap_intoInnerFrame_noMove() { + runTestScenario((robot) -> { + robot.activity().createActivityWithComponent(); + robot.enableHorizontalReachability(/* enabled */ true); + robot.activity().setTopActivityInTransition(/* inTransition */ false); + + robot.configureLetterboxInnerFrameWidth(/* left */ 100, /* right */ 200); + robot.doubleTapAt(150, 100); + + robot.checkLetterboxInnerFrameProvidedInvoked(/* invoked */ true); + robot.applyOnConf((c) -> { + c.checkToNextLeftStop(/* invoked */ false); + c.checkToNextRightStop(/* invoked */ false); + }); + }); + } + + + @Test + public void handleVerticalDoubleTap_reachabilityDisabled_nothingHappen() { + runTestScenario((robot) -> { + robot.activity().createActivityWithComponent(); + robot.enableVerticalReachability(/* enabled */ false); + robot.activity().setTopActivityInTransition(/* inTransition */ true); + robot.doubleTapAt(100, 100); + + robot.checkLetterboxInnerFrameProvidedInvoked(/* invoked */ false); + }); + } + + @Test + public void handleVerticalDoubleTap_reachabilityEnabledInTransition_nothingHappen() { + runTestScenario((robot) -> { + robot.activity().createActivityWithComponent(); + robot.enableVerticalReachability(/* enabled */ true); + robot.activity().setTopActivityInTransition(/* inTransition */ true); + + robot.checkLetterboxInnerFrameProvidedInvoked(/* invoked */ false); + }); + } + + @Test + public void handleVerticalDoubleTap_reachabilityDisabledNotInTransition_nothingHappen() { + runTestScenario((robot) -> { + robot.activity().createActivityWithComponent(); + robot.enableVerticalReachability(/* enabled */ false); + robot.activity().setTopActivityInTransition(/* inTransition */ false); + + robot.checkLetterboxInnerFrameProvidedInvoked(/* invoked */ false); + }); + } + + @Test + public void handleVerticalDoubleTap_topInnerFrame_moveToTop() { + runTestScenario((robot) -> { + robot.activity().createActivityWithComponent(); + robot.enableVerticalReachability(/* enabled */ true); + robot.activity().setTopActivityInTransition(/* inTransition */ false); + + robot.configureLetterboxInnerFrameHeight(/* top */ 100, /* bottom */ 200); + robot.doubleTapAt(100, 99); + + robot.checkLetterboxInnerFrameProvidedInvoked(/* invoked */ true); + robot.applyOnConf((c) -> { + c.checkToNextTopStop(/* invoked */ true); + c.checkToNextBottomStop(/* invoked */ false); + }); + }); + } + + @Test + public void handleVerticalDoubleTap_bottomInnerFrame_moveToBottom() { + runTestScenario((robot) -> { + robot.activity().createActivityWithComponent(); + robot.enableVerticalReachability(/* enabled */ true); + robot.activity().setTopActivityInTransition(/* inTransition */ false); + + robot.configureLetterboxInnerFrameHeight(/* top */ 100, /* bottom */ 200); + robot.doubleTapAt(100, 201); + + robot.checkLetterboxInnerFrameProvidedInvoked(/* invoked */ true); + robot.applyOnConf((c) -> { + c.checkToNextTopStop(/* invoked */ false); + c.checkToNextBottomStop(/* invoked */ true); + }); + }); + } + + @Test + public void handleVerticalDoubleTap_intoInnerFrame_noMove() { + runTestScenario((robot) -> { + robot.activity().createActivityWithComponent(); + robot.enableVerticalReachability(/* enabled */ true); + robot.activity().setTopActivityInTransition(/* inTransition */ false); + + robot.configureLetterboxInnerFrameHeight(/* top */ 100, /* bottom */ 200); + robot.doubleTapAt(100, 150); + + robot.checkLetterboxInnerFrameProvidedInvoked(/* invoked */ true); + robot.applyOnConf((c) -> { + c.checkToNextTopStop(/* invoked */ false); + c.checkToNextBottomStop(/* invoked */ false); + }); + }); + } + + + /** + * Runs a test scenario providing a Robot. + */ + void runTestScenario(@NonNull Consumer<ReachabilityPolicyRobotTest> consumer) { + spyOn(mWm.mAppCompatConfiguration); + final ReachabilityPolicyRobotTest robot = + new ReachabilityPolicyRobotTest(mWm, mAtm, mSupervisor); + consumer.accept(robot); + } + + private static class ReachabilityPolicyRobotTest extends AppCompatRobotBase { + + private final Supplier<Rect> mLetterboxInnerBoundsSupplier = spy(Rect::new); + + ReachabilityPolicyRobotTest(@NonNull WindowManagerService wm, + @NonNull ActivityTaskManagerService atm, + @NonNull ActivityTaskSupervisor supervisor) { + super(wm, atm, supervisor); + } + + @Override + void onPostActivityCreation(@NonNull ActivityRecord activity) { + super.onPostActivityCreation(activity); + spyOn(activity.mAppCompatController.getAppCompatReachabilityOverrides()); + activity.mAppCompatController.getAppCompatReachabilityPolicy() + .setLetterboxInnerBoundsSupplier(mLetterboxInnerBoundsSupplier); + } + + void configureLetterboxInnerFrameWidth(int left, int right) { + doReturn(new Rect(left, /* top */ 0, right, /* bottom */ 100)) + .when(mLetterboxInnerBoundsSupplier).get(); + } + + void configureLetterboxInnerFrameHeight(int top, int bottom) { + doReturn(new Rect(/* left */ 0, top, /* right */ 100, bottom)) + .when(mLetterboxInnerBoundsSupplier).get(); + } + + void enableHorizontalReachability(boolean enabled) { + doReturn(enabled).when(getAppCompatReachabilityOverrides()) + .isHorizontalReachabilityEnabled(); + } + + void enableVerticalReachability(boolean enabled) { + doReturn(enabled).when(getAppCompatReachabilityOverrides()) + .isVerticalReachabilityEnabled(); + } + + void doubleTapAt(int x, int y) { + getAppCompatReachabilityPolicy().handleDoubleTap(x, y); + } + + void checkLetterboxInnerFrameProvidedInvoked(boolean invoked) { + verify(mLetterboxInnerBoundsSupplier, times(invoked ? 1 : 0)).get(); + } + + @NonNull + private AppCompatReachabilityOverrides getAppCompatReachabilityOverrides() { + return activity().top().mAppCompatController.getAppCompatReachabilityOverrides(); + } + + @NonNull + private AppCompatReachabilityPolicy getAppCompatReachabilityPolicy() { + return activity().top().mAppCompatController.getAppCompatReachabilityPolicy(); + } + + } + +} diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatResizeOverridesTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatResizeOverridesTest.java index 8fc1a77bd5e3..cade213ca3d7 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatResizeOverridesTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatResizeOverridesTest.java @@ -39,7 +39,7 @@ import java.util.function.Consumer; /** * Test class for {@link AppCompatResizeOverrides}. - * <p> + * <p/> * Build/Install/Run: * atest WmTests:AppCompatResizeOverridesTest */ diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatRobotBase.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatRobotBase.java index 6939f97e1799..4e58e1df59d4 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatRobotBase.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatRobotBase.java @@ -16,6 +16,7 @@ package com.android.server.wm; +import androidx.annotation.CallSuper; import androidx.annotation.NonNull; import java.util.function.Consumer; @@ -42,7 +43,8 @@ abstract class AppCompatRobotBase { @NonNull ActivityTaskSupervisor supervisor, int displayWidth, int displayHeight) { mActivityRobot = new AppCompatActivityRobot(wm, atm, supervisor, - displayWidth, displayHeight); + displayWidth, displayHeight, this::onPostActivityCreation, + this::onPostDisplayContentCreation); mConfigurationRobot = new AppCompatConfigurationRobot(wm.mAppCompatConfiguration); mOptPropRobot = new AppCompatComponentPropRobot(wm); @@ -54,6 +56,26 @@ abstract class AppCompatRobotBase { this(wm, atm, supervisor, DEFAULT_DISPLAY_WIDTH, DEFAULT_DISPLAY_HEIGHT); } + /** + * Specific Robots can override this method to add operation to run on a newly created + * {@link ActivityRecord}. Common case is to invoke spyOn(). + * + * @param activity THe newly created {@link ActivityRecord}. + */ + @CallSuper + void onPostActivityCreation(@NonNull ActivityRecord activity) { + } + + /** + * Specific Robots can override this method to add operation to run on a newly created + * {@link DisplayContent}. Common case is to invoke spyOn(). + * + * @param displayContent THe newly created {@link DisplayContent}. + */ + @CallSuper + void onPostDisplayContentCreation(@NonNull DisplayContent displayContent) { + } + @NonNull AppCompatConfigurationRobot conf() { return mConfigurationRobot; diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatUtilsTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatUtilsTest.java index 9e242eeeb58e..21fac9bcd1e4 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatUtilsTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatUtilsTest.java @@ -16,6 +16,8 @@ package com.android.server.wm; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; + import static org.mockito.Mockito.when; import android.platform.test.annotations.Presubmit; @@ -42,7 +44,10 @@ public class AppCompatUtilsTest extends WindowTestsBase { @Test public void getLetterboxReasonString_inSizeCompatMode() { runTestScenario((robot) -> { - robot.activity().setTopActivityInSizeCompatMode(/* inScm */ true); + robot.applyOnActivity((a) -> { + a.createActivityWithComponent(); + a.setTopActivityInSizeCompatMode(/* inScm */ true); + }); robot.checkTopActivityLetterboxReason(/* expected */ "SIZE_COMPAT_MODE"); }); @@ -51,7 +56,10 @@ public class AppCompatUtilsTest extends WindowTestsBase { @Test public void getLetterboxReasonString_fixedOrientation() { runTestScenario((robot) -> { - robot.activity().checkTopActivityInSizeCompatMode(/* inScm */ false); + robot.applyOnActivity((a) -> { + a.createActivityWithComponent(); + a.checkTopActivityInSizeCompatMode(/* inScm */ false); + }); robot.setIsLetterboxedForFixedOrientationAndAspectRatio( /* forFixedOrientationAndAspectRatio */ true); @@ -62,7 +70,10 @@ public class AppCompatUtilsTest extends WindowTestsBase { @Test public void getLetterboxReasonString_isLetterboxedForDisplayCutout() { runTestScenario((robot) -> { - robot.activity().checkTopActivityInSizeCompatMode(/* inScm */ false); + robot.applyOnActivity((a) -> { + a.createActivityWithComponent(); + a.checkTopActivityInSizeCompatMode(/* inScm */ false); + }); robot.setIsLetterboxedForFixedOrientationAndAspectRatio( /* forFixedOrientationAndAspectRatio */ false); robot.setIsLetterboxedForDisplayCutout(/* displayCutout */ true); @@ -74,7 +85,10 @@ public class AppCompatUtilsTest extends WindowTestsBase { @Test public void getLetterboxReasonString_aspectRatio() { runTestScenario((robot) -> { - robot.activity().checkTopActivityInSizeCompatMode(/* inScm */ false); + robot.applyOnActivity((a) -> { + a.createActivityWithComponent(); + a.checkTopActivityInSizeCompatMode(/* inScm */ false); + }); robot.setIsLetterboxedForFixedOrientationAndAspectRatio( /* forFixedOrientationAndAspectRatio */ false); robot.setIsLetterboxedForDisplayCutout(/* displayCutout */ false); @@ -87,7 +101,10 @@ public class AppCompatUtilsTest extends WindowTestsBase { @Test public void getLetterboxReasonString_unknownReason() { runTestScenario((robot) -> { - robot.activity().checkTopActivityInSizeCompatMode(/* inScm */ false); + robot.applyOnActivity((a) -> { + a.createActivityWithComponent(); + a.checkTopActivityInSizeCompatMode(/* inScm */ false); + }); robot.setIsLetterboxedForFixedOrientationAndAspectRatio( /* forFixedOrientationAndAspectRatio */ false); robot.setIsLetterboxedForDisplayCutout(/* displayCutout */ false); @@ -97,7 +114,6 @@ public class AppCompatUtilsTest extends WindowTestsBase { }); } - /** * Runs a test scenario providing a Robot. */ @@ -114,10 +130,15 @@ public class AppCompatUtilsTest extends WindowTestsBase { @NonNull ActivityTaskManagerService atm, @NonNull ActivityTaskSupervisor supervisor) { super(wm, atm, supervisor); - activity().createActivityWithComponent(); mWindowState = Mockito.mock(WindowState.class); } + @Override + void onPostActivityCreation(@NonNull ActivityRecord activity) { + super.onPostActivityCreation(activity); + spyOn(activity.mAppCompatController.getAppCompatAspectRatioPolicy()); + } + void setIsLetterboxedForFixedOrientationAndAspectRatio( boolean forFixedOrientationAndAspectRatio) { when(activity().top().mAppCompatController.getAppCompatAspectRatioPolicy() diff --git a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java index b687042edfc3..07e95d83d7bc 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java @@ -31,6 +31,7 @@ import static android.content.res.Configuration.ORIENTATION_PORTRAIT; import static android.util.DisplayMetrics.DENSITY_DEFAULT; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.server.wm.DesktopModeBoundsCalculator.DESKTOP_MODE_INITIAL_BOUNDS_SCALE; import static com.android.server.wm.DesktopModeBoundsCalculator.DESKTOP_MODE_LANDSCAPE_APP_PADDING; import static com.android.server.wm.DesktopModeBoundsCalculator.calculateAspectRatio; @@ -231,6 +232,56 @@ public class DesktopModeLaunchParamsModifierTests extends } @Test + @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS}) + public void testDefaultLandscapeBounds_landscapeDevice_userFullscreenOverride() { + setupDesktopModeLaunchParamsModifier(); + + final TestDisplayContent display = createDisplayContent(ORIENTATION_LANDSCAPE, + LANDSCAPE_DISPLAY_BOUNDS); + final Task task = createTask(display, SCREEN_ORIENTATION_LANDSCAPE, true); + + spyOn(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides()); + doReturn(true).when( + mActivity.mAppCompatController.getAppCompatAspectRatioOverrides()) + .isUserFullscreenOverrideEnabled(); + + final int desiredWidth = + (int) (LANDSCAPE_DISPLAY_BOUNDS.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE); + final int desiredHeight = + (int) (LANDSCAPE_DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE); + + assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate()); + assertEquals(desiredWidth, mResult.mBounds.width()); + assertEquals(desiredHeight, mResult.mBounds.height()); + } + + @Test + @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS}) + public void testDefaultLandscapeBounds_landscapeDevice_systemFullscreenOverride() { + setupDesktopModeLaunchParamsModifier(); + + final TestDisplayContent display = createDisplayContent(ORIENTATION_LANDSCAPE, + LANDSCAPE_DISPLAY_BOUNDS); + final Task task = createTask(display, SCREEN_ORIENTATION_LANDSCAPE, true); + + spyOn(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides()); + doReturn(true).when( + mActivity.mAppCompatController.getAppCompatAspectRatioOverrides()) + .isSystemOverrideToFullscreenEnabled(); + + final int desiredWidth = + (int) (LANDSCAPE_DISPLAY_BOUNDS.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE); + final int desiredHeight = + (int) (LANDSCAPE_DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE); + + assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate()); + assertEquals(desiredWidth, mResult.mBounds.width()); + assertEquals(desiredHeight, mResult.mBounds.height()); + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) public void testResizablePortraitBounds_landscapeDevice_resizable_portraitOrientation() { setupDesktopModeLaunchParamsModifier(); @@ -332,6 +383,56 @@ public class DesktopModeLaunchParamsModifierTests extends } @Test + @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS}) + public void testDefaultPortraitBounds_portraitDevice_userFullscreenOverride() { + setupDesktopModeLaunchParamsModifier(); + + final TestDisplayContent display = createDisplayContent(ORIENTATION_PORTRAIT, + PORTRAIT_DISPLAY_BOUNDS); + final Task task = createTask(display, SCREEN_ORIENTATION_PORTRAIT, true); + + spyOn(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides()); + doReturn(true).when( + mActivity.mAppCompatController.getAppCompatAspectRatioOverrides()) + .isUserFullscreenOverrideEnabled(); + + final int desiredWidth = + (int) (PORTRAIT_DISPLAY_BOUNDS.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE); + final int desiredHeight = + (int) (PORTRAIT_DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE); + + assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate()); + assertEquals(desiredWidth, mResult.mBounds.width()); + assertEquals(desiredHeight, mResult.mBounds.height()); + } + + @Test + @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS}) + public void testDefaultPortraitBounds_portraitDevice_systemFullscreenOverride() { + setupDesktopModeLaunchParamsModifier(); + + final TestDisplayContent display = createDisplayContent(ORIENTATION_PORTRAIT, + PORTRAIT_DISPLAY_BOUNDS); + final Task task = createTask(display, SCREEN_ORIENTATION_PORTRAIT, true); + + spyOn(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides()); + doReturn(true).when( + mActivity.mAppCompatController.getAppCompatAspectRatioOverrides()) + .isSystemOverrideToFullscreenEnabled(); + + final int desiredWidth = + (int) (PORTRAIT_DISPLAY_BOUNDS.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE); + final int desiredHeight = + (int) (PORTRAIT_DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE); + + assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate()); + assertEquals(desiredWidth, mResult.mBounds.width()); + assertEquals(desiredHeight, mResult.mBounds.height()); + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) public void testResizableLandscapeBounds_portraitDevice_resizable_landscapeOrientation() { setupDesktopModeLaunchParamsModifier(); diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java index 33df5d896f7f..695068a5842a 100644 --- a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java @@ -23,11 +23,9 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -36,8 +34,6 @@ import android.compat.testing.PlatformCompatChangeRule; import android.content.ComponentName; import android.content.res.Resources; import android.graphics.Rect; -import android.platform.test.annotations.DisableFlags; -import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import android.view.InsetsSource; import android.view.InsetsState; @@ -49,7 +45,6 @@ import android.view.WindowManager; import androidx.test.filters.SmallTest; import com.android.internal.R; -import com.android.window.flags.Flags; import org.junit.Before; import org.junit.Rule; @@ -296,106 +291,6 @@ public class LetterboxUiControllerTest extends WindowTestsBase { } @Test - public void testIsVerticalThinLetterboxed() { - // Vertical thin letterbox disabled - doReturn(-1).when(mActivity.mWmService.mAppCompatConfiguration) - .getThinLetterboxHeightPx(); - final AppCompatReachabilityOverrides reachabilityOverrides = mActivity.mAppCompatController - .getAppCompatReachabilityOverrides(); - assertFalse(reachabilityOverrides.isVerticalThinLetterboxed()); - // Define a Task 100x100 - final Task task = mock(Task.class); - doReturn(new Rect(0, 0, 100, 100)).when(task).getBounds(); - doReturn(10).when(mActivity.mWmService.mAppCompatConfiguration) - .getThinLetterboxHeightPx(); - - // Vertical thin letterbox disabled without Task - doReturn(null).when(mActivity).getTask(); - assertFalse(reachabilityOverrides.isVerticalThinLetterboxed()); - // Assign a Task for the Activity - doReturn(task).when(mActivity).getTask(); - - // (task.width() - act.width()) / 2 = 5 < 10 - doReturn(new Rect(5, 5, 95, 95)).when(mActivity).getBounds(); - assertTrue(reachabilityOverrides.isVerticalThinLetterboxed()); - - // (task.width() - act.width()) / 2 = 10 = 10 - doReturn(new Rect(10, 10, 90, 90)).when(mActivity).getBounds(); - assertTrue(reachabilityOverrides.isVerticalThinLetterboxed()); - - // (task.width() - act.width()) / 2 = 11 > 10 - doReturn(new Rect(11, 11, 89, 89)).when(mActivity).getBounds(); - assertFalse(reachabilityOverrides.isVerticalThinLetterboxed()); - } - - @Test - public void testIsHorizontalThinLetterboxed() { - // Horizontal thin letterbox disabled - doReturn(-1).when(mActivity.mWmService.mAppCompatConfiguration) - .getThinLetterboxWidthPx(); - final AppCompatReachabilityOverrides reachabilityOverrides = mActivity.mAppCompatController - .getAppCompatReachabilityOverrides(); - assertFalse(reachabilityOverrides.isHorizontalThinLetterboxed()); - // Define a Task 100x100 - final Task task = mock(Task.class); - doReturn(new Rect(0, 0, 100, 100)).when(task).getBounds(); - doReturn(10).when(mActivity.mWmService.mAppCompatConfiguration) - .getThinLetterboxWidthPx(); - - // Vertical thin letterbox disabled without Task - doReturn(null).when(mActivity).getTask(); - assertFalse(reachabilityOverrides.isHorizontalThinLetterboxed()); - // Assign a Task for the Activity - doReturn(task).when(mActivity).getTask(); - - // (task.height() - act.height()) / 2 = 5 < 10 - doReturn(new Rect(5, 5, 95, 95)).when(mActivity).getBounds(); - assertTrue(reachabilityOverrides.isHorizontalThinLetterboxed()); - - // (task.height() - act.height()) / 2 = 10 = 10 - doReturn(new Rect(10, 10, 90, 90)).when(mActivity).getBounds(); - assertTrue(reachabilityOverrides.isHorizontalThinLetterboxed()); - - // (task.height() - act.height()) / 2 = 11 > 10 - doReturn(new Rect(11, 11, 89, 89)).when(mActivity).getBounds(); - assertFalse(reachabilityOverrides.isHorizontalThinLetterboxed()); - } - - @Test - @EnableFlags(Flags.FLAG_DISABLE_THIN_LETTERBOXING_POLICY) - public void testAllowReachabilityForThinLetterboxWithFlagEnabled() { - final AppCompatReachabilityOverrides reachabilityOverrides = - mActivity.mAppCompatController.getAppCompatReachabilityOverrides(); - spyOn(reachabilityOverrides); - doReturn(true).when(reachabilityOverrides).isVerticalThinLetterboxed(); - assertFalse(reachabilityOverrides.allowVerticalReachabilityForThinLetterbox()); - doReturn(true).when(reachabilityOverrides).isHorizontalThinLetterboxed(); - assertFalse(reachabilityOverrides.allowHorizontalReachabilityForThinLetterbox()); - - doReturn(false).when(reachabilityOverrides).isVerticalThinLetterboxed(); - assertTrue(reachabilityOverrides.allowVerticalReachabilityForThinLetterbox()); - doReturn(false).when(reachabilityOverrides).isHorizontalThinLetterboxed(); - assertTrue(reachabilityOverrides.allowHorizontalReachabilityForThinLetterbox()); - } - - @Test - @DisableFlags(Flags.FLAG_DISABLE_THIN_LETTERBOXING_POLICY) - public void testAllowReachabilityForThinLetterboxWithFlagDisabled() { - final AppCompatReachabilityOverrides reachabilityOverrides = - mActivity.mAppCompatController.getAppCompatReachabilityOverrides(); - spyOn(reachabilityOverrides); - doReturn(true).when(reachabilityOverrides).isVerticalThinLetterboxed(); - assertTrue(reachabilityOverrides.allowVerticalReachabilityForThinLetterbox()); - doReturn(true).when(reachabilityOverrides).isHorizontalThinLetterboxed(); - assertTrue(reachabilityOverrides.allowHorizontalReachabilityForThinLetterbox()); - - doReturn(false).when(reachabilityOverrides).isVerticalThinLetterboxed(); - assertTrue(reachabilityOverrides.allowVerticalReachabilityForThinLetterbox()); - doReturn(false).when(reachabilityOverrides).isHorizontalThinLetterboxed(); - assertTrue(reachabilityOverrides.allowHorizontalReachabilityForThinLetterbox()); - } - - @Test public void testIsLetterboxEducationEnabled() { mController.isLetterboxEducationEnabled(); verify(mAppCompatConfiguration).getIsEducationEnabled(); diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java index c53addcf220c..6fd5faf8e27d 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java @@ -131,6 +131,7 @@ public class TaskDisplayAreaTests extends WindowTestsBase { final Task adjacentRootTask = createTask( mDisplayContent, WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD); adjacentRootTask.mCreatedByOrganizer = true; + createActivityRecord(adjacentRootTask); final TaskDisplayArea taskDisplayArea = rootTask.getDisplayArea(); adjacentRootTask.setAdjacentTaskFragment(rootTask); diff --git a/services/tests/wmtests/src/com/android/server/wm/TransparentPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/TransparentPolicyTest.java index 407218d310c7..a0641cd49018 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TransparentPolicyTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TransparentPolicyTest.java @@ -22,6 +22,8 @@ import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; import static android.view.Surface.ROTATION_0; import static android.view.Surface.ROTATION_90; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; + import static org.mockito.Mockito.clearInvocations; import android.platform.test.annotations.EnableFlags; @@ -363,6 +365,12 @@ public class TransparentPolicyTest extends WindowTestsBase { activity().createNewTaskWithBaseActivity(); } + @Override + void onPostActivityCreation(@NonNull ActivityRecord activity) { + super.onPostActivityCreation(activity); + spyOn(activity.mAppCompatController.getTransparentPolicy()); + } + void transparentActivity(@NonNull Consumer<AppCompatTransparentActivityRobot> consumer) { consumer.accept(mTransparentActivityRobot); } diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java index dea10b70b7b9..f0850af5fc2e 100644 --- a/telephony/java/android/telephony/SubscriptionManager.java +++ b/telephony/java/android/telephony/SubscriptionManager.java @@ -1177,6 +1177,16 @@ public class SubscriptionManager { */ public static final String SATELLITE_ESOS_SUPPORTED = SimInfo.COLUMN_SATELLITE_ESOS_SUPPORTED; + /** + * TelephonyProvider column name for satellite provisioned status. The value of this + * column is set based on whether carrier roaming NB-IOT satellite service is provisioned or + * not. By default, it's disabled. + * + * @hide + */ + public static final String IS_SATELLITE_PROVISIONED_FOR_NON_IP_DATAGRAM = + SimInfo.COLUMN_IS_SATELLITE_PROVISIONED_FOR_NON_IP_DATAGRAM; + /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef(prefix = {"USAGE_SETTING_"}, diff --git a/telephony/java/android/telephony/satellite/stub/ISatellite.aidl b/telephony/java/android/telephony/satellite/stub/ISatellite.aidl index b82396e710fd..e66a0824f545 100644 --- a/telephony/java/android/telephony/satellite/stub/ISatellite.aidl +++ b/telephony/java/android/telephony/satellite/stub/ISatellite.aidl @@ -206,77 +206,6 @@ oneway interface ISatellite { void stopSendingSatellitePointingInfo(in IIntegerConsumer resultCallback); /** - * Provision the device with a satellite provider. - * This is needed if the provider allows dynamic registration. - * Once provisioned, ISatelliteListener#onSatelliteProvisionStateChanged should report true. - * - * @param token The token to be used as a unique identifier for provisioning with satellite - * gateway. - * @param provisionData Data from the provisioning app that can be used by provisioning server - * @param resultCallback The callback to receive the error code result of the operation. - * - * Valid result codes returned: - * SatelliteResult:SATELLITE_RESULT_SUCCESS - * SatelliteResult:SATELLITE_RESULT_SERVICE_ERROR - * SatelliteResult:SATELLITE_RESULT_MODEM_ERROR - * SatelliteResult:SATELLITE_RESULT_NETWORK_ERROR - * SatelliteResult:SATELLITE_RESULT_INVALID_MODEM_STATE - * SatelliteResult:SATELLITE_RESULT_INVALID_ARGUMENTS - * SatelliteResult:SATELLITE_RESULT_RADIO_NOT_AVAILABLE - * SatelliteResult:SATELLITE_RESULT_REQUEST_NOT_SUPPORTED - * SatelliteResult:SATELLITE_RESULT_NO_RESOURCES - * SatelliteResult:SATELLITE_RESULT_REQUEST_ABORTED - * SatelliteResult:SATELLITE_RESULT_NETWORK_TIMEOUT - */ - void provisionSatelliteService(in String token, in byte[] provisionData, - in IIntegerConsumer resultCallback); - - /** - * Deprovision the device with the satellite provider. - * This is needed if the provider allows dynamic registration. - * Once deprovisioned, ISatelliteListener#onSatelliteProvisionStateChanged should report false. - * - * @param token The token of the device/subscription to be deprovisioned. - * @param resultCallback The callback to receive the error code result of the operation. - * - * Valid result codes returned: - * SatelliteResult:SATELLITE_RESULT_SUCCESS - * SatelliteResult:SATELLITE_RESULT_SERVICE_ERROR - * SatelliteResult:SATELLITE_RESULT_MODEM_ERROR - * SatelliteResult:SATELLITE_RESULT_NETWORK_ERROR - * SatelliteResult:SATELLITE_RESULT_INVALID_MODEM_STATE - * SatelliteResult:SATELLITE_RESULT_INVALID_ARGUMENTS - * SatelliteResult:SATELLITE_RESULT_RADIO_NOT_AVAILABLE - * SatelliteResult:SATELLITE_RESULT_REQUEST_NOT_SUPPORTED - * SatelliteResult:SATELLITE_RESULT_NO_RESOURCES - * SatelliteResult:SATELLITE_RESULT_REQUEST_ABORTED - * SatelliteResult:SATELLITE_RESULT_NETWORK_TIMEOUT - */ - void deprovisionSatelliteService(in String token, in IIntegerConsumer resultCallback); - - /** - * Request to get whether this device is provisioned with a satellite provider. - * - * @param resultCallback The callback to receive the error code result of the operation. - * This must only be sent when the result is not - * SatelliteResult#SATELLITE_RESULT_SUCCESS. - * @param callback If the result is SatelliteResult#SATELLITE_RESULT_SUCCESS, the callback to - * receive whether this device is provisioned with a satellite provider. - * - * Valid result codes returned: - * SatelliteResult:SATELLITE_RESULT_SUCCESS - * SatelliteResult:SATELLITE_RESULT_SERVICE_ERROR - * SatelliteResult:SATELLITE_RESULT_MODEM_ERROR - * SatelliteResult:SATELLITE_RESULT_INVALID_MODEM_STATE - * SatelliteResult:SATELLITE_RESULT_INVALID_ARGUMENTS - * SatelliteResult:SATELLITE_RESULT_RADIO_NOT_AVAILABLE - * SatelliteResult:SATELLITE_RESULT_REQUEST_NOT_SUPPORTED - * SatelliteResult:SATELLITE_RESULT_NO_RESOURCES - */ - void requestIsSatelliteProvisioned(in IIntegerConsumer resultCallback, - in IBooleanConsumer callback); - - /** * Poll the pending datagrams to be received over satellite. * The satellite service should check if there are any pending datagrams to be received over * satellite and report them via ISatelliteListener#onSatelliteDatagramsReceived. diff --git a/telephony/java/android/telephony/satellite/stub/ISatelliteListener.aidl b/telephony/java/android/telephony/satellite/stub/ISatelliteListener.aidl index b4eb15fde632..3f2fce2b9f1d 100644 --- a/telephony/java/android/telephony/satellite/stub/ISatelliteListener.aidl +++ b/telephony/java/android/telephony/satellite/stub/ISatelliteListener.aidl @@ -28,13 +28,6 @@ import android.telephony.satellite.stub.SatelliteModemState; */ oneway interface ISatelliteListener { /** - * Indicates that the satellite provision state has changed. - * - * @param provisioned True means the service is provisioned and false means it is not. - */ - void onSatelliteProvisionStateChanged(in boolean provisioned); - - /** * Indicates that new datagrams have been received on the device. * * @param datagram New datagram that was received. diff --git a/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java b/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java index d8b4974f23b9..c50e469e83cb 100644 --- a/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java +++ b/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java @@ -142,32 +142,6 @@ public class SatelliteImplBase extends SatelliteService { } @Override - public void provisionSatelliteService(String token, byte[] provisionData, - IIntegerConsumer resultCallback) throws RemoteException { - executeMethodAsync( - () -> SatelliteImplBase.this - .provisionSatelliteService(token, provisionData, resultCallback), - "provisionSatelliteService"); - } - - @Override - public void deprovisionSatelliteService(String token, IIntegerConsumer resultCallback) - throws RemoteException { - executeMethodAsync( - () -> SatelliteImplBase.this.deprovisionSatelliteService(token, resultCallback), - "deprovisionSatelliteService"); - } - - @Override - public void requestIsSatelliteProvisioned(IIntegerConsumer resultCallback, - IBooleanConsumer callback) throws RemoteException { - executeMethodAsync( - () -> SatelliteImplBase.this - .requestIsSatelliteProvisioned(resultCallback, callback), - "requestIsSatelliteProvisioned"); - } - - @Override public void pollPendingSatelliteDatagrams(IIntegerConsumer resultCallback) throws RemoteException { executeMethodAsync( @@ -487,85 +461,6 @@ public class SatelliteImplBase extends SatelliteService { } /** - * Provision the device with a satellite provider. - * This is needed if the provider allows dynamic registration. - * Once provisioned, ISatelliteListener#onSatelliteProvisionStateChanged should report true. - * - * @param token The token to be used as a unique identifier for provisioning with satellite - * gateway. - * @param provisionData Data from the provisioning app that can be used by provisioning - * server - * @param resultCallback The callback to receive the error code result of the operation. - * - * Valid result codes returned: - * SatelliteResult:SATELLITE_RESULT_SUCCESS - * SatelliteResult:SATELLITE_RESULT_SERVICE_ERROR - * SatelliteResult:SATELLITE_RESULT_MODEM_ERROR - * SatelliteResult:SATELLITE_RESULT_NETWORK_ERROR - * SatelliteResult:SATELLITE_RESULT_INVALID_MODEM_STATE - * SatelliteResult:SATELLITE_RESULT_INVALID_ARGUMENTS - * SatelliteResult:SATELLITE_RESULT_RADIO_NOT_AVAILABLE - * SatelliteResult:SATELLITE_RESULT_REQUEST_NOT_SUPPORTED - * SatelliteResult:SATELLITE_RESULT_NO_RESOURCES - * SatelliteResult:SATELLITE_RESULT_REQUEST_ABORTED - * SatelliteResult:SATELLITE_RESULT_NETWORK_TIMEOUT - */ - public void provisionSatelliteService(@NonNull String token, @NonNull byte[] provisionData, - @NonNull IIntegerConsumer resultCallback) { - // stub implementation - } - - /** - * Deprovision the device with the satellite provider. - * This is needed if the provider allows dynamic registration. - * Once deprovisioned, ISatelliteListener#onSatelliteProvisionStateChanged should report false. - * - * @param token The token of the device/subscription to be deprovisioned. - * @param resultCallback The callback to receive the error code result of the operation. - * - * Valid result codes returned: - * SatelliteResult:SATELLITE_RESULT_SUCCESS - * SatelliteResult:SATELLITE_RESULT_SERVICE_ERROR - * SatelliteResult:SATELLITE_RESULT_MODEM_ERROR - * SatelliteResult:SATELLITE_RESULT_NETWORK_ERROR - * SatelliteResult:SATELLITE_RESULT_INVALID_MODEM_STATE - * SatelliteResult:SATELLITE_RESULT_INVALID_ARGUMENTS - * SatelliteResult:SATELLITE_RESULT_RADIO_NOT_AVAILABLE - * SatelliteResult:SATELLITE_RESULT_REQUEST_NOT_SUPPORTED - * SatelliteResult:SATELLITE_RESULT_NO_RESOURCES - * SatelliteResult:SATELLITE_RESULT_REQUEST_ABORTED - * SatelliteResult:SATELLITE_RESULT_NETWORK_TIMEOUT - */ - public void deprovisionSatelliteService(@NonNull String token, - @NonNull IIntegerConsumer resultCallback) { - // stub implementation - } - - /** - * Request to get whether this device is provisioned with a satellite provider. - * - * @param resultCallback The callback to receive the error code result of the operation. - * This must only be sent when the result is not - * SatelliteResult#SATELLITE_RESULT_SUCCESS. - * @param callback If the result is SatelliteResult#SATELLITE_RESULT_SUCCESS, the callback to - * receive whether this device is provisioned with a satellite provider. - * - * Valid result codes returned: - * SatelliteResult:SATELLITE_RESULT_SUCCESS - * SatelliteResult:SATELLITE_RESULT_SERVICE_ERROR - * SatelliteResult:SATELLITE_RESULT_MODEM_ERROR - * SatelliteResult:SATELLITE_RESULT_INVALID_MODEM_STATE - * SatelliteResult:SATELLITE_RESULT_INVALID_ARGUMENTS - * SatelliteResult:SATELLITE_RESULT_RADIO_NOT_AVAILABLE - * SatelliteResult:SATELLITE_RESULT_REQUEST_NOT_SUPPORTED - * SatelliteResult:SATELLITE_RESULT_NO_RESOURCES - */ - public void requestIsSatelliteProvisioned(@NonNull IIntegerConsumer resultCallback, - @NonNull IBooleanConsumer callback) { - // stub implementation - } - - /** * Poll the pending datagrams to be received over satellite. * The satellite service should check if there are any pending datagrams to be received over * satellite and report them via ISatelliteListener#onSatelliteDatagramsReceived. diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl index 3dbda7ae10a3..2f8e95713eba 100644 --- a/telephony/java/com/android/internal/telephony/ITelephony.aidl +++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl @@ -3007,16 +3007,18 @@ interface ITelephony { */ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(" + "android.Manifest.permission.SATELLITE_COMMUNICATION)") - void setDeviceAlignedWithSatellite(int subId, in boolean isAligned); + void setDeviceAlignedWithSatellite(int subId, boolean isAligned); /** * This API can be used by only CTS to update satellite vendor service package name. * * @param servicePackageName The package name of the satellite vendor service. + * @param provisioned Whether satellite should be provisioned or not. + * * @return {@code true} if the satellite vendor service is set successfully, * {@code false} otherwise. */ - boolean setSatelliteServicePackageName(in String servicePackageName); + boolean setSatelliteServicePackageName(in String servicePackageName, in String provisioned); /** * This API can be used by only CTS to update satellite gateway service package name. diff --git a/tests/Input/src/android/hardware/input/KeyboardSystemShortcutListenerTest.kt b/tests/Input/src/android/hardware/input/KeyboardSystemShortcutListenerTest.kt new file mode 100644 index 000000000000..24d7291bec87 --- /dev/null +++ b/tests/Input/src/android/hardware/input/KeyboardSystemShortcutListenerTest.kt @@ -0,0 +1,202 @@ +/* + * Copyright 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 android.hardware.input + +import android.content.Context +import android.content.ContextWrapper +import android.os.Handler +import android.os.HandlerExecutor +import android.os.test.TestLooper +import android.platform.test.annotations.Presubmit +import android.platform.test.flag.junit.SetFlagsRule +import android.view.KeyEvent +import androidx.test.core.app.ApplicationProvider +import com.android.server.testutils.any +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.Mockito.doAnswer +import org.mockito.Mockito.`when` +import org.mockito.junit.MockitoJUnitRunner +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.fail + +/** + * Tests for [InputManager.KeyboardSystemShortcutListener]. + * + * Build/Install/Run: + * atest InputTests:KeyboardSystemShortcutListenerTest + */ +@Presubmit +@RunWith(MockitoJUnitRunner::class) +class KeyboardSystemShortcutListenerTest { + + companion object { + const val DEVICE_ID = 1 + val HOME_SHORTCUT = KeyboardSystemShortcut( + intArrayOf(KeyEvent.KEYCODE_H), + KeyEvent.META_META_ON or KeyEvent.META_META_LEFT_ON, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_HOME + ) + } + + @get:Rule + val rule = SetFlagsRule() + + private val testLooper = TestLooper() + private val executor = HandlerExecutor(Handler(testLooper.looper)) + private var registeredListener: IKeyboardSystemShortcutListener? = null + private lateinit var context: Context + private lateinit var inputManager: InputManager + private lateinit var inputManagerGlobalSession: InputManagerGlobal.TestSession + + @Mock + private lateinit var iInputManagerMock: IInputManager + + @Before + fun setUp() { + context = Mockito.spy(ContextWrapper(ApplicationProvider.getApplicationContext())) + inputManagerGlobalSession = InputManagerGlobal.createTestSession(iInputManagerMock) + inputManager = InputManager(context) + `when`(context.getSystemService(Mockito.eq(Context.INPUT_SERVICE))) + .thenReturn(inputManager) + + // Handle keyboard system shortcut listener registration. + doAnswer { + val listener = it.getArgument(0) as IKeyboardSystemShortcutListener + if (registeredListener != null && + registeredListener!!.asBinder() != listener.asBinder()) { + // There can only be one registered keyboard system shortcut listener per process. + fail("Trying to register a new listener when one already exists") + } + registeredListener = listener + null + }.`when`(iInputManagerMock).registerKeyboardSystemShortcutListener(any()) + + // Handle keyboard system shortcut listener being unregistered. + doAnswer { + val listener = it.getArgument(0) as IKeyboardSystemShortcutListener + if (registeredListener == null || + registeredListener!!.asBinder() != listener.asBinder()) { + fail("Trying to unregister a listener that is not registered") + } + registeredListener = null + null + }.`when`(iInputManagerMock).unregisterKeyboardSystemShortcutListener(any()) + } + + @After + fun tearDown() { + if (this::inputManagerGlobalSession.isInitialized) { + inputManagerGlobalSession.close() + } + } + + private fun notifyKeyboardSystemShortcutTriggered(id: Int, shortcut: KeyboardSystemShortcut) { + registeredListener!!.onKeyboardSystemShortcutTriggered( + id, + shortcut.keycodes, + shortcut.modifierState, + shortcut.systemShortcut + ) + } + + @Test + fun testListenerHasCorrectSystemShortcutNotified() { + var callbackCount = 0 + + // Add a keyboard system shortcut listener + inputManager.registerKeyboardSystemShortcutListener(executor) { + deviceId: Int, systemShortcut: KeyboardSystemShortcut -> + assertEquals(DEVICE_ID, deviceId) + assertEquals(HOME_SHORTCUT, systemShortcut) + callbackCount++ + } + + // Notifying keyboard system shortcut triggered will notify the listener. + notifyKeyboardSystemShortcutTriggered(DEVICE_ID, HOME_SHORTCUT) + testLooper.dispatchNext() + assertEquals(1, callbackCount) + } + + @Test + fun testAddingListenersRegistersInternalCallbackListener() { + // Set up two callbacks. + val callback1 = InputManager.KeyboardSystemShortcutListener {_, _ -> } + val callback2 = InputManager.KeyboardSystemShortcutListener {_, _ -> } + + assertNull(registeredListener) + + // Adding the listener should register the callback with InputManagerService. + inputManager.registerKeyboardSystemShortcutListener(executor, callback1) + assertNotNull(registeredListener) + + // Adding another listener should not register new internal listener. + val currListener = registeredListener + inputManager.registerKeyboardSystemShortcutListener(executor, callback2) + assertEquals(currListener, registeredListener) + } + + @Test + fun testRemovingListenersUnregistersInternalCallbackListener() { + // Set up two callbacks. + val callback1 = InputManager.KeyboardSystemShortcutListener {_, _ -> } + val callback2 = InputManager.KeyboardSystemShortcutListener {_, _ -> } + + inputManager.registerKeyboardSystemShortcutListener(executor, callback1) + inputManager.registerKeyboardSystemShortcutListener(executor, callback2) + + // Only removing all listeners should remove the internal callback + inputManager.unregisterKeyboardSystemShortcutListener(callback1) + assertNotNull(registeredListener) + inputManager.unregisterKeyboardSystemShortcutListener(callback2) + assertNull(registeredListener) + } + + @Test + fun testMultipleListeners() { + // Set up two callbacks. + var callbackCount1 = 0 + var callbackCount2 = 0 + val callback1 = InputManager.KeyboardSystemShortcutListener { _, _ -> callbackCount1++ } + val callback2 = InputManager.KeyboardSystemShortcutListener { _, _ -> callbackCount2++ } + + // Add both keyboard system shortcut listeners + inputManager.registerKeyboardSystemShortcutListener(executor, callback1) + inputManager.registerKeyboardSystemShortcutListener(executor, callback2) + + // Notifying keyboard system shortcut triggered, should notify both the callbacks. + notifyKeyboardSystemShortcutTriggered(DEVICE_ID, HOME_SHORTCUT) + testLooper.dispatchAll() + assertEquals(1, callbackCount1) + assertEquals(1, callbackCount2) + + inputManager.unregisterKeyboardSystemShortcutListener(callback2) + // Notifying keyboard system shortcut triggered, should still trigger callback1 but not + // callback2. + notifyKeyboardSystemShortcutTriggered(DEVICE_ID, HOME_SHORTCUT) + testLooper.dispatchAll() + assertEquals(2, callbackCount1) + assertEquals(1, callbackCount2) + } +} diff --git a/tests/Input/src/com/android/server/input/KeyboardShortcutCallbackHandlerTests.kt b/tests/Input/src/com/android/server/input/KeyboardShortcutCallbackHandlerTests.kt new file mode 100644 index 000000000000..5a40a1c8201e --- /dev/null +++ b/tests/Input/src/com/android/server/input/KeyboardShortcutCallbackHandlerTests.kt @@ -0,0 +1,96 @@ +/* + * Copyright 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.server.input + +import android.content.Context +import android.content.ContextWrapper +import android.hardware.input.IKeyboardSystemShortcutListener +import android.hardware.input.KeyboardSystemShortcut +import android.platform.test.annotations.Presubmit +import android.view.KeyEvent +import androidx.test.core.app.ApplicationProvider +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.Mockito +import org.mockito.junit.MockitoJUnit + +/** + * Tests for {@link KeyboardShortcutCallbackHandler}. + * + * Build/Install/Run: + * atest InputTests:KeyboardShortcutCallbackHandlerTests + */ +@Presubmit +class KeyboardShortcutCallbackHandlerTests { + + companion object { + val DEVICE_ID = 1 + val HOME_SHORTCUT = KeyboardSystemShortcut( + intArrayOf(KeyEvent.KEYCODE_H), + KeyEvent.META_META_ON or KeyEvent.META_META_LEFT_ON, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_HOME + ) + } + + @get:Rule + val rule = MockitoJUnit.rule()!! + + private lateinit var keyboardShortcutCallbackHandler: KeyboardShortcutCallbackHandler + private lateinit var context: Context + private var lastShortcut: KeyboardSystemShortcut? = null + + @Before + fun setup() { + context = Mockito.spy(ContextWrapper(ApplicationProvider.getApplicationContext())) + keyboardShortcutCallbackHandler = KeyboardShortcutCallbackHandler() + } + + @Test + fun testKeyboardSystemShortcutTriggered_registerUnregisterListener() { + val listener = KeyboardSystemShortcutListener() + + // Register keyboard system shortcut listener + keyboardShortcutCallbackHandler.registerKeyboardSystemShortcutListener(listener, 0) + keyboardShortcutCallbackHandler.onKeyboardSystemShortcutTriggered(DEVICE_ID, HOME_SHORTCUT) + assertEquals( + "Listener should get callback on keyboard system shortcut triggered", + HOME_SHORTCUT, + lastShortcut!! + ) + + // Unregister listener + lastShortcut = null + keyboardShortcutCallbackHandler.unregisterKeyboardSystemShortcutListener(listener, 0) + keyboardShortcutCallbackHandler.onKeyboardSystemShortcutTriggered(DEVICE_ID, HOME_SHORTCUT) + assertNull("Listener should not get callback after being unregistered", lastShortcut) + } + + inner class KeyboardSystemShortcutListener : IKeyboardSystemShortcutListener.Stub() { + override fun onKeyboardSystemShortcutTriggered( + deviceId: Int, + keycodes: IntArray, + modifierState: Int, + shortcut: Int + ) { + assertEquals(DEVICE_ID, deviceId) + lastShortcut = KeyboardSystemShortcut(keycodes, modifierState, shortcut) + } + } +}
\ No newline at end of file diff --git a/tests/Internal/src/com/android/internal/protolog/ProtoLogCommandHandlerTest.java b/tests/Internal/src/com/android/internal/protolog/ProtoLogCommandHandlerTest.java new file mode 100644 index 000000000000..e3ec62d5b5a6 --- /dev/null +++ b/tests/Internal/src/com/android/internal/protolog/ProtoLogCommandHandlerTest.java @@ -0,0 +1,207 @@ +/* + * 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.internal.protolog; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.contains; +import static org.mockito.ArgumentMatchers.endsWith; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.times; + +import android.platform.test.annotations.Presubmit; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +import java.io.FileDescriptor; +import java.io.PrintWriter; + +/** + * Test class for {@link ProtoLogImpl}. + */ +@Presubmit +@RunWith(MockitoJUnitRunner.class) +public class ProtoLogCommandHandlerTest { + + @Mock + ProtoLogService mProtoLogService; + @Mock + PrintWriter mPrintWriter; + + @Test + public void printsHelpForAllAvailableCommands() { + final ProtoLogCommandHandler cmdHandler = + new ProtoLogCommandHandler(mProtoLogService, mPrintWriter); + + cmdHandler.onHelp(); + validateOnHelpPrinted(); + } + + @Test + public void printsHelpIfCommandIsNull() { + final ProtoLogCommandHandler cmdHandler = + new ProtoLogCommandHandler(mProtoLogService, mPrintWriter); + + cmdHandler.onCommand(null); + validateOnHelpPrinted(); + } + + @Test + public void handlesGroupListCommand() { + Mockito.when(mProtoLogService.getGroups()) + .thenReturn(new String[] {"MY_TEST_GROUP", "MY_OTHER_GROUP"}); + final ProtoLogCommandHandler cmdHandler = + new ProtoLogCommandHandler(mProtoLogService, mPrintWriter); + + cmdHandler.exec(mProtoLogService, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err, + new String[] { "groups", "list" }); + + Mockito.verify(mPrintWriter, times(1)) + .println(contains("MY_TEST_GROUP")); + Mockito.verify(mPrintWriter, times(1)) + .println(contains("MY_OTHER_GROUP")); + } + + @Test + public void handlesIncompleteGroupsCommand() { + final ProtoLogCommandHandler cmdHandler = + new ProtoLogCommandHandler(mProtoLogService, mPrintWriter); + + cmdHandler.exec(mProtoLogService, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err, + new String[] { "groups" }); + + Mockito.verify(mPrintWriter, times(1)) + .println(contains("Incomplete command")); + } + + @Test + public void handlesGroupStatusCommand() { + Mockito.when(mProtoLogService.getGroups()).thenReturn(new String[] {"MY_GROUP"}); + Mockito.when(mProtoLogService.isLoggingToLogcat("MY_GROUP")).thenReturn(true); + final ProtoLogCommandHandler cmdHandler = + new ProtoLogCommandHandler(mProtoLogService, mPrintWriter); + + cmdHandler.exec(mProtoLogService, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err, + new String[] { "groups", "status", "MY_GROUP" }); + + Mockito.verify(mPrintWriter, times(1)) + .println(contains("MY_GROUP")); + Mockito.verify(mPrintWriter, times(1)) + .println(contains("LOG_TO_LOGCAT = true")); + } + + @Test + public void handlesGroupStatusCommandOfUnregisteredGroups() { + Mockito.when(mProtoLogService.getGroups()).thenReturn(new String[] {}); + final ProtoLogCommandHandler cmdHandler = + new ProtoLogCommandHandler(mProtoLogService, mPrintWriter); + + cmdHandler.exec(mProtoLogService, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err, + new String[] { "groups", "status", "MY_GROUP" }); + + Mockito.verify(mPrintWriter, times(1)) + .println(contains("MY_GROUP")); + Mockito.verify(mPrintWriter, times(1)) + .println(contains("UNREGISTERED")); + } + + @Test + public void handlesGroupStatusCommandWithNoGroups() { + final ProtoLogCommandHandler cmdHandler = + new ProtoLogCommandHandler(mProtoLogService, mPrintWriter); + + cmdHandler.exec(mProtoLogService, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err, + new String[] { "groups", "status" }); + + Mockito.verify(mPrintWriter, times(1)) + .println(contains("Incomplete command")); + } + + @Test + public void handlesIncompleteLogcatCommand() { + final ProtoLogCommandHandler cmdHandler = + new ProtoLogCommandHandler(mProtoLogService, mPrintWriter); + + cmdHandler.exec(mProtoLogService, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err, + new String[] { "logcat" }); + + Mockito.verify(mPrintWriter, times(1)) + .println(contains("Incomplete command")); + } + + @Test + public void handlesLogcatEnableCommand() { + final ProtoLogCommandHandler cmdHandler = + new ProtoLogCommandHandler(mProtoLogService, mPrintWriter); + + cmdHandler.exec(mProtoLogService, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err, + new String[] { "logcat", "enable", "MY_GROUP" }); + Mockito.verify(mProtoLogService).enableProtoLogToLogcat("MY_GROUP"); + + cmdHandler.exec(mProtoLogService, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err, + new String[] { "logcat", "enable", "MY_GROUP", "MY_OTHER_GROUP" }); + Mockito.verify(mProtoLogService) + .enableProtoLogToLogcat("MY_GROUP", "MY_OTHER_GROUP"); + } + + @Test + public void handlesLogcatDisableCommand() { + final ProtoLogCommandHandler cmdHandler = + new ProtoLogCommandHandler(mProtoLogService, mPrintWriter); + + cmdHandler.exec(mProtoLogService, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err, + new String[] { "logcat", "disable", "MY_GROUP" }); + Mockito.verify(mProtoLogService).disableProtoLogToLogcat("MY_GROUP"); + + cmdHandler.exec(mProtoLogService, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err, + new String[] { "logcat", "disable", "MY_GROUP", "MY_OTHER_GROUP" }); + Mockito.verify(mProtoLogService) + .disableProtoLogToLogcat("MY_GROUP", "MY_OTHER_GROUP"); + } + + @Test + public void handlesLogcatEnableCommandWithNoGroups() { + final ProtoLogCommandHandler cmdHandler = + new ProtoLogCommandHandler(mProtoLogService, mPrintWriter); + + cmdHandler.exec(mProtoLogService, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err, + new String[] { "logcat", "enable" }); + Mockito.verify(mPrintWriter).println(contains("Incomplete command")); + } + + @Test + public void handlesLogcatDisableCommandWithNoGroups() { + final ProtoLogCommandHandler cmdHandler = + new ProtoLogCommandHandler(mProtoLogService, mPrintWriter); + + cmdHandler.exec(mProtoLogService, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err, + new String[] { "logcat", "disable" }); + Mockito.verify(mPrintWriter).println(contains("Incomplete command")); + } + + private void validateOnHelpPrinted() { + Mockito.verify(mPrintWriter, times(1)).println(endsWith("help")); + Mockito.verify(mPrintWriter, times(1)) + .println(endsWith("groups (list | status)")); + Mockito.verify(mPrintWriter, times(1)) + .println(endsWith("logcat (enable | disable) <group>")); + Mockito.verify(mPrintWriter, atLeast(0)).println(anyString()); + } +} diff --git a/tests/Internal/src/com/android/internal/protolog/ProtoLogServiceTest.java b/tests/Internal/src/com/android/internal/protolog/ProtoLogServiceTest.java new file mode 100644 index 000000000000..feac59c702ea --- /dev/null +++ b/tests/Internal/src/com/android/internal/protolog/ProtoLogServiceTest.java @@ -0,0 +1,283 @@ +/* + * 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.internal.protolog; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; + +import static java.io.File.createTempFile; +import static java.nio.file.Files.createTempDirectory; + +import android.os.IBinder; +import android.os.RemoteException; +import android.platform.test.annotations.Presubmit; +import android.tools.ScenarioBuilder; +import android.tools.Tag; +import android.tools.io.ResultArtifactDescriptor; +import android.tools.io.TraceType; +import android.tools.traces.TraceConfig; +import android.tools.traces.TraceConfigs; +import android.tools.traces.io.ResultReader; +import android.tools.traces.io.ResultWriter; +import android.tools.traces.monitors.PerfettoTraceMonitor; + +import com.google.common.truth.Truth; +import com.google.protobuf.InvalidProtocolBufferException; + +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.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +import perfetto.protos.Protolog.ProtoLogViewerConfig; +import perfetto.protos.ProtologCommon; +import perfetto.protos.TraceOuterClass.Trace; +import perfetto.protos.TracePacketOuterClass.TracePacket; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.List; + +/** + * Test class for {@link ProtoLogImpl}. + */ +@Presubmit +@RunWith(MockitoJUnitRunner.class) +public class ProtoLogServiceTest { + + private static final String TEST_GROUP = "MY_TEST_GROUP"; + private static final String OTHER_TEST_GROUP = "MY_OTHER_TEST_GROUP"; + + private static final ProtoLogViewerConfig VIEWER_CONFIG = + ProtoLogViewerConfig.newBuilder() + .addGroups( + ProtoLogViewerConfig.Group.newBuilder() + .setId(1) + .setName(TEST_GROUP) + .setTag(TEST_GROUP) + ).addMessages( + ProtoLogViewerConfig.MessageData.newBuilder() + .setMessageId(1) + .setMessage("My Test Debug Log Message %b") + .setLevel(ProtologCommon.ProtoLogLevel.PROTOLOG_LEVEL_DEBUG) + .setGroupId(1) + ).addMessages( + ProtoLogViewerConfig.MessageData.newBuilder() + .setMessageId(2) + .setMessage("My Test Verbose Log Message %b") + .setLevel(ProtologCommon.ProtoLogLevel.PROTOLOG_LEVEL_VERBOSE) + .setGroupId(1) + ).build(); + + @Mock + IProtoLogClient mMockClient; + + @Mock + IProtoLogClient mSecondMockClient; + + @Mock + IBinder mMockClientBinder; + + @Mock + IBinder mSecondMockClientBinder; + + private final File mTracingDirectory = createTempDirectory("temp").toFile(); + + private final ResultWriter mWriter = new ResultWriter() + .forScenario(new ScenarioBuilder() + .forClass(createTempFile("temp", "").getName()).build()) + .withOutputDir(mTracingDirectory) + .setRunComplete(); + + private final TraceConfigs mTraceConfig = new TraceConfigs( + new TraceConfig(false, true, false), + new TraceConfig(false, true, false), + new TraceConfig(false, true, false), + new TraceConfig(false, true, false) + ); + + @Captor + ArgumentCaptor<IBinder.DeathRecipient> mDeathRecipientArgumentCaptor; + + @Captor + ArgumentCaptor<IBinder.DeathRecipient> mSecondDeathRecipientArgumentCaptor; + + private File mViewerConfigFile; + + public ProtoLogServiceTest() throws IOException { + } + + @Before + public void setUp() { + Mockito.when(mMockClient.asBinder()).thenReturn(mMockClientBinder); + Mockito.when(mSecondMockClient.asBinder()).thenReturn(mSecondMockClientBinder); + + try { + mViewerConfigFile = File.createTempFile("viewer-config", ".pb"); + try (var fos = new FileOutputStream(mViewerConfigFile); + BufferedOutputStream bos = new BufferedOutputStream(fos)) { + + bos.write(VIEWER_CONFIG.toByteArray()); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Test + public void canRegisterClientWithGroupsOnly() throws RemoteException { + final ProtoLogService service = new ProtoLogService(); + + final ProtoLogService.RegisterClientArgs args = new ProtoLogService.RegisterClientArgs() + .setGroups(new ProtoLogService.RegisterClientArgs.GroupConfig(TEST_GROUP, true)); + service.registerClient(mMockClient, args); + + Truth.assertThat(service.isLoggingToLogcat(TEST_GROUP)).isTrue(); + Truth.assertThat(service.getGroups()).asList().containsExactly(TEST_GROUP); + } + + @Test + public void willDumpViewerConfigOnlyOnceOnTraceStop() + throws RemoteException, InvalidProtocolBufferException { + final ProtoLogService service = new ProtoLogService(); + + final ProtoLogService.RegisterClientArgs args = new ProtoLogService.RegisterClientArgs() + .setGroups(new ProtoLogService.RegisterClientArgs.GroupConfig(TEST_GROUP, true)) + .setViewerConfigFile(mViewerConfigFile.getAbsolutePath()); + service.registerClient(mMockClient, args); + service.registerClient(mSecondMockClient, args); + + PerfettoTraceMonitor traceMonitor = + PerfettoTraceMonitor.newBuilder().enableProtoLog().build(); + + traceMonitor.start(); + traceMonitor.stop(mWriter); + final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig); + final byte[] traceData = reader.getArtifact() + .readBytes(new ResultArtifactDescriptor(TraceType.PERFETTO, Tag.ALL)); + + final Trace trace = Trace.parseFrom(traceData); + + final List<TracePacket> configPackets = trace.getPacketList().stream() + .filter(it -> it.hasProtologViewerConfig()) + // Exclude viewer configs from regular system tracing + .filter(it -> + it.getProtologViewerConfig().getGroups(0).getName().equals(TEST_GROUP)) + .toList(); + Truth.assertThat(configPackets).hasSize(1); + Truth.assertThat(configPackets.get(0).getProtologViewerConfig().toString()) + .isEqualTo(VIEWER_CONFIG.toString()); + } + + @Test + public void willDumpViewerConfigOnLastClientDisconnected() + throws RemoteException, FileNotFoundException { + final ProtoLogService.ViewerConfigFileTracer tracer = + Mockito.mock(ProtoLogService.ViewerConfigFileTracer.class); + final ProtoLogService service = new ProtoLogService(tracer); + + final ProtoLogService.RegisterClientArgs args = new ProtoLogService.RegisterClientArgs() + .setGroups(new ProtoLogService.RegisterClientArgs.GroupConfig( + TEST_GROUP, true)) + .setViewerConfigFile(mViewerConfigFile.getAbsolutePath()); + service.registerClient(mMockClient, args); + service.registerClient(mSecondMockClient, args); + + Mockito.verify(mMockClientBinder) + .linkToDeath(mDeathRecipientArgumentCaptor.capture(), anyInt()); + Mockito.verify(mSecondMockClientBinder) + .linkToDeath(mSecondDeathRecipientArgumentCaptor.capture(), anyInt()); + + mDeathRecipientArgumentCaptor.getValue().binderDied(); + Mockito.verify(tracer, never()).trace(any(), any()); + mSecondDeathRecipientArgumentCaptor.getValue().binderDied(); + Mockito.verify(tracer).trace(any(), eq(mViewerConfigFile.getAbsolutePath())); + } + + @Test + public void sendEnableLoggingToLogcatToClient() throws RemoteException { + final var service = new ProtoLogService(); + + final var args = new ProtoLogService.RegisterClientArgs() + .setGroups(new ProtoLogService.RegisterClientArgs.GroupConfig(TEST_GROUP, false)); + service.registerClient(mMockClient, args); + + Truth.assertThat(service.isLoggingToLogcat(TEST_GROUP)).isFalse(); + service.enableProtoLogToLogcat(TEST_GROUP); + Truth.assertThat(service.isLoggingToLogcat(TEST_GROUP)).isTrue(); + + Mockito.verify(mMockClient).toggleLogcat(eq(true), + Mockito.argThat(it -> it.length == 1 && it[0].equals(TEST_GROUP))); + } + + @Test + public void sendDisableLoggingToLogcatToClient() throws RemoteException { + final ProtoLogService service = new ProtoLogService(); + + final ProtoLogService.RegisterClientArgs args = new ProtoLogService.RegisterClientArgs() + .setGroups(new ProtoLogService.RegisterClientArgs.GroupConfig(TEST_GROUP, true)); + service.registerClient(mMockClient, args); + + Truth.assertThat(service.isLoggingToLogcat(TEST_GROUP)).isTrue(); + service.disableProtoLogToLogcat(TEST_GROUP); + Truth.assertThat(service.isLoggingToLogcat(TEST_GROUP)).isFalse(); + + Mockito.verify(mMockClient).toggleLogcat(eq(false), + Mockito.argThat(it -> it.length == 1 && it[0].equals(TEST_GROUP))); + } + + @Test + public void doNotSendLoggingToLogcatToClientWithoutRegisteredGroup() throws RemoteException { + final ProtoLogService service = new ProtoLogService(); + + final ProtoLogService.RegisterClientArgs args = new ProtoLogService.RegisterClientArgs() + .setGroups(new ProtoLogService.RegisterClientArgs.GroupConfig(TEST_GROUP, false)); + service.registerClient(mMockClient, args); + + Truth.assertThat(service.isLoggingToLogcat(TEST_GROUP)).isFalse(); + service.enableProtoLogToLogcat(OTHER_TEST_GROUP); + Truth.assertThat(service.isLoggingToLogcat(TEST_GROUP)).isFalse(); + + Mockito.verify(mMockClient, never()).toggleLogcat(anyBoolean(), any()); + } + + @Test + public void handlesToggleToLogcatBeforeClientIsRegistered() throws RemoteException { + final ProtoLogService service = new ProtoLogService(); + + Truth.assertThat(service.getGroups()).asList().doesNotContain(TEST_GROUP); + service.enableProtoLogToLogcat(TEST_GROUP); + Truth.assertThat(service.isLoggingToLogcat(TEST_GROUP)).isTrue(); + + final ProtoLogService.RegisterClientArgs args = new ProtoLogService.RegisterClientArgs() + .setGroups(new ProtoLogService.RegisterClientArgs.GroupConfig(TEST_GROUP, false)); + service.registerClient(mMockClient, args); + + Mockito.verify(mMockClient).toggleLogcat(eq(true), + Mockito.argThat(it -> it.length == 1 && it[0].equals(TEST_GROUP))); + } +} |