diff options
827 files changed, 26911 insertions, 10341 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index 3620a11fe036..59a7cbc16587 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -21,6 +21,7 @@ aconfig_declarations_group { // !!! KEEP THIS LIST ALPHABETICAL !!! "aconfig_mediacodec_flags_java_lib", "android.adaptiveauth.flags-aconfig-java", + "android.app.appfunctions.flags-aconfig-java", "android.app.contextualsearch.flags-aconfig-java", "android.app.flags-aconfig-java", "android.app.ondeviceintelligence-aconfig-java", @@ -172,6 +173,7 @@ cc_aconfig_library { // Window aconfig_declarations { name: "com.android.window.flags.window-aconfig", + exportable: true, package: "com.android.window.flags", container: "system", srcs: ["core/java/android/window/flags/*.aconfig"], @@ -1108,6 +1110,7 @@ cc_aconfig_library { // Chooser / "Sharesheet" aconfig_declarations { name: "android.service.chooser.flags-aconfig", + exportable: true, package: "android.service.chooser", container: "system", srcs: ["core/java/android/service/chooser/flags.aconfig"], @@ -1381,6 +1384,21 @@ java_aconfig_library { defaults: ["framework-minus-apex-aconfig-java-defaults"], } +// AppFunctions +aconfig_declarations { + name: "android.app.appfunctions.flags-aconfig", + exportable: true, + package: "android.app.appfunctions.flags", + container: "system", + srcs: ["core/java/android/app/appfunctions/flags/flags.aconfig"], +} + +java_aconfig_library { + name: "android.app.appfunctions.flags-aconfig-java", + aconfig_declarations: "android.app.appfunctions.flags-aconfig", + defaults: ["framework-minus-apex-aconfig-java-defaults"], +} + // Adaptive Auth aconfig_declarations { name: "android.adaptiveauth.flags-aconfig", diff --git a/Ravenwood.bp b/Ravenwood.bp index 7faa33f8834e..5f32ba026b50 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", @@ -41,148 +43,101 @@ genrule_defaults { ], out: [ "ravenwood.jar", - - // Following files are created just as FYI. - "hoststubgen_framework-minus-apex_keep_all.txt", - "hoststubgen_framework-minus-apex_dump.txt", - "hoststubgen_framework-minus-apex.log", - "hoststubgen_framework-minus-apex_stats.csv", - "hoststubgen_framework-minus-apex_apis.csv", ], - visibility: ["//visibility:private"], } +framework_minus_apex_cmd = "$(location hoststubgen) " + + "@$(location :ravenwood-standard-options) " + + "--debug-log $(location hoststubgen_framework-minus-apex.log) " + + "--out-impl-jar $(location ravenwood.jar) " + + "--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: "$(location hoststubgen) " + - "--num-shards 6 --shard-index 0 " + // 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 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) " + + cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 4", +} - "--out-impl-jar $(location ravenwood.jar) " + +java_genrule { + name: "framework-minus-apex.ravenwood-base_X5", + defaults: ["framework-minus-apex.ravenwood-base_defaults"], + cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 5", +} - "--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_X6", + defaults: ["framework-minus-apex.ravenwood-base_defaults"], + cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 6", +} - "--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_X7", + defaults: ["framework-minus-apex.ravenwood-base_defaults"], + cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 7", } java_genrule { - name: "framework-minus-apex.ravenwood-base_X5", + name: "framework-minus-apex.ravenwood-base_X8", 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 8", +} - "@$(location :ravenwood-standard-options) " + +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", +} - "--debug-log $(location hoststubgen_framework-minus-apex.log) " + +// 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. Also some of the dump files ("apis") may be slow even when sharded, because +// the output contains the information from all the input classes, rather than the output classes. +// 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 + "--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) " + + "--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) ", + out: [ + "hoststubgen_framework-minus-apex_keep_all.txt", + "hoststubgen_framework-minus-apex_dump.txt", + "hoststubgen_framework-minus-apex_stats.csv", + "hoststubgen_framework-minus-apex_apis.csv", + ], } // Marge all the sharded jars @@ -198,73 +153,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 +223,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 +238,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/multiuser/Android.bp b/apct-tests/perftests/multiuser/Android.bp index 856dba3f804c..9eea712b33dd 100644 --- a/apct-tests/perftests/multiuser/Android.bp +++ b/apct-tests/perftests/multiuser/Android.bp @@ -45,3 +45,8 @@ filegroup { "trace_configs/trace_config_multi_user.textproto", ], } + +prebuilt_etc { + name: "trace_config_multi_user.textproto", + src: ":multi_user_trace_config", +} diff --git a/apct-tests/perftests/settingsprovider/src/android/provider/SettingsProviderPerfTest.java b/apct-tests/perftests/settingsprovider/src/android/provider/SettingsProviderPerfTest.java index c00c8d550885..06cd94263847 100644 --- a/apct-tests/perftests/settingsprovider/src/android/provider/SettingsProviderPerfTest.java +++ b/apct-tests/perftests/settingsprovider/src/android/provider/SettingsProviderPerfTest.java @@ -36,7 +36,7 @@ import java.util.List; @RunWith(AndroidJUnit4.class) public final class SettingsProviderPerfTest { - private static final String NAMESPACE = "test@namespace"; + private static final String NAMESPACE = "testing"; private static final String SETTING_NAME1 = "test:setting1"; private static final String SETTING_NAME2 = "test-setting2"; private static final String UNSET_SETTING = "test_unset_setting"; diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java index 18ee6f2c7992..ba66ff72bfdd 100644 --- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java +++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java @@ -4441,6 +4441,11 @@ public class AlarmManagerService extends SystemService { public void run() { ArrayList<Alarm> triggerList = new ArrayList<Alarm>(); + synchronized (mLock) { + mLastTimeChangeClockTime = mInjector.getCurrentTimeMillis(); + mLastTimeChangeRealtime = mInjector.getElapsedRealtimeMillis(); + } + while (true) { int result = mInjector.waitForAlarm(); final long nowRTC = mInjector.getCurrentTimeMillis(); @@ -4464,10 +4469,9 @@ public class AlarmManagerService extends SystemService { expectedClockTime = lastTimeChangeClockTime + (nowELAPSED - mLastTimeChangeRealtime); } - if (lastTimeChangeClockTime == 0 || nowRTC < (expectedClockTime - 1000) + if (nowRTC < (expectedClockTime - 1000) || nowRTC > (expectedClockTime + 1000)) { - // The change is by at least +/- 1000 ms (or this is the first change), - // let's do it! + // The change is by at least +/- 1000 ms, let's do it! if (DEBUG_BATCH) { Slog.v(TAG, "Time changed notification from kernel; rebatching"); } diff --git a/api/Android.bp b/api/Android.bp index d931df165a8f..341be3d53844 100644 --- a/api/Android.bp +++ b/api/Android.bp @@ -284,7 +284,7 @@ packages_to_document = [ // These are libs from framework-internal-utils that are required (i.e. being referenced) // from framework-non-updatable-sources. Add more here when there's a need. // DO NOT add the entire framework-internal-utils. It might cause unnecessary circular -// dependencies gets bigger. +// dependencies when the list gets bigger. android_non_updatable_stubs_libs = [ "android.hardware.cas-V1.2-java", "android.hardware.health-V1.0-java-constants", @@ -384,6 +384,11 @@ non_updatable_api_deps_on_modules = [ "sdk_system_current_android", ] +java_defaults { + name: "module-classpath-java-defaults", + libs: non_updatable_api_deps_on_modules, +} + // Defaults with module APIs in the classpath (mostly from prebuilts). // Suitable for compiling android-non-updatable. stubs_defaults { diff --git a/api/StubLibraries.bp b/api/StubLibraries.bp index 8dfddf0e13c8..d991da59f167 100644 --- a/api/StubLibraries.bp +++ b/api/StubLibraries.bp @@ -563,8 +563,12 @@ java_library { java_defaults { name: "android-non-updatable_from_text_defaults", + defaults: ["android-non-updatable-stubs-libs-defaults"], static_libs: ["framework-res-package-jar"], libs: ["stub-annotations"], + sdk_version: "none", + system_modules: "none", + previous_api: ":android.api.public.latest", } java_defaults { @@ -582,10 +586,10 @@ java_api_library { "api-stubs-docs-non-updatable.api.contribution", ], defaults: ["android-non-updatable_everything_from_text_defaults"], - full_api_surface_stub: "android_stubs_current.from-text", // Use full Android API not just the non-updatable API as the latter is incomplete // and can result in incorrect behavior. previous_api: ":android.api.combined.public.latest", + libs: ["all-modules-public-stubs"], } java_api_library { @@ -596,10 +600,10 @@ java_api_library { "system-api-stubs-docs-non-updatable.api.contribution", ], defaults: ["android-non-updatable_everything_from_text_defaults"], - full_api_surface_stub: "android_system_stubs_current.from-text", // Use full Android API not just the non-updatable API as the latter is incomplete // and can result in incorrect behavior. previous_api: ":android.api.combined.system.latest", + libs: ["all-modules-system-stubs"], } java_api_library { @@ -611,10 +615,10 @@ java_api_library { "test-api-stubs-docs-non-updatable.api.contribution", ], defaults: ["android-non-updatable_everything_from_text_defaults"], - full_api_surface_stub: "android_test_stubs_current.from-text", // Use full Android API not just the non-updatable API as the latter is incomplete // and can result in incorrect behavior. previous_api: ":android.api.combined.test.latest", + libs: ["all-modules-system-stubs"], } java_api_library { @@ -625,8 +629,10 @@ java_api_library { "system-api-stubs-docs-non-updatable.api.contribution", "module-lib-api-stubs-docs-non-updatable.api.contribution", ], - defaults: ["android-non-updatable_everything_from_text_defaults"], - full_api_surface_stub: "android_module_lib_stubs_current_full.from-text", + defaults: [ + "module-classpath-java-defaults", + "android-non-updatable_everything_from_text_defaults", + ], // Use full Android API not just the non-updatable API as the latter is incomplete // and can result in incorrect behavior. previous_api: ":android.api.combined.module-lib.latest", @@ -644,14 +650,16 @@ java_api_library { "test-api-stubs-docs-non-updatable.api.contribution", "module-lib-api-stubs-docs-non-updatable.api.contribution", ], - defaults: ["android-non-updatable_everything_from_text_defaults"], - full_api_surface_stub: "android_test_module_lib_stubs_current.from-text", + defaults: [ + "module-classpath-java-defaults", + "android-non-updatable_everything_from_text_defaults", + ], // No need to specify previous_api as this is not used for compiling against. - // This module is only used for hiddenapi, and other modules should not // depend on this module. visibility: ["//visibility:private"], + libs: ["all-modules-system-stubs"], } java_defaults { @@ -665,7 +673,7 @@ java_defaults { } java_library { - name: "android_stubs_current.from-source", + name: "android_stubs_current", static_libs: [ "all-modules-public-stubs", "android-non-updatable.stubs", @@ -675,7 +683,7 @@ java_library { } java_library { - name: "android_stubs_current_exportable.from-source", + name: "android_stubs_current_exportable", static_libs: [ "all-modules-public-stubs-exportable", "android-non-updatable.stubs.exportable", @@ -685,7 +693,7 @@ java_library { } java_library { - name: "android_system_stubs_current.from-source", + name: "android_system_stubs_current", static_libs: [ "all-modules-system-stubs", "android-non-updatable.stubs.system", @@ -698,7 +706,7 @@ java_library { } java_library { - name: "android_system_stubs_current_exportable.from-source", + name: "android_system_stubs_current_exportable", static_libs: [ "all-modules-system-stubs-exportable", "android-non-updatable.stubs.exportable.system", @@ -722,7 +730,7 @@ java_library { } java_library { - name: "android_test_stubs_current.from-source", + name: "android_test_stubs_current", static_libs: [ // Updatable modules do not have test APIs, but we want to include their SystemApis, like we // include the SystemApi of framework-non-updatable-sources. @@ -739,7 +747,7 @@ java_library { } java_library { - name: "android_test_stubs_current_exportable.from-source", + name: "android_test_stubs_current_exportable", static_libs: [ // Updatable modules do not have test APIs, but we want to include their SystemApis, like we // include the SystemApi of framework-non-updatable-sources. @@ -760,7 +768,7 @@ java_library { // This module does not need to be copied to dist java_library { - name: "android_test_frameworks_core_stubs_current.from-source", + name: "android_test_frameworks_core_stubs_current", static_libs: [ "all-updatable-modules-system-stubs", "android-non-updatable.stubs.test", @@ -772,7 +780,7 @@ java_library { } java_library { - name: "android_module_lib_stubs_current.from-source", + name: "android_module_lib_stubs_current", defaults: [ "android.jar_defaults", ], @@ -785,7 +793,7 @@ java_library { } java_library { - name: "android_module_lib_stubs_current_exportable.from-source", + name: "android_module_lib_stubs_current_exportable", defaults: [ "android.jar_defaults", "android_stubs_dists_default", @@ -801,20 +809,20 @@ java_library { } java_library { - name: "android_system_server_stubs_current.from-source", + name: "android_system_server_stubs_current", defaults: [ "android.jar_defaults", ], srcs: [":services-non-updatable-stubs"], installable: false, static_libs: [ - "android_module_lib_stubs_current.from-source", + "android_module_lib_stubs_current", ], visibility: ["//frameworks/base/services"], } java_library { - name: "android_system_server_stubs_current_exportable.from-source", + name: "android_system_server_stubs_current_exportable", defaults: [ "android.jar_defaults", "android_stubs_dists_default", @@ -822,7 +830,7 @@ java_library { srcs: [":services-non-updatable-stubs{.exportable}"], installable: false, static_libs: [ - "android_module_lib_stubs_current_exportable.from-source", + "android_module_lib_stubs_current_exportable", ], dist: { dir: "apistubs/android/system-server", @@ -897,215 +905,6 @@ java_genrule { }, } -// -// Java API defaults and libraries for single tree build -// - -java_defaults { - name: "stub-annotation-defaults", - libs: [ - "stub-annotations", - ], - static_libs: [ - // stub annotations do not contribute to the API surfaces but are statically - // linked in the stubs for API surfaces (see frameworks/base/StubLibraries.bp). - // This is because annotation processors insist on loading the classes for any - // annotations found, thus should exist inside android.jar. - "private-stub-annotations-jar", - ], - is_stubs_module: true, -} - -// Listing of API domains contribution and dependencies per API surfaces -java_defaults { - name: "android_test_stubs_current_contributions", - api_surface: "test", - api_contributions: [ - "framework-virtualization.stubs.source.test.api.contribution", - "framework-location.stubs.source.test.api.contribution", - ], -} - -java_defaults { - name: "android_test_frameworks_core_stubs_current_contributions", - api_surface: "test", - api_contributions: [ - "test-api-stubs-docs-non-updatable.api.contribution", - ], -} - -java_defaults { - name: "android_module_lib_stubs_current_contributions", - api_surface: "module-lib", - api_contributions: [ - "api-stubs-docs-non-updatable.api.contribution", - "system-api-stubs-docs-non-updatable.api.contribution", - "module-lib-api-stubs-docs-non-updatable.api.contribution", - "art.module.public.api.stubs.source.api.contribution", - "art.module.public.api.stubs.source.system.api.contribution", - "art.module.public.api.stubs.source.module_lib.api.contribution", - "i18n.module.public.api.stubs.source.api.contribution", - "i18n.module.public.api.stubs.source.system.api.contribution", - "i18n.module.public.api.stubs.source.module_lib.api.contribution", - ], - previous_api: ":android.api.combined.module-lib.latest", -} - -// Java API library definitions per API surface -java_api_library { - name: "android_stubs_current.from-text", - api_surface: "public", - defaults: [ - // This module is dynamically created at frameworks/base/api/api.go - // instead of being written out, in order to minimize edits in the codebase - // when there is a change in the list of modules. - // that contributes to an api surface. - "android_stubs_current_contributions", - "stub-annotation-defaults", - ], - api_contributions: [ - "api-stubs-docs-non-updatable.api.contribution", - ], - visibility: ["//visibility:public"], - enable_validation: false, - stubs_type: "everything", -} - -java_api_library { - name: "android_system_stubs_current.from-text", - api_surface: "system", - defaults: [ - "android_stubs_current_contributions", - "android_system_stubs_current_contributions", - "stub-annotation-defaults", - ], - api_contributions: [ - "api-stubs-docs-non-updatable.api.contribution", - "system-api-stubs-docs-non-updatable.api.contribution", - ], - visibility: ["//visibility:public"], - enable_validation: false, - stubs_type: "everything", -} - -java_api_library { - name: "android_test_stubs_current.from-text", - api_surface: "test", - defaults: [ - "android_stubs_current_contributions", - "android_system_stubs_current_contributions", - "android_test_stubs_current_contributions", - "stub-annotation-defaults", - ], - api_contributions: [ - "api-stubs-docs-non-updatable.api.contribution", - "system-api-stubs-docs-non-updatable.api.contribution", - "test-api-stubs-docs-non-updatable.api.contribution", - ], - visibility: ["//visibility:public"], - enable_validation: false, - stubs_type: "everything", -} - -java_api_library { - name: "android_test_frameworks_core_stubs_current.from-text", - api_surface: "test", - defaults: [ - "android_stubs_current_contributions", - "android_system_stubs_current_contributions", - "android_test_frameworks_core_stubs_current_contributions", - ], - libs: [ - "stub-annotations", - ], - api_contributions: [ - "api-stubs-docs-non-updatable.api.contribution", - "system-api-stubs-docs-non-updatable.api.contribution", - ], - enable_validation: false, - stubs_type: "everything", -} - -java_api_library { - name: "android_module_lib_stubs_current_full.from-text", - api_surface: "module-lib", - defaults: [ - "android_stubs_current_contributions", - "android_system_stubs_current_contributions", - "android_module_lib_stubs_current_contributions_full", - ], - libs: [ - "stub-annotations", - ], - api_contributions: [ - "api-stubs-docs-non-updatable.api.contribution", - "system-api-stubs-docs-non-updatable.api.contribution", - "module-lib-api-stubs-docs-non-updatable.api.contribution", - ], - visibility: ["//visibility:public"], - enable_validation: false, - stubs_type: "everything", -} - -java_api_library { - name: "android_module_lib_stubs_current.from-text", - api_surface: "module-lib", - defaults: [ - "android_module_lib_stubs_current_contributions", - ], - libs: [ - "android_module_lib_stubs_current_full.from-text", - "stub-annotations", - ], - visibility: ["//visibility:public"], - enable_validation: false, - stubs_type: "everything", -} - -java_api_library { - name: "android_test_module_lib_stubs_current.from-text", - api_surface: "module-lib", - defaults: [ - "android_stubs_current_contributions", - "android_system_stubs_current_contributions", - "android_test_stubs_current_contributions", - "android_module_lib_stubs_current_contributions", - ], - libs: [ - "android_module_lib_stubs_current_full.from-text", - "stub-annotations", - ], - api_contributions: [ - "test-api-stubs-docs-non-updatable.api.contribution", - ], - - // This module is only used to build android-non-updatable.stubs.test_module_lib - // and other modules should not depend on this module. - visibility: [ - "//visibility:private", - ], - enable_validation: false, - stubs_type: "everything", -} - -java_api_library { - name: "android_system_server_stubs_current.from-text", - api_surface: "system-server", - api_contributions: [ - "services-non-updatable-stubs.api.contribution", - ], - libs: [ - "android_module_lib_stubs_current.from-text", - "stub-annotations", - ], - static_libs: [ - "android_module_lib_stubs_current.from-text", - ], - visibility: ["//visibility:public"], - enable_validation: false, - stubs_type: "everything", -} - //////////////////////////////////////////////////////////////////////// // api-versions.xml generation, for public and system. This API database // also contains the android.test.* APIs. diff --git a/api/api.go b/api/api.go index b6b1a7e44510..5b7f534443fb 100644 --- a/api/api.go +++ b/api/api.go @@ -15,9 +15,7 @@ package api import ( - "fmt" "sort" - "strings" "github.com/google/blueprint/proptools" @@ -464,79 +462,6 @@ func createMergedTxts(ctx android.LoadHookContext, bootclasspath, system_server_ } } -func createApiContributionDefaults(ctx android.LoadHookContext, modules []string) { - defaultsSdkKinds := []android.SdkKind{ - android.SdkPublic, android.SdkSystem, android.SdkModule, - } - for _, sdkKind := range defaultsSdkKinds { - props := defaultsProps{} - props.Name = proptools.StringPtr( - sdkKind.DefaultJavaLibraryName() + "_contributions") - if sdkKind == android.SdkModule { - props.Name = proptools.StringPtr( - sdkKind.DefaultJavaLibraryName() + "_contributions_full") - } - props.Api_surface = proptools.StringPtr(sdkKind.String()) - apiSuffix := "" - if sdkKind != android.SdkPublic { - apiSuffix = "." + strings.ReplaceAll(sdkKind.String(), "-", "_") - } - props.Api_contributions = transformArray( - modules, "", fmt.Sprintf(".stubs.source%s.api.contribution", apiSuffix)) - props.Defaults_visibility = []string{"//visibility:public"} - props.Previous_api = proptools.StringPtr(":android.api.combined." + sdkKind.String() + ".latest") - ctx.CreateModule(java.DefaultsFactory, &props) - } -} - -func createFullApiLibraries(ctx android.LoadHookContext) { - javaLibraryNames := []string{ - "android_stubs_current", - "android_system_stubs_current", - "android_test_stubs_current", - "android_test_frameworks_core_stubs_current", - "android_module_lib_stubs_current", - "android_system_server_stubs_current", - } - - for _, libraryName := range javaLibraryNames { - props := libraryProps{} - props.Name = proptools.StringPtr(libraryName) - staticLib := libraryName + ".from-source" - if ctx.Config().BuildFromTextStub() { - staticLib = libraryName + ".from-text" - } - props.Static_libs = []string{staticLib} - props.Defaults = []string{"android.jar_defaults"} - props.Visibility = []string{"//visibility:public"} - props.Is_stubs_module = proptools.BoolPtr(true) - - ctx.CreateModule(java.LibraryFactory, &props) - } -} - -func createFullExportableApiLibraries(ctx android.LoadHookContext) { - javaLibraryNames := []string{ - "android_stubs_current_exportable", - "android_system_stubs_current_exportable", - "android_test_stubs_current_exportable", - "android_module_lib_stubs_current_exportable", - "android_system_server_stubs_current_exportable", - } - - for _, libraryName := range javaLibraryNames { - props := libraryProps{} - props.Name = proptools.StringPtr(libraryName) - staticLib := libraryName + ".from-source" - props.Static_libs = []string{staticLib} - props.Defaults = []string{"android.jar_defaults"} - props.Visibility = []string{"//visibility:public"} - props.Is_stubs_module = proptools.BoolPtr(true) - - ctx.CreateModule(java.LibraryFactory, &props) - } -} - func (a *CombinedApis) createInternalModules(ctx android.LoadHookContext) { bootclasspath := a.bootclasspath(ctx) system_server_classpath := a.systemServerClasspath(ctx) @@ -562,12 +487,6 @@ func (a *CombinedApis) createInternalModules(ctx android.LoadHookContext) { createMergedAnnotationsFilegroups(ctx, bootclasspath, system_server_classpath) createPublicStubsSourceFilegroup(ctx, bootclasspath) - - createApiContributionDefaults(ctx, bootclasspath) - - createFullApiLibraries(ctx) - - createFullExportableApiLibraries(ctx) } func combinedApisModuleFactory() android.Module { diff --git a/api/api_test.go b/api/api_test.go index 47d167093b39..fb26f821eec1 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -52,6 +52,12 @@ func gatherRequiredDepsForTest() string { "core.current.stubs", "ext", "framework", + "android_stubs_current", + "android_system_stubs_current", + "android_test_stubs_current", + "android_test_frameworks_core_stubs_current", + "android_module_lib_stubs_current", + "android_system_server_stubs_current", "android_stubs_current.from-text", "android_system_stubs_current.from-text", "android_test_stubs_current.from-text", @@ -190,61 +196,60 @@ func TestCombinedApisDefaults(t *testing.T) { } }), ).RunTestWithBp(t, ` - java_sdk_library { - name: "framework-foo", - srcs: ["a.java"], - public: { - enabled: true, - }, - system: { - enabled: true, - }, - test: { - enabled: true, - }, - module_lib: { - enabled: true, - }, - api_packages: [ - "foo", - ], - sdk_version: "core_current", - annotations_enabled: true, - } + java_sdk_library { + name: "framework-foo", + srcs: ["a.java"], + public: { + enabled: true, + }, + system: { + enabled: true, + }, + test: { + enabled: true, + }, + module_lib: { + enabled: true, + }, + api_packages: [ + "foo", + ], + sdk_version: "core_current", + annotations_enabled: true, + } + java_sdk_library { + name: "framework-bar", + srcs: ["a.java"], + public: { + enabled: true, + }, + system: { + enabled: true, + }, + test: { + enabled: true, + }, + module_lib: { + enabled: true, + }, + api_packages: [ + "foo", + ], + sdk_version: "core_current", + annotations_enabled: true, + } - java_sdk_library { - name: "framework-bar", - srcs: ["a.java"], - public: { - enabled: true, - }, - system: { - enabled: true, - }, - test: { - enabled: true, - }, - module_lib: { - enabled: true, - }, - api_packages: [ - "foo", + combined_apis { + name: "foo", + bootclasspath: [ + "framework-bar", + ] + select(boolean_var_for_testing(), { + true: [ + "framework-foo", ], - sdk_version: "core_current", - annotations_enabled: true, - } - - combined_apis { - name: "foo", - bootclasspath: [ - "framework-bar", - ] + select(boolean_var_for_testing(), { - true: [ - "framework-foo", - ], - default: [], - }), - } + default: [], + }), + } `) subModuleDependsOnSelectAppendedModule := java.CheckModuleHasDependency(t, diff --git a/core/api/current.txt b/core/api/current.txt index 354e26b2eb02..36c9175830a3 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -10684,6 +10684,7 @@ package android.content { field public static final String ACTIVITY_SERVICE = "activity"; field public static final String ALARM_SERVICE = "alarm"; field public static final String APPWIDGET_SERVICE = "appwidget"; + field @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public static final String APP_FUNCTION_SERVICE = "app_function"; field public static final String APP_OPS_SERVICE = "appops"; field public static final String APP_SEARCH_SERVICE = "app_search"; field public static final String AUDIO_SERVICE = "audio"; @@ -59034,7 +59035,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 +60100,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/Activity.java b/core/java/android/app/Activity.java index 90de7abf845c..4fb35c3d5f5c 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -7312,7 +7312,7 @@ public class Activity extends ContextThemeWrapper @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) private void finish(int finishTask) { if (DEBUG_FINISH_ACTIVITY) { - Log.d("Instrumentation", "finishActivity: finishTask=" + finishTask, new Throwable()); + Log.d(Instrumentation.TAG, "finishActivity: finishTask=" + finishTask, new Throwable()); } if (mParent == null) { int resultCode; diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index 74e95839b2f5..be70de20c2e2 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -16,6 +16,7 @@ package android.app; +import static android.app.Instrumentation.DEBUG_FINISH_ACTIVITY; import static android.app.WindowConfiguration.activityTypeToString; import static android.app.WindowConfiguration.windowingModeToString; import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS; @@ -80,6 +81,7 @@ import android.os.WorkSource; import android.text.TextUtils; import android.util.ArrayMap; import android.util.DisplayMetrics; +import android.util.Log; import android.util.Singleton; import android.util.Size; import android.view.WindowInsetsController.Appearance; @@ -6011,6 +6013,10 @@ public class ActivityManager { * Finishes all activities in this task and removes it from the recent tasks list. */ public void finishAndRemoveTask() { + if (DEBUG_FINISH_ACTIVITY) { + Log.d(Instrumentation.TAG, "AppTask#finishAndRemoveTask: task=" + + getTaskInfo(), new Throwable()); + } try { mAppTaskImpl.finishAndRemoveTask(); } catch (RemoteException e) { diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java index 89efa9b77a60..d31881265064 100644 --- a/core/java/android/app/ActivityManagerInternal.java +++ b/core/java/android/app/ActivityManagerInternal.java @@ -961,6 +961,17 @@ public abstract class ActivityManagerInternal { @Nullable VoiceInteractionManagerProvider provider); /** + * Get whether or not the previous user's packages will be killed before the user is + * stopped during a user switch. + * + * <p> The primary use case of this method is for {@link com.android.server.SystemService} + * classes to call this API in their + * {@link com.android.server.SystemService#onUserSwitching} method implementation to prevent + * restarting any of the previous user's processes that will be killed during the user switch. + */ + public abstract boolean isEarlyPackageKillEnabledForUserSwitch(int fromUserId, int toUserId); + + /** * Sets whether the current foreground user (and its profiles) should be stopped after switched * out. */ 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/app/DisabledWallpaperManager.java b/core/java/android/app/DisabledWallpaperManager.java index 4a5836cef76d..b06fb9e2f284 100644 --- a/core/java/android/app/DisabledWallpaperManager.java +++ b/core/java/android/app/DisabledWallpaperManager.java @@ -15,11 +15,16 @@ */ package android.app; +import android.annotation.FloatRange; import android.annotation.NonNull; +import android.annotation.Nullable; +import android.compat.annotation.UnsupportedAppUsage; import android.content.ComponentName; import android.content.Intent; import android.graphics.Bitmap; +import android.graphics.Point; import android.graphics.Rect; +import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; @@ -27,9 +32,12 @@ import android.os.Handler; import android.os.IBinder; import android.os.ParcelFileDescriptor; import android.util.Log; +import android.util.SparseArray; import java.io.IOException; import java.io.InputStream; +import java.util.List; +import java.util.Map; /** * A no-op implementation of {@link WallpaperManager}. @@ -54,29 +62,19 @@ final class DisabledWallpaperManager extends WallpaperManager { private DisabledWallpaperManager() { } - @Override - public boolean isWallpaperSupported() { - return false; + @UnsupportedAppUsage + public IWallpaperManager getIWallpaperManager() { + return unsupported(); } @Override - public boolean isSetWallpaperAllowed() { - return false; - } - - private static <T> T unsupported() { - if (DEBUG) Log.w(TAG, "unsupported method called; returning null", new Exception()); - return null; - } - - private static boolean unsupportedBoolean() { - if (DEBUG) Log.w(TAG, "unsupported method called; returning false", new Exception()); - return false; + public boolean isLockscreenLiveWallpaperEnabled() { + return unsupportedBoolean(); } - private static int unsupportedInt() { - if (DEBUG) Log.w(TAG, "unsupported method called; returning -1", new Exception()); - return -1; + @Override + public boolean shouldEnableWideColorGamut() { + return unsupportedBoolean(); } @Override @@ -122,6 +120,11 @@ final class DisabledWallpaperManager extends WallpaperManager { } @Override + public boolean wallpaperSupportsWcg(int which) { + return unsupportedBoolean(); + } + + @Override public Bitmap getBitmap() { return unsupported(); } @@ -131,12 +134,61 @@ final class DisabledWallpaperManager extends WallpaperManager { return unsupported(); } + @Nullable + public Bitmap getBitmap(boolean hardware, @SetWallpaperFlags int which) { + return unsupported(); + } + @Override public Bitmap getBitmapAsUser(int userId, boolean hardware) { return unsupported(); } @Override + public Bitmap getBitmapAsUser(int userId, boolean hardware, @SetWallpaperFlags int which) { + return unsupported(); + } + + @Override + public Bitmap getBitmapAsUser(int userId, boolean hardware, + @SetWallpaperFlags int which, boolean returnDefault) { + return unsupported(); + } + + @Override + public Rect peekBitmapDimensions() { + return unsupported(); + } + + @Override + public Rect peekBitmapDimensions(@SetWallpaperFlags int which) { + return unsupported(); + } + + @Nullable + public Rect peekBitmapDimensions(@SetWallpaperFlags int which, boolean returnDefault) { + return unsupported(); + } + + @Override + public List<Rect> getBitmapCrops(@NonNull List<Point> displaySizes, + @SetWallpaperFlags int which, boolean originalBitmap) { + return unsupported(); + } + + @Override + public List<Rect> getBitmapCrops(@NonNull Point bitmapSize, @NonNull List<Point> displaySizes, + @Nullable Map<Point, Rect> cropHints) { + return unsupported(); + } + + @Override + public WallpaperColors getWallpaperColors(@NonNull Bitmap bitmap, + @Nullable Map<Point, Rect> cropHints) { + return unsupported(); + } + + @Override public ParcelFileDescriptor getWallpaperFile(int which) { return unsupported(); } @@ -173,6 +225,17 @@ final class DisabledWallpaperManager extends WallpaperManager { } @Override + public void addOnColorsChangedListener(@NonNull LocalWallpaperColorConsumer callback, + List<RectF> regions, int which) throws IllegalArgumentException { + unsupported(); + } + + @Override + public void removeOnColorsChangedListener(@NonNull LocalWallpaperColorConsumer callback) { + unsupported(); + } + + @Override public ParcelFileDescriptor getWallpaperFile(int which, int userId) { return unsupported(); } @@ -192,23 +255,22 @@ final class DisabledWallpaperManager extends WallpaperManager { return unsupported(); } - @Override - public ParcelFileDescriptor getWallpaperInfoFile() { + public WallpaperInfo getWallpaperInfoForUser(int userId) { return unsupported(); } @Override - public WallpaperInfo getWallpaperInfoForUser(int userId) { + public WallpaperInfo getWallpaperInfo(@SetWallpaperFlags int which) { return unsupported(); } @Override - public WallpaperInfo getWallpaperInfo(@SetWallpaperFlags int which) { + public WallpaperInfo getWallpaperInfo(@SetWallpaperFlags int which, int userId) { return unsupported(); } @Override - public WallpaperInfo getWallpaperInfo(@SetWallpaperFlags int which, int userId) { + public ParcelFileDescriptor getWallpaperInfoFile() { return unsupported(); } @@ -264,6 +326,11 @@ final class DisabledWallpaperManager extends WallpaperManager { return 0; } + public int setBitmapWithCrops(@Nullable Bitmap fullImage, @NonNull Map<Point, Rect> cropHints, + boolean allowBackup, @SetWallpaperFlags int which) throws IOException { + return unsupportedInt(); + } + @Override public void setStream(InputStream bitmapData) throws IOException { unsupported(); @@ -284,6 +351,19 @@ final class DisabledWallpaperManager extends WallpaperManager { } @Override + public int setStreamWithCrops(InputStream bitmapData, @NonNull Map<Point, Rect> cropHints, + boolean allowBackup, @SetWallpaperFlags int which) throws IOException { + return unsupportedInt(); + } + + + @Override + public int setStreamWithCrops(InputStream bitmapData, @NonNull SparseArray<Rect> cropHints, + boolean allowBackup, @SetWallpaperFlags int which) throws IOException { + return unsupportedInt(); + } + + @Override public boolean hasResourceWallpaper(int resid) { return unsupportedBoolean(); } @@ -328,12 +408,40 @@ final class DisabledWallpaperManager extends WallpaperManager { return unsupportedBoolean(); } + + @Override + public void setWallpaperDimAmount(@FloatRange(from = 0f, to = 1f) float dimAmount) { + unsupported(); + } + + @Override + public @FloatRange(from = 0f, to = 1f) float getWallpaperDimAmount() { + return unsupportedInt(); + } + + @Override + public boolean lockScreenWallpaperExists() { + return unsupportedBoolean(); + } + @Override public boolean setWallpaperComponent(ComponentName name, int userId) { return unsupportedBoolean(); } @Override + public boolean setWallpaperComponentWithFlags(@NonNull ComponentName name, + @SetWallpaperFlags int which) { + return unsupportedBoolean(); + } + + @Override + public boolean setWallpaperComponentWithFlags(@NonNull ComponentName name, + @SetWallpaperFlags int which, int userId) { + return unsupportedBoolean(); + } + + @Override public void setWallpaperOffsets(IBinder windowToken, float xOffset, float yOffset) { unsupported(); } @@ -350,6 +458,21 @@ final class DisabledWallpaperManager extends WallpaperManager { } @Override + public void setWallpaperZoomOut(@NonNull IBinder windowToken, float zoom) { + unsupported(); + } + + @Override + public boolean isWallpaperSupported() { + return false; + } + + @Override + public boolean isSetWallpaperAllowed() { + return false; + } + + @Override public void clearWallpaperOffsets(IBinder windowToken) { unsupported(); } @@ -369,8 +492,18 @@ final class DisabledWallpaperManager extends WallpaperManager { return unsupportedBoolean(); } - @Override - public boolean wallpaperSupportsWcg(int which) { - return unsupportedBoolean(); + private static <T> T unsupported() { + if (DEBUG) Log.w(TAG, "unsupported method called; returning null", new Exception()); + return null; + } + + private static boolean unsupportedBoolean() { + if (DEBUG) Log.w(TAG, "unsupported method called; returning false", new Exception()); + return false; + } + + private static int unsupportedInt() { + if (DEBUG) Log.w(TAG, "unsupported method called; returning -1", new Exception()); + return -1; } } diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java index be270463e576..45852c7d338a 100644 --- a/core/java/android/app/Instrumentation.java +++ b/core/java/android/app/Instrumentation.java @@ -98,7 +98,7 @@ public class Instrumentation { */ public static final String REPORT_KEY_STREAMRESULT = "stream"; - private static final String TAG = "Instrumentation"; + static final String TAG = "Instrumentation"; private static final long CONNECT_TIMEOUT_MILLIS = 60_000; diff --git a/core/java/android/app/ResourcesManager.java b/core/java/android/app/ResourcesManager.java index fd4d8e90adf9..0cc210b7db41 100644 --- a/core/java/android/app/ResourcesManager.java +++ b/core/java/android/app/ResourcesManager.java @@ -1836,9 +1836,10 @@ public class ResourcesManager { // have shared library asset paths appended if there are any. if (r.getImpl() != null) { final ResourcesImpl oldImpl = r.getImpl(); + final AssetManager oldAssets = oldImpl.getAssets(); // ResourcesImpl constructor will help to append shared library asset paths. - if (oldImpl.getAssets().isUpToDate()) { - final ResourcesImpl newImpl = new ResourcesImpl(oldImpl.getAssets(), + if (oldAssets != AssetManager.getSystem() && oldAssets.isUpToDate()) { + final ResourcesImpl newImpl = new ResourcesImpl(oldAssets, oldImpl.getMetrics(), oldImpl.getConfiguration(), oldImpl.getDisplayAdjustments()); r.setImpl(newImpl); diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index e73f4718732f..114a2c4d5649 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -16,6 +16,8 @@ package android.app; +import static android.app.appfunctions.flags.Flags.enableAppFunctionManager; + import android.accounts.AccountManager; import android.accounts.IAccountManager; import android.adservices.AdServicesFrameworkInitializer; @@ -28,6 +30,8 @@ import android.app.admin.DevicePolicyManager; import android.app.admin.IDevicePolicyManager; import android.app.ambientcontext.AmbientContextManager; import android.app.ambientcontext.IAmbientContextManager; +import android.app.appfunctions.AppFunctionManager; +import android.app.appfunctions.IAppFunctionManager; import android.app.appsearch.AppSearchManagerFrameworkInitializer; import android.app.blob.BlobStoreManagerFrameworkInitializer; import android.app.contentsuggestions.ContentSuggestionsManager; @@ -925,6 +929,21 @@ public final class SystemServiceRegistry { return new CompanionDeviceManager(service, ctx.getOuterContext()); }}); + if (enableAppFunctionManager()) { + registerService(Context.APP_FUNCTION_SERVICE, AppFunctionManager.class, + new CachedServiceFetcher<>() { + @Override + public AppFunctionManager createService(ContextImpl ctx) + throws ServiceNotFoundException { + IAppFunctionManager service; + //TODO(b/357551503): If the feature not present avoid look up every time + service = IAppFunctionManager.Stub.asInterface( + ServiceManager.getServiceOrThrow(Context.APP_FUNCTION_SERVICE)); + return new AppFunctionManager(service, ctx.getOuterContext()); + } + }); + } + registerService(Context.VIRTUAL_DEVICE_SERVICE, VirtualDeviceManager.class, new CachedServiceFetcher<VirtualDeviceManager>() { @Override diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java index 1a72df10fbd6..5903a7ff619c 100644 --- a/core/java/android/app/WallpaperManager.java +++ b/core/java/android/app/WallpaperManager.java @@ -123,6 +123,8 @@ import java.util.concurrent.TimeUnit; * <p> An app can check whether wallpapers are supported for the current user, by calling * {@link #isWallpaperSupported()}, and whether setting of wallpapers is allowed, by calling * {@link #isSetWallpaperAllowed()}. + * Any public APIs added to WallpaperManager should have a corresponding stub in + * {@link DisabledWallpaperManager}. */ @SystemService(Context.WALLPAPER_SERVICE) public class WallpaperManager { diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig index c789af32e2b1..9148e3c3a072 100644 --- a/core/java/android/app/admin/flags/flags.aconfig +++ b/core/java/android/app/admin/flags/flags.aconfig @@ -37,7 +37,6 @@ flag { } } - flag { name: "onboarding_bugreport_v2_enabled" is_exported: true @@ -403,3 +402,13 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "dont_read_policy_definition" + namespace: "enterprise" + description: "Rely on <policy-key-entry> to determine policy definition and ignore <policy-definition-entry>" + bug: "335663055" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/android/app/appfunctions/AppFunctionManager.java b/core/java/android/app/appfunctions/AppFunctionManager.java new file mode 100644 index 000000000000..a01e373c5e83 --- /dev/null +++ b/core/java/android/app/appfunctions/AppFunctionManager.java @@ -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. + */ + +package android.app.appfunctions; + +import android.annotation.SystemService; +import android.content.Context; + +/** + * Provides app functions related functionalities. + * + * <p>App function is a specific piece of functionality that an app offers to the system. These + * functionalities can be integrated into various system features. + * + * @hide + */ +@SystemService(Context.APP_FUNCTION_SERVICE) +public final class AppFunctionManager { + private final IAppFunctionManager mService; + private final Context mContext; + + /** + * TODO(b/357551503): add comments when implement this class + * + * @hide + */ + public AppFunctionManager(IAppFunctionManager mService, Context context) { + this.mService = mService; + this.mContext = context; + } +} diff --git a/core/java/android/app/appfunctions/IAppFunctionManager.aidl b/core/java/android/app/appfunctions/IAppFunctionManager.aidl new file mode 100644 index 000000000000..018bc758f69f --- /dev/null +++ b/core/java/android/app/appfunctions/IAppFunctionManager.aidl @@ -0,0 +1,24 @@ +/* + * 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 android.app.appfunctions; + +/** +* Interface between an app and the server implementation service (AppFunctionManagerService). +* @hide +*/ +oneway interface IAppFunctionManager { +}
\ No newline at end of file diff --git a/core/java/android/app/appfunctions/OWNERS b/core/java/android/app/appfunctions/OWNERS new file mode 100644 index 000000000000..c6827cc93222 --- /dev/null +++ b/core/java/android/app/appfunctions/OWNERS @@ -0,0 +1,6 @@ +avayvod@google.com +oadesina@google.com +toki@google.com +tonymak@google.com +mingweiliao@google.com +anothermark@google.com diff --git a/core/java/android/app/appfunctions/flags/flags.aconfig b/core/java/android/app/appfunctions/flags/flags.aconfig new file mode 100644 index 000000000000..367effc9e9bb --- /dev/null +++ b/core/java/android/app/appfunctions/flags/flags.aconfig @@ -0,0 +1,11 @@ +package: "android.app.appfunctions.flags" +container: "system" + +flag { + name: "enable_app_function_manager" + is_exported: true + is_fixed_read_only: true + namespace: "machine_learning" + description: "This flag the new App Function manager system service." + bug: "357551503" +}
\ No newline at end of file diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 8365840b1efb..9dccc9ae7145 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -16,6 +16,7 @@ package android.content; +import static android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER; import static android.content.flags.Flags.FLAG_ENABLE_BIND_PACKAGE_ISOLATED_PROCESS; import android.annotation.AttrRes; @@ -51,6 +52,7 @@ import android.app.IApplicationThread; import android.app.IServiceConnection; import android.app.VrManager; import android.app.ambientcontext.AmbientContextManager; +import android.app.appfunctions.AppFunctionManager; import android.app.people.PeopleManager; import android.app.time.TimeManager; import android.companion.virtual.VirtualDeviceManager; @@ -6310,6 +6312,16 @@ public abstract class Context { /** * Use with {@link #getSystemService(String)} to retrieve an + * {@link AppFunctionManager} for + * executing app functions. + * + * @see #getSystemService(String) + */ + @FlaggedApi(FLAG_ENABLE_APP_FUNCTION_MANAGER) + public static final String APP_FUNCTION_SERVICE = "app_function"; + + /** + * Use with {@link #getSystemService(String)} to retrieve an * {@link android.content.integrity.AppIntegrityManager}. * @hide */ @@ -6676,6 +6688,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/TEST_MAPPING b/core/java/android/content/pm/TEST_MAPPING index b0ab11f48858..1fab3cffcebd 100644 --- a/core/java/android/content/pm/TEST_MAPPING +++ b/core/java/android/content/pm/TEST_MAPPING @@ -171,6 +171,17 @@ "include-filter": "android.content.pm.cts.PackageManagerShellCommandMultiUserTest" } ] + }, + { + "name":"CtsPackageInstallerCUJTestCases", + "options":[ + { + "exclude-annotation":"androidx.test.filters.FlakyTest" + }, + { + "exclude-annotation":"org.junit.Ignore" + } + ] } ] } diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig index e370e85278d5..273519b75f93 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" @@ -278,6 +288,13 @@ flag { } flag { + name: "stop_previous_user_apps" + namespace: "multiuser" + description: "Stop the previous user apps early in a user switch" + bug: "323200731" +} + +flag { name: "disable_private_space_items_on_home" namespace: "profile_experiences" description: "Disables adding items belonging to Private Space on Home Screen manually as well as automatically" @@ -386,4 +403,7 @@ flag { description: "Refactorings related to unicorn mode to work on HSUM mode (Read only flag)" bug: "339201286" is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } }
\ No newline at end of file diff --git a/core/java/android/hardware/biometrics/BiometricManager.java b/core/java/android/hardware/biometrics/BiometricManager.java index 678bd6bc6336..de1cac47ff46 100644 --- a/core/java/android/hardware/biometrics/BiometricManager.java +++ b/core/java/android/hardware/biometrics/BiometricManager.java @@ -415,7 +415,7 @@ public class BiometricManager { @RequiresPermission(TEST_BIOMETRIC) public BiometricTestSession createTestSession(int sensorId) { try { - return new BiometricTestSession(mContext, sensorId, + return new BiometricTestSession(mContext, getSensorProperties(), sensorId, (context, sensorId1, callback) -> mService .createTestSession(sensorId1, callback, context.getOpPackageName())); } catch (RemoteException e) { diff --git a/core/java/android/hardware/biometrics/BiometricPrompt.java b/core/java/android/hardware/biometrics/BiometricPrompt.java index 9007b62bccfc..b11961cc2b21 100644 --- a/core/java/android/hardware/biometrics/BiometricPrompt.java +++ b/core/java/android/hardware/biometrics/BiometricPrompt.java @@ -638,17 +638,17 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan * Set caller's component name for getting logo icon/description. This should only be used * by ConfirmDeviceCredentialActivity, see b/337082634 for more context. * - * @param componentNameForConfirmDeviceCredentialActivity set the component name for - * ConfirmDeviceCredentialActivity. + * @param realCaller set the component name of real caller for + * ConfirmDeviceCredentialActivity. * @return This builder. * @hide */ @NonNull @RequiresPermission(anyOf = {TEST_BIOMETRIC, USE_BIOMETRIC_INTERNAL}) - public Builder setComponentNameForConfirmDeviceCredentialActivity( - ComponentName componentNameForConfirmDeviceCredentialActivity) { - mPromptInfo.setComponentNameForConfirmDeviceCredentialActivity( - componentNameForConfirmDeviceCredentialActivity); + public Builder setRealCallerForConfirmDeviceCredentialActivity(ComponentName realCaller) { + mPromptInfo.setRealCallerForConfirmDeviceCredentialActivity(realCaller); + mPromptInfo.setClassNameIfItIsConfirmDeviceCredentialActivity( + mContext.getClass().getName()); return this; } diff --git a/core/java/android/hardware/biometrics/BiometricTestSession.java b/core/java/android/hardware/biometrics/BiometricTestSession.java index 027d1015a4b5..8bd352888de1 100644 --- a/core/java/android/hardware/biometrics/BiometricTestSession.java +++ b/core/java/android/hardware/biometrics/BiometricTestSession.java @@ -27,12 +27,15 @@ import android.os.RemoteException; import android.util.ArraySet; import android.util.Log; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; /** * Common set of interfaces to test biometric-related APIs, including {@link BiometricPrompt} and * {@link android.hardware.fingerprint.FingerprintManager}. + * * @hide */ @TestApi @@ -48,21 +51,29 @@ public class BiometricTestSession implements AutoCloseable { @NonNull ITestSessionCallback callback) throws RemoteException; } - private final Context mContext; private final int mSensorId; - private final ITestSession mTestSession; + private final List<ITestSession> mTestSessionsForAllSensors = new ArrayList<>(); + private ITestSession mTestSession; // Keep track of users that were tested, which need to be cleaned up when finishing. - @NonNull private final ArraySet<Integer> mTestedUsers; + @NonNull + private final ArraySet<Integer> mTestedUsers; // Track the users currently cleaning up, and provide a latch that gets notified when all // users have finished cleaning up. This is an imperfect system, as there can technically be // multiple cleanups per user. Theoretically we should track the cleanup's BaseClientMonitor's // unique ID, but it's complicated to plumb it through. This should be fine for now. - @Nullable private CountDownLatch mCloseLatch; - @NonNull private final ArraySet<Integer> mUsersCleaningUp; + @Nullable + private CountDownLatch mCloseLatch; + @NonNull + private final ArraySet<Integer> mUsersCleaningUp; + + private class TestSessionCallbackIml extends ITestSessionCallback.Stub { + private final int mSensorId; + private TestSessionCallbackIml(int sensorId) { + mSensorId = sensorId; + } - private final ITestSessionCallback mCallback = new ITestSessionCallback.Stub() { @Override public void onCleanupStarted(int userId) { Log.d(getTag(), "onCleanupStarted, sensor: " + mSensorId + ", userId: " + userId); @@ -76,19 +87,30 @@ public class BiometricTestSession implements AutoCloseable { mUsersCleaningUp.remove(userId); if (mUsersCleaningUp.isEmpty() && mCloseLatch != null) { + Log.d(getTag(), "counting down"); mCloseLatch.countDown(); } } - }; + } /** * @hide */ - public BiometricTestSession(@NonNull Context context, int sensorId, - @NonNull TestSessionProvider testSessionProvider) throws RemoteException { - mContext = context; + public BiometricTestSession(@NonNull Context context, List<SensorProperties> sensors, + int sensorId, @NonNull TestSessionProvider testSessionProvider) throws RemoteException { mSensorId = sensorId; - mTestSession = testSessionProvider.createTestSession(context, sensorId, mCallback); + // When any of the sensors should create the test session, all the other sensors should + // set test hal enabled too. + for (SensorProperties sensor : sensors) { + final int id = sensor.getSensorId(); + final ITestSession session = testSessionProvider.createTestSession(context, id, + new TestSessionCallbackIml(id)); + mTestSessionsForAllSensors.add(session); + if (id == sensorId) { + mTestSession = session; + } + } + mTestedUsers = new ArraySet<>(); mUsersCleaningUp = new ArraySet<>(); setTestHalEnabled(true); @@ -107,8 +129,11 @@ public class BiometricTestSession implements AutoCloseable { @RequiresPermission(TEST_BIOMETRIC) private void setTestHalEnabled(boolean enabled) { try { - Log.w(getTag(), "setTestHalEnabled, sensor: " + mSensorId + " enabled: " + enabled); - mTestSession.setTestHalEnabled(enabled); + for (ITestSession session : mTestSessionsForAllSensors) { + Log.w(getTag(), "setTestHalEnabled, sensor: " + session.getSensorId() + " enabled: " + + enabled); + session.setTestHalEnabled(enabled); + } } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -175,10 +200,12 @@ public class BiometricTestSession implements AutoCloseable { /** * Simulates an acquired message from the HAL. * - * @param userId User that this command applies to. + * @param userId User that this command applies to. * @param acquireInfo See - * {@link BiometricPrompt.AuthenticationCallback#onAuthenticationAcquired(int)} and - * {@link FingerprintManager.AuthenticationCallback#onAuthenticationAcquired(int)} + * {@link + * BiometricPrompt.AuthenticationCallback#onAuthenticationAcquired(int)} and + * {@link + * FingerprintManager.AuthenticationCallback#onAuthenticationAcquired(int)} */ @RequiresPermission(TEST_BIOMETRIC) public void notifyAcquired(int userId, int acquireInfo) { @@ -192,10 +219,12 @@ public class BiometricTestSession implements AutoCloseable { /** * Simulates an error message from the HAL. * - * @param userId User that this command applies to. + * @param userId User that this command applies to. * @param errorCode See - * {@link BiometricPrompt.AuthenticationCallback#onAuthenticationError(int, CharSequence)} and - * {@link FingerprintManager.AuthenticationCallback#onAuthenticationError(int, CharSequence)} + * {@link BiometricPrompt.AuthenticationCallback#onAuthenticationError(int, + * CharSequence)} and + * {@link FingerprintManager.AuthenticationCallback#onAuthenticationError(int, + * CharSequence)} */ @RequiresPermission(TEST_BIOMETRIC) public void notifyError(int userId, int errorCode) { @@ -220,8 +249,20 @@ public class BiometricTestSession implements AutoCloseable { Log.w(getTag(), "Cleanup already in progress for user: " + userId); } - mUsersCleaningUp.add(userId); - mTestSession.cleanupInternalState(userId); + for (ITestSession session : mTestSessionsForAllSensors) { + mUsersCleaningUp.add(userId); + Log.d(getTag(), "cleanupInternalState for sensor: " + session.getSensorId()); + mCloseLatch = new CountDownLatch(1); + session.cleanupInternalState(userId); + + try { + Log.d(getTag(), "Awaiting latch..."); + mCloseLatch.await(3, TimeUnit.SECONDS); + Log.d(getTag(), "Finished awaiting"); + } catch (InterruptedException e) { + Log.e(getTag(), "Latch interrupted", e); + } + } } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -234,18 +275,9 @@ public class BiometricTestSession implements AutoCloseable { // Cleanup can be performed using the test HAL, since it always responds to enumerate with // zero enrollments. if (!mTestedUsers.isEmpty()) { - mCloseLatch = new CountDownLatch(1); for (int user : mTestedUsers) { cleanupInternalState(user); } - - try { - Log.d(getTag(), "Awaiting latch..."); - mCloseLatch.await(3, TimeUnit.SECONDS); - Log.d(getTag(), "Finished awaiting"); - } catch (InterruptedException e) { - Log.e(getTag(), "Latch interrupted", e); - } } if (!mUsersCleaningUp.isEmpty()) { diff --git a/core/java/android/hardware/biometrics/ITestSession.aidl b/core/java/android/hardware/biometrics/ITestSession.aidl index df9f504a2c05..bd99606808b7 100644 --- a/core/java/android/hardware/biometrics/ITestSession.aidl +++ b/core/java/android/hardware/biometrics/ITestSession.aidl @@ -59,4 +59,8 @@ interface ITestSession { // HAL is disabled (e.g. to clean up after a test). @EnforcePermission("TEST_BIOMETRIC") void cleanupInternalState(int userId); + + // Get the sensor id of the current test session. + @EnforcePermission("TEST_BIOMETRIC") + int getSensorId(); } diff --git a/core/java/android/hardware/biometrics/PromptInfo.java b/core/java/android/hardware/biometrics/PromptInfo.java index 901f6b7ba5c1..df5d864196b4 100644 --- a/core/java/android/hardware/biometrics/PromptInfo.java +++ b/core/java/android/hardware/biometrics/PromptInfo.java @@ -57,7 +57,8 @@ public class PromptInfo implements Parcelable { private boolean mIsForLegacyFingerprintManager = false; private boolean mShowEmergencyCallButton = false; private boolean mUseParentProfileForDeviceCredential = false; - private ComponentName mComponentNameForConfirmDeviceCredentialActivity = null; + private ComponentName mRealCallerForConfirmDeviceCredentialActivity = null; + private String mClassNameIfItIsConfirmDeviceCredentialActivity = null; public PromptInfo() { @@ -89,8 +90,9 @@ public class PromptInfo implements Parcelable { mIsForLegacyFingerprintManager = in.readBoolean(); mShowEmergencyCallButton = in.readBoolean(); mUseParentProfileForDeviceCredential = in.readBoolean(); - mComponentNameForConfirmDeviceCredentialActivity = in.readParcelable( + mRealCallerForConfirmDeviceCredentialActivity = in.readParcelable( ComponentName.class.getClassLoader(), ComponentName.class); + mClassNameIfItIsConfirmDeviceCredentialActivity = in.readString(); } public static final Creator<PromptInfo> CREATOR = new Creator<PromptInfo>() { @@ -136,7 +138,8 @@ public class PromptInfo implements Parcelable { dest.writeBoolean(mIsForLegacyFingerprintManager); dest.writeBoolean(mShowEmergencyCallButton); dest.writeBoolean(mUseParentProfileForDeviceCredential); - dest.writeParcelable(mComponentNameForConfirmDeviceCredentialActivity, 0); + dest.writeParcelable(mRealCallerForConfirmDeviceCredentialActivity, 0); + dest.writeString(mClassNameIfItIsConfirmDeviceCredentialActivity); } // LINT.IfChange @@ -155,7 +158,7 @@ public class PromptInfo implements Parcelable { return true; } else if (mShowEmergencyCallButton) { return true; - } else if (mComponentNameForConfirmDeviceCredentialActivity != null) { + } else if (mRealCallerForConfirmDeviceCredentialActivity != null) { return true; } return false; @@ -321,10 +324,8 @@ public class PromptInfo implements Parcelable { mShowEmergencyCallButton = showEmergencyCallButton; } - public void setComponentNameForConfirmDeviceCredentialActivity( - ComponentName componentNameForConfirmDeviceCredentialActivity) { - mComponentNameForConfirmDeviceCredentialActivity = - componentNameForConfirmDeviceCredentialActivity; + public void setRealCallerForConfirmDeviceCredentialActivity(ComponentName realCaller) { + mRealCallerForConfirmDeviceCredentialActivity = realCaller; } public void setUseParentProfileForDeviceCredential( @@ -332,6 +333,14 @@ public class PromptInfo implements Parcelable { mUseParentProfileForDeviceCredential = useParentProfileForDeviceCredential; } + /** + * Set the class name of ConfirmDeviceCredentialActivity. + */ + void setClassNameIfItIsConfirmDeviceCredentialActivity(String className) { + mClassNameIfItIsConfirmDeviceCredentialActivity = className; + } + + // Getters /** @@ -455,8 +464,15 @@ public class PromptInfo implements Parcelable { return mShowEmergencyCallButton; } - public ComponentName getComponentNameForConfirmDeviceCredentialActivity() { - return mComponentNameForConfirmDeviceCredentialActivity; + public ComponentName getRealCallerForConfirmDeviceCredentialActivity() { + return mRealCallerForConfirmDeviceCredentialActivity; } + /** + * Get the class name of ConfirmDeviceCredentialActivity. Returns null if the direct caller is + * not ConfirmDeviceCredentialActivity. + */ + public String getClassNameIfItIsConfirmDeviceCredentialActivity() { + return mClassNameIfItIsConfirmDeviceCredentialActivity; + } } diff --git a/core/java/android/hardware/camera2/params/SessionConfiguration.java b/core/java/android/hardware/camera2/params/SessionConfiguration.java index 0c55ed5323a0..9bd4860e7ccc 100644 --- a/core/java/android/hardware/camera2/params/SessionConfiguration.java +++ b/core/java/android/hardware/camera2/params/SessionConfiguration.java @@ -17,8 +17,6 @@ package android.hardware.camera2.params; -import static com.android.internal.util.Preconditions.*; - import android.annotation.CallbackExecutor; import android.annotation.FlaggedApi; import android.annotation.IntDef; @@ -32,8 +30,6 @@ import android.hardware.camera2.CameraDevice; import android.hardware.camera2.CameraDevice.CameraDeviceSetup; import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.impl.CameraMetadataNative; -import android.hardware.camera2.params.InputConfiguration; -import android.hardware.camera2.params.OutputConfiguration; import android.hardware.camera2.utils.HashCodeHelpers; import android.media.ImageReader; import android.os.Parcel; @@ -46,6 +42,7 @@ import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.concurrent.Executor; /** @@ -95,8 +92,8 @@ public final class SessionConfiguration implements Parcelable { public @interface SessionMode {}; // Camera capture session related parameters. - private List<OutputConfiguration> mOutputConfigurations; - private CameraCaptureSession.StateCallback mStateCallback; + private final @NonNull List<OutputConfiguration> mOutputConfigurations; + private CameraCaptureSession.StateCallback mStateCallback = null; private int mSessionType; private Executor mExecutor = null; private InputConfiguration mInputConfig = null; @@ -268,7 +265,8 @@ public final class SessionConfiguration implements Parcelable { */ @Override public int hashCode() { - return HashCodeHelpers.hashCode(mOutputConfigurations.hashCode(), mInputConfig.hashCode(), + return HashCodeHelpers.hashCode(mOutputConfigurations.hashCode(), + Objects.hashCode(mInputConfig), mSessionType); } diff --git a/core/java/android/hardware/fingerprint/FingerprintManager.java b/core/java/android/hardware/fingerprint/FingerprintManager.java index 903e91646332..7f1cac08b430 100644 --- a/core/java/android/hardware/fingerprint/FingerprintManager.java +++ b/core/java/android/hardware/fingerprint/FingerprintManager.java @@ -172,7 +172,7 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing @RequiresPermission(TEST_BIOMETRIC) public BiometricTestSession createTestSession(int sensorId) { try { - return new BiometricTestSession(mContext, sensorId, + return new BiometricTestSession(mContext, getSensorProperties(), sensorId, (context, sensorId1, callback) -> mService .createTestSession(sensorId1, callback, context.getOpPackageName())); } catch (RemoteException e) { 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/os/ExternalVibrationScale.aidl b/core/java/android/os/ExternalVibrationScale.aidl index cf6f8ed52f7d..644beced2091 100644 --- a/core/java/android/os/ExternalVibrationScale.aidl +++ b/core/java/android/os/ExternalVibrationScale.aidl @@ -33,12 +33,24 @@ parcelable ExternalVibrationScale { SCALE_VERY_HIGH = 2 } + // TODO(b/345186129): remove this once we finish migrating to scale factor. /** * The scale level that will be applied to external vibrations. */ ScaleLevel scaleLevel = ScaleLevel.SCALE_NONE; /** + * The scale factor that will be applied to external vibrations. + * + * Values in (0,1) will scale down the vibrations, values > 1 will scale up vibrations within + * hardware limits. A zero scale factor indicates the external vibration should be muted. + * + * TODO(b/345186129): update this once we finish migrating, negative should not be expected. + * Negative values should be ignored in favour of the legacy ScaleLevel. + */ + float scaleFactor = -1f; // undefined + + /** * The adaptive haptics scale that will be applied to external vibrations. */ float adaptiveHapticsScale = 1f; diff --git a/core/java/android/os/IVibratorManagerService.aidl b/core/java/android/os/IVibratorManagerService.aidl index 97993b609fda..6aa9852314df 100644 --- a/core/java/android/os/IVibratorManagerService.aidl +++ b/core/java/android/os/IVibratorManagerService.aidl @@ -42,4 +42,12 @@ interface IVibratorManagerService { // vibrate/isVibrating/cancel. oneway void performHapticFeedback(int uid, int deviceId, String opPkg, int constant, String reason, int flags, int privFlags); + + // Similar to performHapticFeedback but the effect is customized to the input device. The + // customization for each constant is defined on a device basis, and the behavior will be the + // same as performHapticFeedback when no customization is provided for a given constant and + // device. + oneway void performHapticFeedbackForInputDevice(int uid, int deviceId, String opPkg, + int constant, int inputDeviceId, int inputSource, String reason, int flags, + int privFlags); } diff --git a/core/java/android/os/SystemVibrator.java b/core/java/android/os/SystemVibrator.java index 5339d7331426..011a3ee91ada 100644 --- a/core/java/android/os/SystemVibrator.java +++ b/core/java/android/os/SystemVibrator.java @@ -215,6 +215,17 @@ public class SystemVibrator extends Vibrator { } @Override + public void performHapticFeedbackForInputDevice(int constant, int inputDeviceId, + int inputSource, String reason, int flags, int privFlags) { + if (mVibratorManager == null) { + Log.w(TAG, "Failed to perform haptic feedback for input device; no vibrator manager."); + return; + } + mVibratorManager.performHapticFeedbackForInputDevice(constant, inputDeviceId, inputSource, + reason, flags, privFlags); + } + + @Override public void cancel() { if (mVibratorManager == null) { Log.w(TAG, "Failed to cancel vibrate; no vibrator manager."); diff --git a/core/java/android/os/SystemVibratorManager.java b/core/java/android/os/SystemVibratorManager.java index a9846ba7e264..58ab5b6fd7ca 100644 --- a/core/java/android/os/SystemVibratorManager.java +++ b/core/java/android/os/SystemVibratorManager.java @@ -161,6 +161,22 @@ public class SystemVibratorManager extends VibratorManager { } @Override + public void performHapticFeedbackForInputDevice(int constant, int inputDeviceId, + int inputSource, String reason, int flags, int privFlags) { + if (mService == null) { + Log.w(TAG, "Failed to perform haptic feedback for input device;" + + " no vibrator manager service."); + return; + } + try { + mService.performHapticFeedbackForInputDevice(mUid, mContext.getDeviceId(), mPackageName, + constant, inputDeviceId, inputSource, reason, flags, privFlags); + } catch (RemoteException e) { + Log.w(TAG, "Failed to perform haptic feedback for input device.", e); + } + } + + @Override public void cancel() { cancelVibration(VibrationAttributes.USAGE_FILTER_MATCH_ALL); } diff --git a/core/java/android/os/Trace.java b/core/java/android/os/Trace.java index edb3a641f107..4a37e0a70443 100644 --- a/core/java/android/os/Trace.java +++ b/core/java/android/os/Trace.java @@ -520,8 +520,20 @@ public final class Trace { * @param counterValue The counter value. */ public static void setCounter(@NonNull String counterName, long counterValue) { - if (isTagEnabled(TRACE_TAG_APP)) { - nativeTraceCounter(TRACE_TAG_APP, counterName, counterValue); + setCounter(TRACE_TAG_APP, counterName, counterValue); + } + + /** + * Writes trace message to indicate the value of a given counter under a given trace tag. + * + * @param traceTag The trace tag. + * @param counterName The counter name to appear in the trace. + * @param counterValue The counter value. + * @hide + */ + public static void setCounter(long traceTag, @NonNull String counterName, long counterValue) { + if (isTagEnabled(traceTag)) { + nativeTraceCounter(traceTag, counterName, counterValue); } } diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java index 06c516aee8f3..28f2c2530ae9 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -4824,6 +4824,7 @@ public class UserManager { * <p>Note that this does not alter the user's pre-existing user restrictions. * * @param userId the id of the user to become admin + * @throws SecurityException if changing ADMIN status of the user is not allowed * @hide */ @RequiresPermission(allOf = { @@ -4844,6 +4845,7 @@ public class UserManager { * <p>Note that this does not alter the user's pre-existing user restrictions. * * @param userId the id of the user to revoke admin rights from + * @throws SecurityException if changing ADMIN status of the user is not allowed * @hide */ @RequiresPermission(allOf = { diff --git a/core/java/android/os/VibrationEffect.java b/core/java/android/os/VibrationEffect.java index f3ef9e15b8f0..e68b74683292 100644 --- a/core/java/android/os/VibrationEffect.java +++ b/core/java/android/os/VibrationEffect.java @@ -663,6 +663,15 @@ public abstract class VibrationEffect implements Parcelable { * @hide */ public static float scale(float intensity, float scaleFactor) { + if (Flags.hapticsScaleV2Enabled()) { + if (Float.compare(scaleFactor, 1) <= 0 || Float.compare(intensity, 0) == 0) { + // Scaling down or scaling zero intensity is straightforward. + return scaleFactor * intensity; + } + // Using S * x / (1 + (S - 1) * x^2) as the scale up function to converge to 1.0. + return (scaleFactor * intensity) / (1 + (scaleFactor - 1) * intensity * intensity); + } + // Applying gamma correction to the scale factor, which is the same as encoding the input // value, scaling it, then decoding the scaled value. float scale = MathUtils.pow(scaleFactor, 1f / SCALE_GAMMA); diff --git a/core/java/android/os/Vibrator.java b/core/java/android/os/Vibrator.java index 161cce0293e7..36233b7be2bb 100644 --- a/core/java/android/os/Vibrator.java +++ b/core/java/android/os/Vibrator.java @@ -553,6 +553,31 @@ public abstract class Vibrator { } /** + * Performs a haptic feedback. Similar to {@link #performHapticFeedback} but also take into the + * consideration the {@link InputDevice} that triggered the haptic + * + * <p>A haptic feedback is a short vibration feedback. The type of feedback is identified via + * the {@code constant}, which should be one of the effect constants provided in + * {@link HapticFeedbackConstants}. The haptic feedback provided for a given effect ID is + * consistent across all usages on the same device. + * + * @param constant the ID for the haptic feedback. This should be one of the constants + * defined in {@link HapticFeedbackConstants}. + * @param inputDeviceId the integer id of the input device that triggered the haptic feedback. + * @param inputSource the {@link InputDevice.Source} that triggered the haptic feedback. + * @param reason the reason for this haptic feedback. + * @param flags Additional flags as per {@link HapticFeedbackConstants}. + * @param privFlags Additional private flags as per {@link HapticFeedbackConstants}. + * @hide + */ + public void performHapticFeedbackForInputDevice( + int constant, int inputDeviceId, int inputSource, String reason, + @HapticFeedbackConstants.Flags int flags, + @HapticFeedbackConstants.PrivateFlags int privFlags) { + Log.w(TAG, "performHapticFeedbackForInputDevice is not supported"); + } + + /** * Query whether the vibrator natively supports the given effects. * * <p>If an effect is not supported, the system may still automatically fall back to playing diff --git a/core/java/android/os/VibratorManager.java b/core/java/android/os/VibratorManager.java index 2c7a852cf29f..0428876891f9 100644 --- a/core/java/android/os/VibratorManager.java +++ b/core/java/android/os/VibratorManager.java @@ -155,6 +155,27 @@ public abstract class VibratorManager { } /** + * Performs a haptic feedback. Similar to {@link #performHapticFeedback} but also take input + * into consideration. + * + * @param constant the ID of the requested haptic feedback. Should be one of the constants + * defined in {@link HapticFeedbackConstants}. + * @param inputDeviceId the integer id of the input device that customizes the haptic feedback + * corresponding to the {@code constant}. + * @param inputSource the {@link InputDevice.Source} that customizes the haptic feedback + * corresponding to the {@code constant}. + * @param reason the reason for this haptic feedback. + * @param flags Additional flags as per {@link HapticFeedbackConstants}. + * @param privFlags Additional private flags as per {@link HapticFeedbackConstants}. + * @hide + */ + public void performHapticFeedbackForInputDevice(int constant, int inputDeviceId, + int inputSource, String reason, @HapticFeedbackConstants.Flags int flags, + @HapticFeedbackConstants.PrivateFlags int privFlags) { + Log.w(TAG, "performHapticFeedbackForInputDevice is not supported"); + } + + /** * Turn all the vibrators off. */ @RequiresPermission(android.Manifest.permission.VIBRATE) diff --git a/core/java/android/os/vibrator/VibrationConfig.java b/core/java/android/os/vibrator/VibrationConfig.java index a4164e9f204c..e6e5a27bd731 100644 --- a/core/java/android/os/vibrator/VibrationConfig.java +++ b/core/java/android/os/vibrator/VibrationConfig.java @@ -49,8 +49,22 @@ import java.util.Arrays; */ public class VibrationConfig { + /** + * Hardcoded default scale level gain to be applied between each scale level to define their + * scale factor value. + * + * <p>Default gain defined as 3 dBs. + */ + private static final float DEFAULT_SCALE_LEVEL_GAIN = 1.4f; + + /** + * Hardcoded default amplitude to be used when device config is invalid, i.e. not in [1,255]. + */ + private static final int DEFAULT_AMPLITUDE = 255; + // TODO(b/191150049): move these to vibrator static config file private final float mHapticChannelMaxVibrationAmplitude; + private final int mDefaultVibrationAmplitude; private final int mRampStepDurationMs; private final int mRampDownDurationMs; private final int mRequestVibrationParamsTimeoutMs; @@ -75,8 +89,10 @@ public class VibrationConfig { /** @hide */ public VibrationConfig(@Nullable Resources resources) { + mDefaultVibrationAmplitude = resources.getInteger( + com.android.internal.R.integer.config_defaultVibrationAmplitude); mHapticChannelMaxVibrationAmplitude = loadFloat(resources, - com.android.internal.R.dimen.config_hapticChannelMaxVibrationAmplitude, 0); + com.android.internal.R.dimen.config_hapticChannelMaxVibrationAmplitude); mRampDownDurationMs = loadInteger(resources, com.android.internal.R.integer.config_vibrationWaveformRampDownDuration, 0); mRampStepDurationMs = loadInteger(resources, @@ -87,9 +103,9 @@ public class VibrationConfig { com.android.internal.R.array.config_requestVibrationParamsForUsages); mIgnoreVibrationsOnWirelessCharger = loadBoolean(resources, - com.android.internal.R.bool.config_ignoreVibrationsOnWirelessCharger, false); + com.android.internal.R.bool.config_ignoreVibrationsOnWirelessCharger); mKeyboardVibrationSettingsSupported = loadBoolean(resources, - com.android.internal.R.bool.config_keyboardVibrationSettingsSupported, false); + com.android.internal.R.bool.config_keyboardVibrationSettingsSupported); mDefaultAlarmVibrationIntensity = loadDefaultIntensity(resources, com.android.internal.R.integer.config_defaultAlarmVibrationIntensity); @@ -115,16 +131,16 @@ public class VibrationConfig { return value; } - private static float loadFloat(@Nullable Resources res, int resId, float defaultValue) { - return res != null ? res.getFloat(resId) : defaultValue; + private static float loadFloat(@Nullable Resources res, int resId) { + return res != null ? res.getFloat(resId) : 0f; } private static int loadInteger(@Nullable Resources res, int resId, int defaultValue) { return res != null ? res.getInteger(resId) : defaultValue; } - private static boolean loadBoolean(@Nullable Resources res, int resId, boolean defaultValue) { - return res != null ? res.getBoolean(resId) : defaultValue; + private static boolean loadBoolean(@Nullable Resources res, int resId) { + return res != null && res.getBoolean(resId); } private static int[] loadIntArray(@Nullable Resources res, int resId) { @@ -145,6 +161,26 @@ public class VibrationConfig { } /** + * Return the device default vibration amplitude value to replace the + * {@link android.os.VibrationEffect#DEFAULT_AMPLITUDE} constant. + */ + public int getDefaultVibrationAmplitude() { + if (mDefaultVibrationAmplitude < 1 || mDefaultVibrationAmplitude > 255) { + return DEFAULT_AMPLITUDE; + } + return mDefaultVibrationAmplitude; + } + + /** + * Return the device default gain to be applied between scale levels to define the scale factor + * for each level. + */ + public float getDefaultVibrationScaleLevelGain() { + // TODO(b/356407380): add device config for this + return DEFAULT_SCALE_LEVEL_GAIN; + } + + /** * The duration, in milliseconds, that should be applied to the ramp to turn off the vibrator * when a vibration is cancelled or finished at non-zero amplitude. */ @@ -233,6 +269,7 @@ public class VibrationConfig { public String toString() { return "VibrationConfig{" + "mIgnoreVibrationsOnWirelessCharger=" + mIgnoreVibrationsOnWirelessCharger + + ", mDefaultVibrationAmplitude=" + mDefaultVibrationAmplitude + ", mHapticChannelMaxVibrationAmplitude=" + mHapticChannelMaxVibrationAmplitude + ", mRampStepDurationMs=" + mRampStepDurationMs + ", mRampDownDurationMs=" + mRampDownDurationMs @@ -258,6 +295,7 @@ public class VibrationConfig { pw.println("VibrationConfig:"); pw.increaseIndent(); pw.println("ignoreVibrationsOnWirelessCharger = " + mIgnoreVibrationsOnWirelessCharger); + pw.println("defaultVibrationAmplitude = " + mDefaultVibrationAmplitude); pw.println("hapticChannelMaxAmplitude = " + mHapticChannelMaxVibrationAmplitude); pw.println("rampStepDurationMs = " + mRampStepDurationMs); pw.println("rampDownDurationMs = " + mRampDownDurationMs); diff --git a/core/java/android/os/vibrator/flags.aconfig b/core/java/android/os/vibrator/flags.aconfig index 3fad8dd1da9c..53a1a67dfc58 100644 --- a/core/java/android/os/vibrator/flags.aconfig +++ b/core/java/android/os/vibrator/flags.aconfig @@ -3,13 +3,6 @@ container: "system" flag { namespace: "haptics" - name: "use_vibrator_haptic_feedback" - description: "Enables performHapticFeedback to directly use the vibrator service instead of going through the window session" - bug: "295459081" -} - -flag { - namespace: "haptics" name: "haptic_feedback_vibration_oem_customization_enabled" description: "Enables OEMs/devices to customize vibrations for haptic feedback" # Make read only. This is because the flag is used only once, and this could happen before @@ -88,3 +81,35 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + namespace: "haptics" + name: "load_haptic_feedback_vibration_customization_from_resources" + description: "Load haptic feedback vibrations customization from resources." + is_fixed_read_only: true + bug: "295142743" + metadata { + purpose: PURPOSE_FEATURE + } +} + +flag { + namespace: "haptics" + name: "haptic_feedback_input_source_customization_enabled" + description: "Enabled the extended haptic feedback customization by input source." + bug: "331819348" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_FEATURE + } +} + +flag { + namespace: "haptics" + name: "haptics_scale_v2_enabled" + description: "Enables new haptics scaling function across all usages" + bug: "345186129" + metadata { + purpose: PURPOSE_FEATURE + } +} diff --git a/core/java/android/os/vibrator/persistence/VibrationXmlSerializer.java b/core/java/android/os/vibrator/persistence/VibrationXmlSerializer.java index a26c6f434e15..a95ce7914d8b 100644 --- a/core/java/android/os/vibrator/persistence/VibrationXmlSerializer.java +++ b/core/java/android/os/vibrator/persistence/VibrationXmlSerializer.java @@ -104,7 +104,7 @@ public final class VibrationXmlSerializer { public static void serialize(@NonNull VibrationEffect effect, @NonNull Writer writer, @Flags int flags) throws IOException { // Serialize effect first to fail early. - XmlSerializedVibration<VibrationEffect> serializedVibration = + XmlSerializedVibration<? extends VibrationEffect> serializedVibration = toSerializedVibration(effect, flags); TypedXmlSerializer xmlSerializer = Xml.newFastSerializer(); xmlSerializer.setFeature(XML_FEATURE_INDENT_OUTPUT, (flags & FLAG_PRETTY_PRINT) != 0); @@ -114,9 +114,9 @@ public final class VibrationXmlSerializer { xmlSerializer.endDocument(); } - private static XmlSerializedVibration<VibrationEffect> toSerializedVibration( + private static XmlSerializedVibration<? extends VibrationEffect> toSerializedVibration( VibrationEffect effect, @Flags int flags) throws SerializationFailedException { - XmlSerializedVibration<VibrationEffect> serializedVibration; + XmlSerializedVibration<? extends VibrationEffect> serializedVibration; int serializerFlags = 0; if ((flags & FLAG_ALLOW_HIDDEN_APIS) != 0) { serializerFlags |= XmlConstants.FLAG_ALLOW_HIDDEN_APIS; 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/security/net/config/SystemCertificateSource.java b/core/java/android/security/net/config/SystemCertificateSource.java index 3a254c1d92fc..bdda42a389eb 100644 --- a/core/java/android/security/net/config/SystemCertificateSource.java +++ b/core/java/android/security/net/config/SystemCertificateSource.java @@ -19,6 +19,8 @@ package android.security.net.config; import android.os.Environment; import android.os.UserHandle; +import com.android.internal.util.ArrayUtils; + import java.io.File; /** @@ -45,7 +47,7 @@ public final class SystemCertificateSource extends DirectoryCertificateSource { } File updatable_dir = new File("/apex/com.android.conscrypt/cacerts"); if (updatable_dir.exists() - && !(updatable_dir.list().length == 0)) { + && !(ArrayUtils.isEmpty(updatable_dir.list()))) { return updatable_dir; } return new File(System.getenv("ANDROID_ROOT") + "/etc/security/cacerts"); 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/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java index 5e15e0160be4..fc6c2e88779f 100644 --- a/core/java/android/service/notification/ZenModeConfig.java +++ b/core/java/android/service/notification/ZenModeConfig.java @@ -25,6 +25,7 @@ import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_LIGHTS; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_SCREEN_OFF; import static android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; +import static android.service.notification.SystemZenRules.PACKAGE_ANDROID; import static android.service.notification.ZenAdapters.peopleTypeToPrioritySenders; import static android.service.notification.ZenAdapters.prioritySendersToPeopleType; import static android.service.notification.ZenAdapters.zenPolicyConversationSendersToNotificationPolicy; @@ -74,8 +75,9 @@ import android.util.PluralsMessageFormatter; import android.util.Slog; import android.util.proto.ProtoOutputStream; +import androidx.annotation.VisibleForTesting; + import com.android.internal.R; -import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.XmlUtils; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; @@ -454,7 +456,7 @@ public class ZenModeConfig implements Parcelable { newRule.conditionId = Uri.EMPTY; newRule.allowManualInvocation = true; newRule.zenPolicy = getDefaultZenPolicy(); - newRule.pkg = "android"; + newRule.pkg = PACKAGE_ANDROID; manualRule = newRule; } } @@ -957,15 +959,9 @@ public class ZenModeConfig implements Parcelable { rt.user = safeInt(parser, ZEN_ATT_USER, rt.user); boolean readSuppressedEffects = false; boolean readManualRule = false; + boolean readManualRuleWithoutPolicy = false; while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) { tag = parser.getName(); - if (type == XmlPullParser.END_TAG && ZEN_TAG.equals(tag)) { - if (Flags.modesUi() && !readManualRule) { - // migrate from fields on config into manual rule - rt.manualRule.zenPolicy = rt.toZenPolicy(); - } - return rt; - } if (type == XmlPullParser.START_TAG) { if (ALLOW_TAG.equals(tag)) { rt.allowCalls = safeBoolean(parser, ALLOW_ATT_CALLS, @@ -1034,9 +1030,17 @@ public class ZenModeConfig implements Parcelable { rt.suppressedVisualEffects = safeInt(parser, DISALLOW_ATT_VISUAL_EFFECTS, DEFAULT_SUPPRESSED_VISUAL_EFFECTS); } else if (MANUAL_TAG.equals(tag)) { - rt.manualRule = readRuleXml(parser); - if (rt.manualRule != null) { + ZenRule manualRule = readRuleXml(parser); + if (manualRule != null) { + rt.manualRule = manualRule; + + // Manual rule may be present prior to modes_ui if it were on, but in that + // case it would not have a set policy, so make note of the need to set + // it up later. readManualRule = true; + if (rt.manualRule.zenPolicy == null) { + readManualRuleWithoutPolicy = true; + } } } else if (AUTOMATIC_TAG.equals(tag) || (Flags.modesApi() && AUTOMATIC_DELETED_TAG.equals(tag))) { @@ -1058,6 +1062,23 @@ public class ZenModeConfig implements Parcelable { STATE_ATT_CHANNELS_BYPASSING_DND, DEFAULT_CHANNELS_BYPASSING_DND); } } + if (type == XmlPullParser.END_TAG && ZEN_TAG.equals(tag)) { + if (Flags.modesUi() && (!readManualRule || readManualRuleWithoutPolicy)) { + // migrate from fields on config into manual rule + rt.manualRule.zenPolicy = rt.toZenPolicy(); + if (readManualRuleWithoutPolicy) { + // indicates that the xml represents a pre-modes_ui XML with an enabled + // manual rule; set rule active, and fill in other fields as would be done + // in ensureManualZenRule() and setManualZenMode(). + rt.manualRule.pkg = PACKAGE_ANDROID; + rt.manualRule.type = AutomaticZenRule.TYPE_OTHER; + rt.manualRule.condition = new Condition( + rt.manualRule.conditionId != null ? rt.manualRule.conditionId + : Uri.EMPTY, "", Condition.STATE_TRUE); + } + } + return rt; + } } throw new IllegalStateException("Failed to reach END_DOCUMENT"); } @@ -2519,10 +2540,34 @@ public class ZenModeConfig implements Parcelable { } public static class ZenRule implements Parcelable { + + /** No manual override. Rule owner can decide its state. */ + public static final int OVERRIDE_NONE = 0; + /** + * User has manually activated a mode. This will temporarily overrule the rule owner's + * decision to deactivate it (see {@link #reconsiderConditionOverride}). + */ + public static final int OVERRIDE_ACTIVATE = 1; + /** + * User has manually deactivated an active mode, or setting ZEN_MODE_OFF (for the few apps + * still allowed to do that) snoozed the mode. This will temporarily overrule the rule + * owner's decision to activate it (see {@link #reconsiderConditionOverride}). + */ + public static final int OVERRIDE_DEACTIVATE = 2; + + @IntDef(prefix = { "OVERRIDE" }, value = { + OVERRIDE_NONE, + OVERRIDE_ACTIVATE, + OVERRIDE_DEACTIVATE + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ConditionOverride {} + @UnsupportedAppUsage public boolean enabled; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) - public boolean snoozing; // user manually disabled this instance + @Deprecated + public boolean snoozing; // user manually disabled this instance. Obsolete with MODES_UI @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public String name; // required for automatic @UnsupportedAppUsage @@ -2558,6 +2603,15 @@ public class ZenModeConfig implements Parcelable { // ZenPolicy, so we store them here, only for the manual rule. @FlaggedApi(Flags.FLAG_MODES_UI) int legacySuppressedEffects; + /** + * Signals a user's action to (temporarily or permanently) activate or deactivate this + * rule, overruling the condition set by the owner. This value is not stored to disk, as + * it shouldn't survive reboots or be involved in B&R. It might be reset by certain + * owner-provided state transitions as well. + */ + @FlaggedApi(Flags.FLAG_MODES_UI) + @ConditionOverride + int conditionOverride = OVERRIDE_NONE; public ZenRule() { } @@ -2599,6 +2653,7 @@ public class ZenModeConfig implements Parcelable { if (Flags.modesUi()) { disabledOrigin = source.readInt(); legacySuppressedEffects = source.readInt(); + conditionOverride = source.readInt(); } } } @@ -2677,6 +2732,7 @@ public class ZenModeConfig implements Parcelable { if (Flags.modesUi()) { dest.writeInt(disabledOrigin); dest.writeInt(legacySuppressedEffects); + dest.writeInt(conditionOverride); } } } @@ -2687,9 +2743,16 @@ public class ZenModeConfig implements Parcelable { .append("id=").append(id) .append(",state=").append(condition == null ? "STATE_FALSE" : Condition.stateToString(condition.state)) - .append(",enabled=").append(String.valueOf(enabled).toUpperCase()) - .append(",snoozing=").append(snoozing) - .append(",name=").append(name) + .append(",enabled=").append(String.valueOf(enabled).toUpperCase()); + + if (Flags.modesUi()) { + sb.append(",conditionOverride=") + .append(conditionOverrideToString(conditionOverride)); + } else { + sb.append(",snoozing=").append(snoozing); + } + + sb.append(",name=").append(name) .append(",zenMode=").append(Global.zenModeToString(zenMode)) .append(",conditionId=").append(conditionId) .append(",pkg=").append(pkg) @@ -2732,6 +2795,15 @@ public class ZenModeConfig implements Parcelable { return sb.append(']').toString(); } + private static String conditionOverrideToString(@ConditionOverride int value) { + return switch(value) { + case OVERRIDE_ACTIVATE -> "OVERRIDE_ACTIVATE"; + case OVERRIDE_DEACTIVATE -> "OVERRIDE_DEACTIVATE"; + case OVERRIDE_NONE -> "OVERRIDE_NONE"; + default -> "UNKNOWN"; + }; + } + /** @hide */ // TODO: add configuration activity public void dumpDebug(ProtoOutputStream proto, long fieldId) { @@ -2742,7 +2814,11 @@ public class ZenModeConfig implements Parcelable { proto.write(ZenRuleProto.CREATION_TIME_MS, creationTime); proto.write(ZenRuleProto.ENABLED, enabled); proto.write(ZenRuleProto.ENABLER, enabler); - proto.write(ZenRuleProto.IS_SNOOZING, snoozing); + if (Flags.modesApi() && Flags.modesUi()) { + proto.write(ZenRuleProto.IS_SNOOZING, conditionOverride == OVERRIDE_DEACTIVATE); + } else { + proto.write(ZenRuleProto.IS_SNOOZING, snoozing); + } proto.write(ZenRuleProto.ZEN_MODE, zenMode); if (conditionId != null) { proto.write(ZenRuleProto.CONDITION_ID, conditionId.toString()); @@ -2795,7 +2871,8 @@ public class ZenModeConfig implements Parcelable { if (Flags.modesUi()) { finalEquals = finalEquals && other.disabledOrigin == disabledOrigin - && other.legacySuppressedEffects == legacySuppressedEffects; + && other.legacySuppressedEffects == legacySuppressedEffects + && other.conditionOverride == conditionOverride; } } @@ -2811,7 +2888,8 @@ public class ZenModeConfig implements Parcelable { zenDeviceEffects, modified, allowManualInvocation, iconResName, triggerDescription, type, userModifiedFields, zenPolicyUserModifiedFields, zenDeviceEffectsUserModifiedFields, - deletionInstant, disabledOrigin, legacySuppressedEffects); + deletionInstant, disabledOrigin, legacySuppressedEffects, + conditionOverride); } else { return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition, component, configurationActivity, pkg, id, enabler, zenPolicy, @@ -2837,8 +2915,74 @@ public class ZenModeConfig implements Parcelable { } } + // TODO: b/333527800 - Rename to isActive() public boolean isAutomaticActive() { - return enabled && !snoozing && getPkg() != null && isTrueOrUnknown(); + if (Flags.modesApi() && Flags.modesUi()) { + if (!enabled || getPkg() == null) { + return false; + } else if (conditionOverride == OVERRIDE_ACTIVATE) { + return true; + } else if (conditionOverride == OVERRIDE_DEACTIVATE) { + return false; + } else { + return isTrueOrUnknown(); + } + } else { + return enabled && !snoozing && getPkg() != null && isTrueOrUnknown(); + } + } + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + @ConditionOverride + public int getConditionOverride() { + if (Flags.modesApi() && Flags.modesUi()) { + return conditionOverride; + } else { + return snoozing ? OVERRIDE_DEACTIVATE : OVERRIDE_NONE; + } + } + + public void setConditionOverride(@ConditionOverride int value) { + if (Flags.modesApi() && Flags.modesUi()) { + conditionOverride = value; + } else { + if (value == OVERRIDE_ACTIVATE) { + Slog.wtf(TAG, "Shouldn't set OVERRIDE_ACTIVATE if MODES_UI is off"); + } else if (value == OVERRIDE_DEACTIVATE) { + snoozing = true; + } else if (value == OVERRIDE_NONE) { + snoozing = false; + } + } + } + + public void resetConditionOverride() { + setConditionOverride(OVERRIDE_NONE); + } + + /** + * Possibly remove the override, depending on the rule owner's intended state. + * + * <p>This allows rule owners to "take over" manually-provided state with their smartness, + * but only once both agree. + * + * <p>For example, a manually activated rule wins over rule owner's opinion that it should + * be off, until the owner says it should be on, at which point it will turn off (without + * manual intervention) when the rule owner says it should be off. And symmetrically for + * manual deactivation (which used to be called "snoozing"). + */ + public void reconsiderConditionOverride() { + if (Flags.modesApi() && Flags.modesUi()) { + if (conditionOverride == OVERRIDE_ACTIVATE && isTrueOrUnknown()) { + resetConditionOverride(); + } else if (conditionOverride == OVERRIDE_DEACTIVATE && !isTrueOrUnknown()) { + resetConditionOverride(); + } + } else { + if (snoozing && !isTrueOrUnknown()) { + snoozing = false; + } + } } public String getPkg() { diff --git a/core/java/android/service/notification/ZenModeDiff.java b/core/java/android/service/notification/ZenModeDiff.java index a37e2277f0d1..05c2a9c26709 100644 --- a/core/java/android/service/notification/ZenModeDiff.java +++ b/core/java/android/service/notification/ZenModeDiff.java @@ -454,6 +454,8 @@ public class ZenModeDiff { */ public static class RuleDiff extends BaseDiff { public static final String FIELD_ENABLED = "enabled"; + public static final String FIELD_CONDITION_OVERRIDE = "conditionOverride"; + @Deprecated public static final String FIELD_SNOOZING = "snoozing"; public static final String FIELD_NAME = "name"; public static final String FIELD_ZEN_MODE = "zenMode"; @@ -507,8 +509,15 @@ public class ZenModeDiff { if (from.enabled != to.enabled) { addField(FIELD_ENABLED, new FieldDiff<>(from.enabled, to.enabled)); } - if (from.snoozing != to.snoozing) { - addField(FIELD_SNOOZING, new FieldDiff<>(from.snoozing, to.snoozing)); + if (Flags.modesApi() && Flags.modesUi()) { + if (from.conditionOverride != to.conditionOverride) { + addField(FIELD_CONDITION_OVERRIDE, + new FieldDiff<>(from.conditionOverride, to.conditionOverride)); + } + } else { + if (from.snoozing != to.snoozing) { + addField(FIELD_SNOOZING, new FieldDiff<>(from.snoozing, to.snoozing)); + } } if (!Objects.equals(from.name, to.name)) { addField(FIELD_NAME, new FieldDiff<>(from.name, to.name)); diff --git a/core/java/android/text/flags/flags.aconfig b/core/java/android/text/flags/flags.aconfig index 88a1b9c562d3..bb3f6c9928ac 100644 --- a/core/java/android/text/flags/flags.aconfig +++ b/core/java/android/text/flags/flags.aconfig @@ -118,6 +118,13 @@ flag { } flag { + name: "insert_mode_highlight_range" + namespace: "text" + description: "Make the highlight range stick after editing, this handles the corner cases where the entire text is replaced with itself(or transformed by developer) after each editing." + bug: "355137282" +} + +flag { name: "insert_mode_not_update_selection" namespace: "text" description: "Fix that InputService#onUpdateSelection is not called when insert mode gesture is performed." @@ -259,4 +266,14 @@ flag { metadata { purpose: PURPOSE_BUGFIX } -}
\ No newline at end of file +} + +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 + } +} diff --git a/core/java/android/text/method/InsertModeTransformationMethod.java b/core/java/android/text/method/InsertModeTransformationMethod.java index 59b80f3a6468..6c6576f8888e 100644 --- a/core/java/android/text/method/InsertModeTransformationMethod.java +++ b/core/java/android/text/method/InsertModeTransformationMethod.java @@ -36,6 +36,7 @@ import android.view.View; import com.android.internal.util.ArrayUtils; import com.android.internal.util.Preconditions; +import com.android.text.flags.Flags; import java.lang.reflect.Array; @@ -171,9 +172,15 @@ public class InsertModeTransformationMethod implements TransformationMethod, Tex // The text change is before the highlight start, move the highlight start. mStart += diff; } else { - // The text change covers the highlight start. Extend the highlight start to the - // change start. This should be a rare case. - mStart = start; + if (Flags.insertModeHighlightRange()) { + // The text change covers the highlight start. Don't change the start except + // when it's out of range. + mStart = Math.min(mStart, s.length()); + } else { + // The text change covers the highlight start. Extend the highlight start to the + // change start. This should be a rare case. + mStart = start; + } } } @@ -181,9 +188,15 @@ public class InsertModeTransformationMethod implements TransformationMethod, Tex // The text change is before the highlight end, move the highlight end. mEnd += diff; } else if (start < mEnd) { - // The text change covers the highlight end. Extend the highlight end to the - // change end. This should be a rare case. - mEnd = start + count; + if (Flags.insertModeHighlightRange()) { + // The text change covers the highlight end. Don't change the end except when it's + // out of range. + mEnd = Math.min(mEnd, s.length()); + } else { + // The text change covers the highlight end. Extend the highlight end to the + // change end. This should be a rare case. + mEnd = start + count; + } } } diff --git a/core/java/android/text/style/AccessibilityClickableSpan.java b/core/java/android/text/style/AccessibilityClickableSpan.java index 534ce63349e0..ee8d156f9aac 100644 --- a/core/java/android/text/style/AccessibilityClickableSpan.java +++ b/core/java/android/text/style/AccessibilityClickableSpan.java @@ -156,4 +156,12 @@ public class AccessibilityClickableSpan extends ClickableSpan return new AccessibilityClickableSpan[size]; } }; + + /** + * @return the ID of the original clickable span that this is applied to. + * @hide + */ + public int getOriginalClickableSpanId() { + return mOriginalClickableSpanId; + } } diff --git a/core/java/android/text/style/BulletSpan.java b/core/java/android/text/style/BulletSpan.java index b3e7bda07413..f70e6c56b5c9 100644 --- a/core/java/android/text/style/BulletSpan.java +++ b/core/java/android/text/style/BulletSpan.java @@ -119,7 +119,10 @@ public class BulletSpan implements LeadingMarginSpan, ParcelableSpan { this(gapWidth, color, true, bulletRadius); } - private BulletSpan(int gapWidth, @ColorInt int color, boolean wantColor, + /** + * @hide + */ + public BulletSpan(int gapWidth, @ColorInt int color, boolean wantColor, @IntRange(from = 0) int bulletRadius) { mGapWidth = gapWidth; mBulletRadius = bulletRadius; @@ -199,6 +202,14 @@ public class BulletSpan implements LeadingMarginSpan, ParcelableSpan { return mColor; } + /** + * @return true if the bullet should apply the color. + * @hide + */ + public boolean getWantColor() { + return mWantColor; + } + @Override public void drawLeadingMargin(@NonNull Canvas canvas, @NonNull Paint paint, int x, int dir, int top, int baseline, int bottom, diff --git a/core/java/android/text/style/SuggestionSpan.java b/core/java/android/text/style/SuggestionSpan.java index ad044af91d59..0cf96f617f4a 100644 --- a/core/java/android/text/style/SuggestionSpan.java +++ b/core/java/android/text/style/SuggestionSpan.java @@ -248,21 +248,42 @@ public class SuggestionSpan extends CharacterStyle implements ParcelableSpan { } public SuggestionSpan(Parcel src) { - mSuggestions = src.readStringArray(); - mFlags = src.readInt(); - mLocaleStringForCompatibility = src.readString(); - mLanguageTag = src.readString(); - mHashCode = src.readInt(); - mEasyCorrectUnderlineColor = src.readInt(); - mEasyCorrectUnderlineThickness = src.readFloat(); - mMisspelledUnderlineColor = src.readInt(); - mMisspelledUnderlineThickness = src.readFloat(); - mAutoCorrectionUnderlineColor = src.readInt(); - mAutoCorrectionUnderlineThickness = src.readFloat(); - mGrammarErrorUnderlineColor = src.readInt(); - mGrammarErrorUnderlineThickness = src.readFloat(); + this(/* suggestions= */ src.readStringArray(), /* flags= */ src.readInt(), + /* localStringForCompatibility= */ src.readString(), + /* languageTag= */ src.readString(), /* hashCode= */ src.readInt(), + /* easyCorrectUnderlineColor= */ src.readInt(), + /* easyCorrectUnderlineThickness= */ src.readFloat(), + /* misspelledUnderlineColor= */ src.readInt(), + /* misspelledUnderlineThickness= */ src.readFloat(), + /* autoCorrectionUnderlineColor= */ src.readInt(), + /* autoCorrectionUnderlineThickness= */ src.readFloat(), + /* grammarErrorUnderlineColor= */ src.readInt(), + /* grammarErrorUnderlineThickness= */ src.readFloat()); } + /** @hide */ + public SuggestionSpan(String[] suggestions, int flags, String localeStringForCompatibility, + String languageTag, int hashCode, int easyCorrectUnderlineColor, + float easyCorrectUnderlineThickness, int misspelledUnderlineColor, + float misspelledUnderlineThickness, int autoCorrectionUnderlineColor, + float autoCorrectionUnderlineThickness, int grammarErrorUnderlineColor, + float grammarErrorUnderlineThickness) { + mSuggestions = suggestions; + mFlags = flags; + mLocaleStringForCompatibility = localeStringForCompatibility; + mLanguageTag = languageTag; + mHashCode = hashCode; + mEasyCorrectUnderlineColor = easyCorrectUnderlineColor; + mEasyCorrectUnderlineThickness = easyCorrectUnderlineThickness; + mMisspelledUnderlineColor = misspelledUnderlineColor; + mMisspelledUnderlineThickness = misspelledUnderlineThickness; + mAutoCorrectionUnderlineColor = autoCorrectionUnderlineColor; + mAutoCorrectionUnderlineThickness = autoCorrectionUnderlineThickness; + mGrammarErrorUnderlineColor = grammarErrorUnderlineColor; + mGrammarErrorUnderlineThickness = grammarErrorUnderlineThickness; + } + + /** * @return an array of suggestion texts for this span */ @@ -452,4 +473,44 @@ public class SuggestionSpan extends CharacterStyle implements ParcelableSpan { public void notifySelection(Context context, String original, int index) { Log.w(TAG, "notifySelection() is deprecated. Does nothing."); } + + /** @hide */ + public float getEasyCorrectUnderlineThickness() { + return mEasyCorrectUnderlineThickness; + } + + /** @hide */ + public int getEasyCorrectUnderlineColor() { + return mEasyCorrectUnderlineColor; + } + + /** @hide */ + public float getMisspelledUnderlineThickness() { + return mMisspelledUnderlineThickness; + } + + /** @hide */ + public int getMisspelledUnderlineColor() { + return mMisspelledUnderlineColor; + } + + /** @hide */ + public float getAutoCorrectionUnderlineThickness() { + return mAutoCorrectionUnderlineThickness; + } + + /** @hide */ + public int getAutoCorrectionUnderlineColor() { + return mAutoCorrectionUnderlineColor; + } + + /** @hide */ + public float getGrammarErrorUnderlineThickness() { + return mGrammarErrorUnderlineThickness; + } + + /** @hide */ + public int getGrammarErrorUnderlineColor() { + return mGrammarErrorUnderlineColor; + } } diff --git a/core/java/android/text/style/TextAppearanceSpan.java b/core/java/android/text/style/TextAppearanceSpan.java index d61228b295af..245a9dbc9f6c 100644 --- a/core/java/android/text/style/TextAppearanceSpan.java +++ b/core/java/android/text/style/TextAppearanceSpan.java @@ -233,36 +233,59 @@ public class TextAppearanceSpan extends MetricAffectingSpan implements Parcelabl } public TextAppearanceSpan(Parcel src) { - mFamilyName = src.readString(); - mStyle = src.readInt(); - mTextSize = src.readInt(); - if (src.readInt() != 0) { - mTextColor = ColorStateList.CREATOR.createFromParcel(src); - } else { - mTextColor = null; - } - if (src.readInt() != 0) { - mTextColorLink = ColorStateList.CREATOR.createFromParcel(src); - } else { - mTextColorLink = null; - } - mTypeface = LeakyTypefaceStorage.readTypefaceFromParcel(src); + this(/* familyName= */ src.readString(), + /* style= */ src.readInt(), + /* textSize= */ src.readInt(), + /* textColor= */ (src.readInt() != 0) + ? ColorStateList.CREATOR.createFromParcel(src) : null, + /* textColorLink= */ (src.readInt() != 0) + ? ColorStateList.CREATOR.createFromParcel(src) : null, + /* typeface= */ LeakyTypefaceStorage.readTypefaceFromParcel(src), + /* textFontWeight= */ src.readInt(), + /* textLocales= */ + src.readParcelable(LocaleList.class.getClassLoader(), LocaleList.class), + /* shadowRadius= */ src.readFloat(), + /* shadowDx= */ src.readFloat(), + /* shadowDy= */ src.readFloat(), + /* shadowColor= */ src.readInt(), + /* hasElegantTextHeight= */ src.readBoolean(), + /* elegantTextHeight= */ src.readBoolean(), + /* hasLetterSpacing= */ src.readBoolean(), + /* letterSpacing= */ src.readFloat(), + /* fontFeatureSettings= */ src.readString(), + /* fontVariationSettings= */ src.readString()); + } - mTextFontWeight = src.readInt(); - mTextLocales = src.readParcelable(LocaleList.class.getClassLoader(), android.os.LocaleList.class); + /** @hide */ + public TextAppearanceSpan(@Nullable String familyName, int style, int textSize, + @Nullable ColorStateList textColor, @Nullable ColorStateList textColorLink, + @Nullable Typeface typeface, + int textFontWeight, @Nullable LocaleList textLocales, float shadowRadius, + float shadowDx, float shadowDy, int shadowColor, boolean hasElegantTextHeight, + boolean elegantTextHeight, boolean hasLetterSpacing, float letterSpacing, + @Nullable String fontFeatureSettings, @Nullable String fontVariationSettings) { + mFamilyName = familyName; + mStyle = style; + mTextSize = textSize; + mTextColor = textColor; + mTextColorLink = textColorLink; + mTypeface = typeface; - mShadowRadius = src.readFloat(); - mShadowDx = src.readFloat(); - mShadowDy = src.readFloat(); - mShadowColor = src.readInt(); + mTextFontWeight = textFontWeight; + mTextLocales = textLocales; - mHasElegantTextHeight = src.readBoolean(); - mElegantTextHeight = src.readBoolean(); - mHasLetterSpacing = src.readBoolean(); - mLetterSpacing = src.readFloat(); + mShadowRadius = shadowRadius; + mShadowDx = shadowDx; + mShadowDy = shadowDy; + mShadowColor = shadowColor; - mFontFeatureSettings = src.readString(); - mFontVariationSettings = src.readString(); + mHasElegantTextHeight = hasElegantTextHeight; + mElegantTextHeight = elegantTextHeight; + mHasLetterSpacing = hasLetterSpacing; + mLetterSpacing = letterSpacing; + + mFontFeatureSettings = fontFeatureSettings; + mFontVariationSettings = fontVariationSettings; } public int getSpanTypeId() { @@ -564,4 +587,14 @@ public class TextAppearanceSpan extends MetricAffectingSpan implements Parcelabl + ", fontVariationSettings='" + getFontVariationSettings() + '\'' + '}'; } + + /** @hide */ + public boolean hasElegantTextHeight() { + return mHasElegantTextHeight; + } + + /** @hide */ + public boolean hasLetterSpacing() { + return mHasLetterSpacing; + } } diff --git a/core/java/android/view/DisplayInfo.java b/core/java/android/view/DisplayInfo.java index 12dbc5afd0a3..157cec8a4d0f 100644 --- a/core/java/android/view/DisplayInfo.java +++ b/core/java/android/view/DisplayInfo.java @@ -708,7 +708,7 @@ public final class DisplayInfo implements Parcelable { */ @Nullable public Display.Mode findDefaultModeByRefreshRate(float refreshRate) { - Display.Mode[] modes = supportedModes; + Display.Mode[] modes = appsSupportedModes; Display.Mode defaultMode = getDefaultMode(); for (int i = 0; i < modes.length; i++) { if (modes[i].matches( @@ -723,7 +723,7 @@ public final class DisplayInfo implements Parcelable { * Returns the list of supported refresh rates in the default mode. */ public float[] getDefaultRefreshRates() { - Display.Mode[] modes = supportedModes; + Display.Mode[] modes = appsSupportedModes; ArraySet<Float> rates = new ArraySet<>(); Display.Mode defaultMode = getDefaultMode(); for (int i = 0; i < modes.length; i++) { diff --git a/core/java/android/view/HapticScrollFeedbackProvider.java b/core/java/android/view/HapticScrollFeedbackProvider.java index f370256391bb..0001176220b5 100644 --- a/core/java/android/view/HapticScrollFeedbackProvider.java +++ b/core/java/android/view/HapticScrollFeedbackProvider.java @@ -100,8 +100,12 @@ public class HapticScrollFeedbackProvider implements ScrollFeedbackProvider { if (Math.abs(mTotalScrollPixels) >= mTickIntervalPixels) { mTotalScrollPixels %= mTickIntervalPixels; - // TODO(b/239594271): create a new `performHapticFeedbackForDevice` and use that here. - mView.performHapticFeedback(HapticFeedbackConstants.SCROLL_TICK); + if (android.os.vibrator.Flags.hapticFeedbackInputSourceCustomizationEnabled()) { + mView.performHapticFeedbackForInputDevice( + HapticFeedbackConstants.SCROLL_TICK, inputDeviceId, source, /* flags= */ 0); + } else { + mView.performHapticFeedback(HapticFeedbackConstants.SCROLL_TICK); + } } } @@ -115,9 +119,12 @@ public class HapticScrollFeedbackProvider implements ScrollFeedbackProvider { if (!mCanPlayLimitFeedback) { return; } - - // TODO(b/239594271): create a new `performHapticFeedbackForDevice` and use that here. - mView.performHapticFeedback(HapticFeedbackConstants.SCROLL_LIMIT); + if (android.os.vibrator.Flags.hapticFeedbackInputSourceCustomizationEnabled()) { + mView.performHapticFeedbackForInputDevice( + HapticFeedbackConstants.SCROLL_LIMIT, inputDeviceId, source, /* flags= */ 0); + } else { + mView.performHapticFeedback(HapticFeedbackConstants.SCROLL_LIMIT); + } mCanPlayLimitFeedback = false; } @@ -128,8 +135,13 @@ public class HapticScrollFeedbackProvider implements ScrollFeedbackProvider { if (!mHapticScrollFeedbackEnabled) { return; } - // TODO(b/239594271): create a new `performHapticFeedbackForDevice` and use that here. - mView.performHapticFeedback(HapticFeedbackConstants.SCROLL_ITEM_FOCUS); + if (android.os.vibrator.Flags.hapticFeedbackInputSourceCustomizationEnabled()) { + mView.performHapticFeedbackForInputDevice( + HapticFeedbackConstants.SCROLL_ITEM_FOCUS, inputDeviceId, source, + /* flags= */ 0); + } else { + mView.performHapticFeedback(HapticFeedbackConstants.SCROLL_ITEM_FOCUS); + } mCanPlayLimitFeedback = true; } diff --git a/core/java/android/view/IWindowSession.aidl b/core/java/android/view/IWindowSession.aidl index 762a302e8170..11a3168daa0e 100644 --- a/core/java/android/view/IWindowSession.aidl +++ b/core/java/android/view/IWindowSession.aidl @@ -140,15 +140,6 @@ interface IWindowSession { oneway void finishDrawing(IWindow window, in SurfaceControl.Transaction postDrawTransaction, int seqId); - @UnsupportedAppUsage - boolean performHapticFeedback(int effectId, int flags, int privFlags); - - /** - * Called by attached views to perform predefined haptic feedback without requiring VIBRATE - * permission. - */ - oneway void performHapticFeedbackAsync(int effectId, int flags, int privFlags); - /** * Initiate the drag operation itself * 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/SurfaceControl.java b/core/java/android/view/SurfaceControl.java index a7641c07bb90..9e4b27d3fa55 100644 --- a/core/java/android/view/SurfaceControl.java +++ b/core/java/android/view/SurfaceControl.java @@ -3574,7 +3574,7 @@ public final class SurfaceControl implements Parcelable { checkPreconditions(sc); if (SurfaceControlRegistry.sCallStackDebuggingEnabled) { SurfaceControlRegistry.getProcessInstance().checkCallStackDebugging( - "reparent", this, sc, + "setColor", this, sc, "r=" + color[0] + " g=" + color[1] + " b=" + color[2]); } nativeSetColor(mNativeObject, sc.mNativeObject, color); diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 5f8bea1cdc47..dbd65de32471 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -142,7 +142,6 @@ import android.os.RemoteException; import android.os.SystemClock; import android.os.Trace; import android.os.Vibrator; -import android.os.vibrator.Flags; import android.service.credentials.CredentialProviderService; import android.sysprop.DisplayProperties; import android.text.InputType; @@ -5712,9 +5711,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ private PointerIcon mMousePointerIcon; - /** Vibrator for haptic feedback. */ - private Vibrator mVibrator; - /** * @hide */ @@ -28667,37 +28663,63 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @param flags Additional flags as per {@link HapticFeedbackConstants}. */ public boolean performHapticFeedback(int feedbackConstant, int flags) { - if (feedbackConstant == HapticFeedbackConstants.NO_HAPTICS - || mAttachInfo == null) { + if (isPerformHapticFeedbackSuppressed(feedbackConstant, flags)) { return false; } + + int privFlags = computeHapticFeedbackPrivateFlags(); + return mAttachInfo.mRootCallbacks.performHapticFeedback(feedbackConstant, flags, privFlags); + } + + /** + * <p>Provide haptic feedback to the user for this view. + * + * <p>Call this method (vs {@link #performHapticFeedback(int)}) to specify more details about + * the {@link InputDevice} that caused this haptic feedback. The framework will choose and + * provide a haptic feedback based on these details. + * + * <p>The feedback will only be performed if {@link #isHapticFeedbackEnabled()} is {@code true}. + * + * @param feedbackConstant One of the constants defined in {@link HapticFeedbackConstants}. + * @param inputDeviceId The ID of the {@link InputDevice} that generated the event which + * triggered this haptic feedback request. + * @param inputSource The input source of the event which triggered this haptic feedback + * request, defined as {@code InputDevice#SOURCE_*}. + * + * @hide + */ + public void performHapticFeedbackForInputDevice(int feedbackConstant, int inputDeviceId, + int inputSource, int flags) { + if (isPerformHapticFeedbackSuppressed(feedbackConstant, flags)) { + return; + } + + int privFlags = computeHapticFeedbackPrivateFlags(); + mAttachInfo.mRootCallbacks.performHapticFeedbackForInputDevice( + feedbackConstant, inputDeviceId, inputSource, flags, privFlags); + } + + private boolean isPerformHapticFeedbackSuppressed(int feedbackConstant, int flags) { + if (feedbackConstant == HapticFeedbackConstants.NO_HAPTICS + || mAttachInfo == null + || mAttachInfo.mSession == null) { + return true; + } //noinspection SimplifiableIfStatement if ((flags & HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING) == 0 && !isHapticFeedbackEnabled()) { - return false; + return true; } + return false; + } + private int computeHapticFeedbackPrivateFlags() { int privFlags = 0; if (mAttachInfo.mViewRootImpl != null && mAttachInfo.mViewRootImpl.mWindowAttributes.type == TYPE_INPUT_METHOD) { privFlags = HapticFeedbackConstants.PRIVATE_FLAG_APPLY_INPUT_METHOD_SETTINGS; } - if (Flags.useVibratorHapticFeedback()) { - if (!mAttachInfo.canPerformHapticFeedback()) { - return false; - } - getSystemVibrator().performHapticFeedback(feedbackConstant, - "View#performHapticFeedback", flags, privFlags); - return true; - } - return mAttachInfo.mRootCallbacks.performHapticFeedback(feedbackConstant, flags, privFlags); - } - - private Vibrator getSystemVibrator() { - if (mVibrator != null) { - return mVibrator; - } - return mVibrator = mContext.getSystemService(Vibrator.class); + return privFlags; } /** @@ -31731,6 +31753,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, boolean performHapticFeedback(int effectId, @HapticFeedbackConstants.Flags int flags, @HapticFeedbackConstants.PrivateFlags int privFlags); + + void performHapticFeedbackForInputDevice(int effectId, + int inputDeviceId, int inputSource, + @HapticFeedbackConstants.Flags int flags, + @HapticFeedbackConstants.PrivateFlags int privFlags); } /** @@ -32297,11 +32324,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, return events; } - private boolean canPerformHapticFeedback() { - return mSession != null - && (mDisplay.getFlags() & Display.FLAG_TOUCH_FEEDBACK_DISABLED) == 0; - } - @Nullable ScrollCaptureInternal getScrollCaptureInternal() { if (mScrollCaptureInternal != null) { diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 9518abfe220b..1c0700f69ab6 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -207,6 +207,7 @@ import android.os.SystemClock; import android.os.SystemProperties; import android.os.Trace; import android.os.UserHandle; +import android.os.Vibrator; import android.provider.Settings; import android.sysprop.DisplayProperties; import android.sysprop.ViewProperties; @@ -362,14 +363,6 @@ public final class ViewRootImpl implements ViewParent, private static final boolean ENABLE_INPUT_LATENCY_TRACKING = true; /** - * Controls whether to use the new oneway performHapticFeedback call. This returns - * true in a few more conditions, but doesn't affect which haptics happen. Notably, it - * makes the call to performHapticFeedback non-blocking, which reduces potential UI jank. - * This is intended as a temporary flag, ultimately becoming permanently 'true'. - */ - private static final boolean USE_ASYNC_PERFORM_HAPTIC_FEEDBACK = true; - - /** * Whether the client (system UI) is handling the transient gesture and the corresponding * animation. * @hide @@ -956,6 +949,11 @@ public final class ViewRootImpl implements ViewParent, */ AudioManager mAudioManager; + /** + * see {@link #performHapticFeedback(int, int, int)} + */ + Vibrator mVibrator; + final AccessibilityManager mAccessibilityManager; AccessibilityInteractionController mAccessibilityInteractionController; @@ -2822,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; } /** @@ -9236,6 +9240,13 @@ public final class ViewRootImpl implements ViewParent, return mAudioManager; } + private Vibrator getSystemVibrator() { + if (mVibrator == null) { + mVibrator = mContext.getSystemService(Vibrator.class); + } + return mVibrator; + } + private @Nullable AutofillManager getAutofillManager() { if (mView instanceof ViewGroup) { ViewGroup decorView = (ViewGroup) mView; @@ -9662,17 +9673,23 @@ public final class ViewRootImpl implements ViewParent, return false; } - try { - if (USE_ASYNC_PERFORM_HAPTIC_FEEDBACK) { - mWindowSession.performHapticFeedbackAsync(effectId, flags, privFlags); - return true; - } else { - // Original blocking binder call path. - return mWindowSession.performHapticFeedback(effectId, flags, privFlags); - } - } catch (RemoteException e) { - return false; + getSystemVibrator().performHapticFeedback( + effectId, "ViewRootImpl#performHapticFeedback", flags, privFlags); + return true; + } + + @Override + public void performHapticFeedbackForInputDevice(int effectId, + int inputDeviceId, int inputSource, + @HapticFeedbackConstants.Flags int flags, + @HapticFeedbackConstants.PrivateFlags int privFlags) { + if ((mDisplay.getFlags() & Display.FLAG_TOUCH_FEEDBACK_DISABLED) != 0) { + return; } + + getSystemVibrator().performHapticFeedbackForInputDevice(effectId, + inputDeviceId, inputSource, "ViewRootImpl#performHapticFeedbackForInputDevice", + flags, privFlags); } /** diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index 85d4ec00c7bc..017e004a7f13 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -132,6 +132,7 @@ import android.window.InputTransferToken; import android.window.TaskFpsCallback; import android.window.TrustedPresentationThresholds; +import com.android.internal.R; import com.android.window.flags.Flags; import java.lang.annotation.ElementType; @@ -482,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 @@ -512,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) @@ -1926,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) { @@ -3468,6 +3476,13 @@ public interface WindowManager extends ViewManager { public static final int PRIVATE_FLAG_CONSUME_IME_INSETS = 1 << 25; /** + * Flag to indicate that the window has the + * {@link R.styleable.Window_windowOptOutEdgeToEdgeEnforcement} flag set. + * @hide + */ + public static final int PRIVATE_FLAG_OPT_OUT_EDGE_TO_EDGE = 1 << 26; + + /** * Flag to indicate that the window is controlling how it fits window insets on its own. * So we don't need to adjust its attributes for fitting window insets. * @hide @@ -3540,6 +3555,7 @@ public interface WindowManager extends ViewManager { PRIVATE_FLAG_NOT_MAGNIFIABLE, PRIVATE_FLAG_COLOR_SPACE_AGNOSTIC, PRIVATE_FLAG_CONSUME_IME_INSETS, + PRIVATE_FLAG_OPT_OUT_EDGE_TO_EDGE, PRIVATE_FLAG_FIT_INSETS_CONTROLLED, PRIVATE_FLAG_TRUSTED_OVERLAY, PRIVATE_FLAG_INSET_PARENT_FRAME_BY_IME, @@ -3644,6 +3660,10 @@ public interface WindowManager extends ViewManager { equals = PRIVATE_FLAG_CONSUME_IME_INSETS, name = "CONSUME_IME_INSETS"), @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_OPT_OUT_EDGE_TO_EDGE, + equals = PRIVATE_FLAG_OPT_OUT_EDGE_TO_EDGE, + name = "OPTOUT_EDGE_TO_EDGE"), + @ViewDebug.FlagToString( mask = PRIVATE_FLAG_FIT_INSETS_CONTROLLED, equals = PRIVATE_FLAG_FIT_INSETS_CONTROLLED, name = "FIT_INSETS_CONTROLLED"), diff --git a/core/java/android/view/WindowManagerGlobal.java b/core/java/android/view/WindowManagerGlobal.java index 961a9c438ba7..f50ea9106a61 100644 --- a/core/java/android/view/WindowManagerGlobal.java +++ b/core/java/android/view/WindowManagerGlobal.java @@ -17,6 +17,8 @@ package android.view; import static android.view.Display.INVALID_DISPLAY; +import static android.view.WindowManager.LayoutParams.LAST_APPLICATION_WINDOW; +import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_OPT_OUT_EDGE_TO_EDGE; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; import android.animation.ValueAnimator; @@ -26,6 +28,7 @@ import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.res.Configuration; +import android.content.res.TypedArray; import android.graphics.HardwareRenderer; import android.os.Binder; import android.os.Build; @@ -45,6 +48,7 @@ import android.window.ITrustedPresentationListener; import android.window.InputTransferToken; import android.window.TrustedPresentationThresholds; +import com.android.internal.R; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.FastPrintWriter; @@ -356,12 +360,12 @@ public final class WindowManagerGlobal { } final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params; + final Context context = view.getContext(); if (parentWindow != null) { parentWindow.adjustLayoutParamsForSubWindow(wparams); } else { // If there's no parent, then hardware acceleration for this view is // set from the application's hardware acceleration setting. - final Context context = view.getContext(); if (context != null && (context.getApplicationInfo().flags & ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) { @@ -369,6 +373,14 @@ public final class WindowManagerGlobal { } } + if (context != null && wparams.type > LAST_APPLICATION_WINDOW) { + final TypedArray styles = context.obtainStyledAttributes(R.styleable.Window); + if (styles.getBoolean(R.styleable.Window_windowOptOutEdgeToEdgeEnforcement, false)) { + wparams.privateFlags |= PRIVATE_FLAG_OPT_OUT_EDGE_TO_EDGE; + } + styles.recycle(); + } + ViewRootImpl root; View panelParentView = null; diff --git a/core/java/android/view/WindowlessWindowManager.java b/core/java/android/view/WindowlessWindowManager.java index 0d027f107a02..d2747e465071 100644 --- a/core/java/android/view/WindowlessWindowManager.java +++ b/core/java/android/view/WindowlessWindowManager.java @@ -504,16 +504,6 @@ public class WindowlessWindowManager implements IWindowSession { } @Override - public boolean performHapticFeedback(int effectId, int flags, int privFlags) { - return false; - } - - @Override - public void performHapticFeedbackAsync(int effectId, int flags, int privFlags) { - performHapticFeedback(effectId, flags, privFlags); - } - - @Override public android.os.IBinder performDrag(android.view.IWindow window, int flags, android.view.SurfaceControl surface, int touchSource, int touchDeviceId, int touchPointerId, float touchX, float touchY, float thumbCenterX, float thumbCenterY, diff --git a/core/java/android/view/animation/Animation.java b/core/java/android/view/animation/Animation.java index 09306c791537..288be9c392e1 100644 --- a/core/java/android/view/animation/Animation.java +++ b/core/java/android/view/animation/Animation.java @@ -28,6 +28,7 @@ import android.os.Handler; import android.os.SystemProperties; import android.util.AttributeSet; import android.util.TypedValue; +import android.view.WindowInsets; import dalvik.system.CloseGuard; @@ -881,12 +882,13 @@ public abstract class Animation implements Cloneable { } /** - * @return if a window animation has outsets applied to it. + * @return the edges to which outsets should be applied if run as a windoow animation. * * @hide */ - public boolean hasExtension() { - return false; + @WindowInsets.Side.InsetsSide + public int getExtensionEdges() { + return 0x0; } /** diff --git a/core/java/android/view/animation/AnimationSet.java b/core/java/android/view/animation/AnimationSet.java index 5aaa994f3f8f..bbdc9d0392ba 100644 --- a/core/java/android/view/animation/AnimationSet.java +++ b/core/java/android/view/animation/AnimationSet.java @@ -21,6 +21,7 @@ import android.content.res.TypedArray; import android.graphics.RectF; import android.os.Build; import android.util.AttributeSet; +import android.view.WindowInsets; import java.util.ArrayList; import java.util.List; @@ -540,12 +541,12 @@ public class AnimationSet extends Animation { /** @hide */ @Override - public boolean hasExtension() { + @WindowInsets.Side.InsetsSide + public int getExtensionEdges() { + int edge = 0x0; for (Animation animation : mAnimations) { - if (animation.hasExtension()) { - return true; - } + edge |= animation.getExtensionEdges(); } - return false; + return edge; } } diff --git a/core/java/android/view/animation/ExtendAnimation.java b/core/java/android/view/animation/ExtendAnimation.java index 210eb8a1ca9d..1aeee07538f8 100644 --- a/core/java/android/view/animation/ExtendAnimation.java +++ b/core/java/android/view/animation/ExtendAnimation.java @@ -20,6 +20,7 @@ import android.content.Context; import android.content.res.TypedArray; import android.graphics.Insets; import android.util.AttributeSet; +import android.view.WindowInsets; /** * An animation that controls the outset of an object. @@ -151,9 +152,12 @@ public class ExtendAnimation extends Animation { /** @hide */ @Override - public boolean hasExtension() { - return mFromInsets.left < 0 || mFromInsets.top < 0 || mFromInsets.right < 0 - || mFromInsets.bottom < 0; + @WindowInsets.Side.InsetsSide + public int getExtensionEdges() { + return (mFromInsets.left < 0 || mToInsets.left < 0 ? WindowInsets.Side.LEFT : 0) + | (mFromInsets.right < 0 || mToInsets.right < 0 ? WindowInsets.Side.RIGHT : 0) + | (mFromInsets.top < 0 || mToInsets.top < 0 ? WindowInsets.Side.TOP : 0) + | (mFromInsets.bottom < 0 || mToInsets.bottom < 0 ? WindowInsets.Side.BOTTOM : 0); } @Override 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/RemoteViewsSerializers.java b/core/java/android/widget/RemoteViewsSerializers.java index 600fea4a0bb8..080f22eafef6 100644 --- a/core/java/android/widget/RemoteViewsSerializers.java +++ b/core/java/android/widget/RemoteViewsSerializers.java @@ -15,12 +15,55 @@ */ package android.widget; +import static com.android.text.flags.Flags.FLAG_NO_BREAK_NO_HYPHENATION_SPAN; +import static com.android.text.flags.Flags.noBreakNoHyphenationSpan; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import android.annotation.FlaggedApi; import android.content.res.ColorStateList; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.BlendMode; import android.graphics.drawable.Icon; +import android.graphics.text.LineBreakConfig; +import android.os.LocaleList; +import android.os.PersistableBundle; +import android.text.Annotation; +import android.text.Layout; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.style.AbsoluteSizeSpan; +import android.text.style.AccessibilityClickableSpan; +import android.text.style.AccessibilityReplacementSpan; +import android.text.style.AccessibilityURLSpan; +import android.text.style.AlignmentSpan; +import android.text.style.BackgroundColorSpan; +import android.text.style.BulletSpan; +import android.text.style.CharacterStyle; +import android.text.style.EasyEditSpan; +import android.text.style.ForegroundColorSpan; +import android.text.style.LeadingMarginSpan; +import android.text.style.LineBackgroundSpan; +import android.text.style.LineBreakConfigSpan; +import android.text.style.LineHeightSpan; +import android.text.style.LocaleSpan; +import android.text.style.QuoteSpan; +import android.text.style.RelativeSizeSpan; +import android.text.style.ScaleXSpan; +import android.text.style.SpellCheckSpan; +import android.text.style.StrikethroughSpan; +import android.text.style.StyleSpan; +import android.text.style.SubscriptSpan; +import android.text.style.SuggestionRangeSpan; +import android.text.style.SuggestionSpan; +import android.text.style.SuperscriptSpan; +import android.text.style.TextAppearanceSpan; +import android.text.style.TtsSpan; +import android.text.style.TypefaceSpan; +import android.text.style.URLSpan; +import android.text.style.UnderlineSpan; import android.util.Log; import android.util.LongSparseArray; import android.util.proto.ProtoInputStream; @@ -29,7 +72,11 @@ import android.util.proto.ProtoUtils; import androidx.annotation.NonNull; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import java.util.function.Function; /** @@ -59,12 +106,13 @@ public class RemoteViewsSerializers { break; case Icon.TYPE_ADAPTIVE_BITMAP: final ByteArrayOutputStream adaptiveBitmapBytes = new ByteArrayOutputStream(); - icon.getBitmap().compress(Bitmap.CompressFormat.WEBP_LOSSLESS, 100, - adaptiveBitmapBytes); + icon.getBitmap() + .compress(Bitmap.CompressFormat.WEBP_LOSSLESS, 100, adaptiveBitmapBytes); out.write(RemoteViewsProto.Icon.ADAPTIVE_BITMAP, adaptiveBitmapBytes.toByteArray()); break; case Icon.TYPE_RESOURCE: - out.write(RemoteViewsProto.Icon.RESOURCE, + out.write( + RemoteViewsProto.Icon.RESOURCE, appResources.getResourceName(icon.getResId())); break; case Icon.TYPE_DATA: @@ -91,7 +139,8 @@ public class RemoteViewsSerializers { while (in.nextField() != ProtoInputStream.NO_MORE_FIELDS) { switch (in.getFieldNumber()) { case (int) RemoteViewsProto.Icon.BLEND_MODE: - values.put(RemoteViewsProto.Icon.BLEND_MODE, + values.put( + RemoteViewsProto.Icon.BLEND_MODE, in.readInt(RemoteViewsProto.Icon.BLEND_MODE)); break; case (int) RemoteViewsProto.Icon.TINT_LIST: @@ -101,7 +150,8 @@ public class RemoteViewsSerializers { break; case (int) RemoteViewsProto.Icon.BITMAP: byte[] bitmapData = in.readBytes(RemoteViewsProto.Icon.BITMAP); - values.put(RemoteViewsProto.Icon.BITMAP, + values.put( + RemoteViewsProto.Icon.BITMAP, BitmapFactory.decodeByteArray(bitmapData, 0, bitmapData.length)); break; case (int) RemoteViewsProto.Icon.ADAPTIVE_BITMAP: @@ -112,23 +162,27 @@ public class RemoteViewsSerializers { bitmapAdaptiveData.length)); break; case (int) RemoteViewsProto.Icon.RESOURCE: - values.put(RemoteViewsProto.Icon.RESOURCE, + values.put( + RemoteViewsProto.Icon.RESOURCE, in.readString(RemoteViewsProto.Icon.RESOURCE)); break; case (int) RemoteViewsProto.Icon.DATA: - values.put(RemoteViewsProto.Icon.DATA, - in.readBytes(RemoteViewsProto.Icon.DATA)); + values.put( + RemoteViewsProto.Icon.DATA, in.readBytes(RemoteViewsProto.Icon.DATA)); break; case (int) RemoteViewsProto.Icon.URI: values.put(RemoteViewsProto.Icon.URI, in.readString(RemoteViewsProto.Icon.URI)); break; case (int) RemoteViewsProto.Icon.URI_ADAPTIVE_BITMAP: - values.put(RemoteViewsProto.Icon.URI_ADAPTIVE_BITMAP, + values.put( + RemoteViewsProto.Icon.URI_ADAPTIVE_BITMAP, in.readString(RemoteViewsProto.Icon.URI_ADAPTIVE_BITMAP)); break; default: - Log.w(TAG, "Unhandled field while reading Icon proto!\n" - + ProtoUtils.currentFieldToString(in)); + Log.w( + TAG, + "Unhandled field while reading Icon proto!\n" + + ProtoUtils.currentFieldToString(in)); } } @@ -174,4 +228,1279 @@ public class RemoteViewsSerializers { return icon; }; } + + public static void writeCharSequenceToProto(@NonNull ProtoOutputStream out, + @NonNull CharSequence cs) { + out.write(RemoteViewsProto.CharSequence.TEXT, cs.toString()); + if (!(cs instanceof Spanned sp)) return; + + Object[] os = sp.getSpans(0, cs.length(), Object.class); + for (Object original : os) { + Object prop = original; + if (prop instanceof CharacterStyle) { + prop = ((CharacterStyle) prop).getUnderlying(); + } + + final long spansToken = out.start(RemoteViewsProto.CharSequence.SPANS); + out.write(RemoteViewsProto.CharSequence.Span.START, sp.getSpanStart(original)); + out.write(RemoteViewsProto.CharSequence.Span.END, sp.getSpanEnd(original)); + out.write(RemoteViewsProto.CharSequence.Span.FLAGS, sp.getSpanFlags(original)); + + if (prop instanceof AbsoluteSizeSpan span) { + final long spanToken = out.start(RemoteViewsProto.CharSequence.Span.ABSOLUTE_SIZE); + writeAbsoluteSizeSpanToProto(out, span); + out.end(spanToken); + } else if (prop instanceof AccessibilityClickableSpan span) { + final long spanToken = out.start( + RemoteViewsProto.CharSequence.Span.ACCESSIBILITY_CLICKABLE); + writeAccessibilityClickableSpanToProto(out, span); + out.end(spanToken); + } else if (prop instanceof AccessibilityReplacementSpan span) { + final long spanToken = out.start( + RemoteViewsProto.CharSequence.Span.ACCESSIBILITY_REPLACEMENT); + writeAccessibilityReplacementSpanToProto(out, span); + out.end(spanToken); + } else if (prop instanceof AccessibilityURLSpan span) { + final long spanToken = out.start( + RemoteViewsProto.CharSequence.Span.ACCESSIBILITY_URL); + writeAccessibilityURLSpanToProto(out, span); + out.end(spanToken); + } else if (prop instanceof Annotation span) { + final long spanToken = out.start(RemoteViewsProto.CharSequence.Span.ANNOTATION); + writeAnnotationToProto(out, span); + out.end(spanToken); + } else if (prop instanceof BackgroundColorSpan span) { + final long spanToken = out.start( + RemoteViewsProto.CharSequence.Span.BACKGROUND_COLOR); + writeBackgroundColorSpanToProto(out, span); + out.end(spanToken); + } else if (prop instanceof BulletSpan span) { + final long spanToken = out.start(RemoteViewsProto.CharSequence.Span.BULLET); + writeBulletSpanToProto(out, span); + out.end(spanToken); + } else if (prop instanceof EasyEditSpan span) { + final long spanToken = out.start(RemoteViewsProto.CharSequence.Span.EASY_EDIT); + writeEasyEditSpanToProto(out, span); + out.end(spanToken); + } else if (prop instanceof ForegroundColorSpan span) { + final long spanToken = out.start( + RemoteViewsProto.CharSequence.Span.FOREGROUND_COLOR); + writeForegroundColorSpanToProto(out, span); + out.end(spanToken); + } else if (noBreakNoHyphenationSpan() && prop instanceof LineBreakConfigSpan span) { + final long spanToken = out.start(RemoteViewsProto.CharSequence.Span.LINE_BREAK); + writeLineBreakConfigSpanToProto(out, span); + out.end(spanToken); + } else if (prop instanceof LocaleSpan span) { + final long spanToken = out.start(RemoteViewsProto.CharSequence.Span.LOCALE); + writeLocaleSpanToProto(out, span); + out.end(spanToken); + } else if (prop instanceof QuoteSpan span) { + final long spanToken = out.start(RemoteViewsProto.CharSequence.Span.QUOTE); + writeQuoteSpanToProto(out, span); + out.end(spanToken); + } else if (prop instanceof RelativeSizeSpan span) { + final long spanToken = out.start(RemoteViewsProto.CharSequence.Span.RELATIVE_SIZE); + writeRelativeSizeSpanToProto(out, span); + out.end(spanToken); + } else if (prop instanceof ScaleXSpan span) { + final long spanToken = out.start(RemoteViewsProto.CharSequence.Span.SCALE_X); + writeScaleXSpanToProto(out, span); + out.end(spanToken); + } else if (prop instanceof SpellCheckSpan span) { + final long spanToken = out.start(RemoteViewsProto.CharSequence.Span.SPELL_CHECK); + writeSpellCheckSpanToProto(out, span); + out.end(spanToken); + } else if (prop instanceof LineBackgroundSpan.Standard span) { + final long spanToken = out.start( + RemoteViewsProto.CharSequence.Span.LINE_BACKGROUND); + writeLineBackgroundSpanStandardToProto(out, span); + out.end(spanToken); + } else if (prop instanceof LineHeightSpan.Standard span) { + final long spanToken = out.start(RemoteViewsProto.CharSequence.Span.LINE_HEIGHT); + writeLineHeightSpanStandardToProto(out, span); + out.end(spanToken); + } else if (prop instanceof LeadingMarginSpan.Standard span) { + final long spanToken = out.start(RemoteViewsProto.CharSequence.Span.LEADING_MARGIN); + writeLeadingMarginSpanStandardToProto(out, span); + out.end(spanToken); + } else if (prop instanceof AlignmentSpan.Standard span) { + final long spanToken = out.start(RemoteViewsProto.CharSequence.Span.ALIGNMENT); + writeAlignmentSpanStandardToProto(out, span); + out.end(spanToken); + } else if (prop instanceof StrikethroughSpan span) { + final long spanToken = out.start(RemoteViewsProto.CharSequence.Span.STRIKETHROUGH); + writeStrikethroughSpanToProto(out, span); + out.end(spanToken); + } else if (prop instanceof StyleSpan span) { + final long spanToken = out.start(RemoteViewsProto.CharSequence.Span.STYLE); + writeStyleSpanToProto(out, span); + out.end(spanToken); + } else if (prop instanceof SubscriptSpan span) { + final long spanToken = out.start(RemoteViewsProto.CharSequence.Span.SUBSCRIPT); + writeSubscriptSpanToProto(out, span); + out.end(spanToken); + } else if (prop instanceof SuggestionRangeSpan span) { + final long spanToken = out.start( + RemoteViewsProto.CharSequence.Span.SUGGESTION_RANGE); + writeSuggestionRangeSpanToProto(out, span); + out.end(spanToken); + } else if (prop instanceof SuggestionSpan span) { + final long spanToken = out.start(RemoteViewsProto.CharSequence.Span.SUGGESTION); + writeSuggestionSpanToProto(out, span); + out.end(spanToken); + } else if (prop instanceof SuperscriptSpan span) { + final long spanToken = out.start(RemoteViewsProto.CharSequence.Span.SUPERSCRIPT); + writeSuperscriptSpanToProto(out, span); + out.end(spanToken); + } else if (prop instanceof TextAppearanceSpan span) { + final long spanToken = out.start( + RemoteViewsProto.CharSequence.Span.TEXT_APPEARANCE); + writeTextAppearanceSpanToProto(out, span); + out.end(spanToken); + } else if (prop instanceof TtsSpan span) { + final long spanToken = out.start(RemoteViewsProto.CharSequence.Span.TTS); + writeTtsSpanToProto(out, span); + out.end(spanToken); + } else if (prop instanceof TypefaceSpan span) { + final long spanToken = out.start(RemoteViewsProto.CharSequence.Span.TYPEFACE); + writeTypefaceSpanToProto(out, span); + out.end(spanToken); + } else if (prop instanceof URLSpan span) { + final long spanToken = out.start(RemoteViewsProto.CharSequence.Span.URL); + writeURLSpanToProto(out, span); + out.end(spanToken); + } else if (prop instanceof UnderlineSpan span) { + final long spanToken = out.start(RemoteViewsProto.CharSequence.Span.UNDERLINE); + writeUnderlineSpanToProto(out, span); + out.end(spanToken); + } + out.end(spansToken); + } + } + + public static CharSequence createCharSequenceFromProto(ProtoInputStream in) throws Exception { + SpannableStringBuilder builder = new SpannableStringBuilder(); + boolean hasSpans = false; + while (in.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (in.getFieldNumber()) { + case (int) RemoteViewsProto.CharSequence.TEXT: + String text = in.readString(RemoteViewsProto.CharSequence.TEXT); + builder.append(text); + break; + case (int) RemoteViewsProto.CharSequence.SPANS: + hasSpans = true; + final long spansToken = in.start(RemoteViewsProto.CharSequence.SPANS); + createSpanFromProto(in, builder); + in.end(spansToken); + break; + default: + Log.w(TAG, "Unhandled field while reading CharSequence proto!\n" + + ProtoUtils.currentFieldToString(in)); + } + } + return hasSpans ? builder : builder.toString(); + } + + private static void createSpanFromProto(ProtoInputStream in, SpannableStringBuilder builder) + throws Exception { + int start = 0; + int end = 0; + int flags = 0; + Object what = null; + while (in.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (in.getFieldNumber()) { + case (int) RemoteViewsProto.CharSequence.Span.START: + start = in.readInt(RemoteViewsProto.CharSequence.Span.START); + break; + case (int) RemoteViewsProto.CharSequence.Span.END: + end = in.readInt(RemoteViewsProto.CharSequence.Span.END); + break; + case (int) RemoteViewsProto.CharSequence.Span.FLAGS: + flags = in.readInt(RemoteViewsProto.CharSequence.Span.FLAGS); + break; + case (int) RemoteViewsProto.CharSequence.Span.ABSOLUTE_SIZE: + final long asToken = in.start(RemoteViewsProto.CharSequence.Span.ABSOLUTE_SIZE); + what = createAbsoluteSizeSpanFromProto(in); + in.end(asToken); + break; + case (int) RemoteViewsProto.CharSequence.Span.ACCESSIBILITY_CLICKABLE: + final long acToken = in.start( + RemoteViewsProto.CharSequence.Span.ACCESSIBILITY_CLICKABLE); + what = createAccessibilityClickableSpanFromProto(in); + in.end(acToken); + break; + case (int) RemoteViewsProto.CharSequence.Span.ACCESSIBILITY_REPLACEMENT: + final long arToken = in.start( + RemoteViewsProto.CharSequence.Span.ACCESSIBILITY_REPLACEMENT); + what = createAccessibilityReplacementSpanFromProto(in); + in.end(arToken); + break; + case (int) RemoteViewsProto.CharSequence.Span.ACCESSIBILITY_URL: + final long auToken = in.start( + RemoteViewsProto.CharSequence.Span.ACCESSIBILITY_URL); + what = createAccessibilityURLSpanFromProto(in); + in.end(auToken); + break; + case (int) RemoteViewsProto.CharSequence.Span.ALIGNMENT: + final long aToken = in.start(RemoteViewsProto.CharSequence.Span.ALIGNMENT); + what = createAlignmentSpanStandardFromProto(in); + in.end(aToken); + break; + case (int) RemoteViewsProto.CharSequence.Span.ANNOTATION: + final long annToken = in.start(RemoteViewsProto.CharSequence.Span.ANNOTATION); + what = createAnnotationFromProto(in); + in.end(annToken); + break; + case (int) RemoteViewsProto.CharSequence.Span.BACKGROUND_COLOR: + final long bcToken = in.start( + RemoteViewsProto.CharSequence.Span.BACKGROUND_COLOR); + what = createBackgroundColorSpanFromProto(in); + in.end(bcToken); + break; + case (int) RemoteViewsProto.CharSequence.Span.BULLET: + final long bToken = in.start(RemoteViewsProto.CharSequence.Span.BULLET); + what = createBulletSpanFromProto(in); + in.end(bToken); + break; + case (int) RemoteViewsProto.CharSequence.Span.EASY_EDIT: + final long eeToken = in.start(RemoteViewsProto.CharSequence.Span.EASY_EDIT); + what = createEasyEditSpanFromProto(in); + in.end(eeToken); + break; + case (int) RemoteViewsProto.CharSequence.Span.FOREGROUND_COLOR: + final long fcToken = in.start( + RemoteViewsProto.CharSequence.Span.FOREGROUND_COLOR); + what = createForegroundColorSpanFromProto(in); + in.end(fcToken); + break; + case (int) RemoteViewsProto.CharSequence.Span.LEADING_MARGIN: + final long lmToken = in.start( + RemoteViewsProto.CharSequence.Span.LEADING_MARGIN); + what = createLeadingMarginSpanStandardFromProto(in); + in.end(lmToken); + break; + case (int) RemoteViewsProto.CharSequence.Span.LINE_BACKGROUND: + final long lbToken = in.start( + RemoteViewsProto.CharSequence.Span.LINE_BACKGROUND); + what = createLineBackgroundSpanStandardFromProto(in); + in.end(lbToken); + break; + case (int) RemoteViewsProto.CharSequence.Span.LINE_BREAK: + if (!noBreakNoHyphenationSpan()) { + continue; + } + final long lbrToken = in.start(RemoteViewsProto.CharSequence.Span.LINE_BREAK); + what = createLineBreakConfigSpanFromProto(in); + in.end(lbrToken); + break; + case (int) RemoteViewsProto.CharSequence.Span.LINE_HEIGHT: + final long lhToken = in.start(RemoteViewsProto.CharSequence.Span.LINE_HEIGHT); + what = createLineHeightSpanStandardFromProto(in); + in.end(lhToken); + break; + case (int) RemoteViewsProto.CharSequence.Span.LOCALE: + final long lToken = in.start(RemoteViewsProto.CharSequence.Span.LOCALE); + what = createLocaleSpanFromProto(in); + in.end(lToken); + break; + case (int) RemoteViewsProto.CharSequence.Span.QUOTE: + final long qToken = in.start(RemoteViewsProto.CharSequence.Span.QUOTE); + what = createQuoteSpanFromProto(in); + in.end(qToken); + break; + case (int) RemoteViewsProto.CharSequence.Span.RELATIVE_SIZE: + final long rsToken = in.start(RemoteViewsProto.CharSequence.Span.RELATIVE_SIZE); + what = createRelativeSizeSpanFromProto(in); + in.end(rsToken); + break; + case (int) RemoteViewsProto.CharSequence.Span.SCALE_X: + final long sxToken = in.start(RemoteViewsProto.CharSequence.Span.SCALE_X); + what = createScaleXSpanFromProto(in); + in.end(sxToken); + break; + case (int) RemoteViewsProto.CharSequence.Span.SPELL_CHECK: + final long scToken = in.start(RemoteViewsProto.CharSequence.Span.SPELL_CHECK); + what = createSpellCheckSpanFromProto(in); + in.end(scToken); + break; + case (int) RemoteViewsProto.CharSequence.Span.STRIKETHROUGH: + final long stToken = in.start(RemoteViewsProto.CharSequence.Span.STRIKETHROUGH); + what = createStrikethroughSpanFromProto(in); + in.end(stToken); + break; + case (int) RemoteViewsProto.CharSequence.Span.STYLE: + final long sToken = in.start(RemoteViewsProto.CharSequence.Span.STYLE); + what = createStyleSpanFromProto(in); + in.end(sToken); + break; + case (int) RemoteViewsProto.CharSequence.Span.SUBSCRIPT: + final long suToken = in.start(RemoteViewsProto.CharSequence.Span.SUBSCRIPT); + what = createSubscriptSpanFromProto(in); + in.end(suToken); + break; + case (int) RemoteViewsProto.CharSequence.Span.SUGGESTION_RANGE: + final long srToken = in.start( + RemoteViewsProto.CharSequence.Span.SUGGESTION_RANGE); + what = createSuggestionRangeSpanFromProto(in); + in.end(srToken); + break; + case (int) RemoteViewsProto.CharSequence.Span.SUGGESTION: + final long sugToken = in.start(RemoteViewsProto.CharSequence.Span.SUGGESTION); + what = createSuggestionSpanFromProto(in); + in.end(sugToken); + break; + case (int) RemoteViewsProto.CharSequence.Span.SUPERSCRIPT: + final long supToken = in.start(RemoteViewsProto.CharSequence.Span.SUPERSCRIPT); + what = createSuperscriptSpanFromProto(in); + in.end(supToken); + break; + case (int) RemoteViewsProto.CharSequence.Span.TEXT_APPEARANCE: + final long taToken = in.start( + RemoteViewsProto.CharSequence.Span.TEXT_APPEARANCE); + what = createTextAppearanceSpanFromProto(in); + in.end(taToken); + break; + case (int) RemoteViewsProto.CharSequence.Span.TTS: + final long ttsToken = in.start(RemoteViewsProto.CharSequence.Span.TTS); + what = createTtsSpanFromProto(in); + in.end(ttsToken); + break; + case (int) RemoteViewsProto.CharSequence.Span.TYPEFACE: + final long tfToken = in.start(RemoteViewsProto.CharSequence.Span.TYPEFACE); + what = createTypefaceSpanFromProto(in); + in.end(tfToken); + break; + case (int) RemoteViewsProto.CharSequence.Span.UNDERLINE: + final long unToken = in.start(RemoteViewsProto.CharSequence.Span.UNDERLINE); + what = createUnderlineSpanFromProto(in); + in.end(unToken); + break; + case (int) RemoteViewsProto.CharSequence.Span.URL: + final long urlToken = in.start(RemoteViewsProto.CharSequence.Span.URL); + what = createURLSpanFromProto(in); + in.end(urlToken); + break; + default: + Log.w(TAG, "Unhandled field while reading CharSequence proto!\n" + + ProtoUtils.currentFieldToString(in)); + } + } + if (what == null) { + return; + } + builder.setSpan(what, start, end, flags); + } + + public static AbsoluteSizeSpan createAbsoluteSizeSpanFromProto(@NonNull ProtoInputStream in) + throws Exception { + int size = 0; + boolean dip = false; + while (in.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (in.getFieldNumber()) { + case (int) RemoteViewsProto.CharSequence.Span.AbsoluteSize.SIZE: + size = in.readInt(RemoteViewsProto.CharSequence.Span.AbsoluteSize.SIZE); + break; + case (int) RemoteViewsProto.CharSequence.Span.AbsoluteSize.DIP: + dip = in.readBoolean(RemoteViewsProto.CharSequence.Span.AbsoluteSize.DIP); + break; + default: + Log.w("AbsoluteSizeSpan", + "Unhandled field while reading AbsoluteSizeSpan " + "proto!\n" + + ProtoUtils.currentFieldToString(in)); + } + } + return new AbsoluteSizeSpan(size, dip); + } + + public static void writeAbsoluteSizeSpanToProto(@NonNull ProtoOutputStream out, + AbsoluteSizeSpan span) { + out.write(RemoteViewsProto.CharSequence.Span.AbsoluteSize.SIZE, span.getSize()); + out.write(RemoteViewsProto.CharSequence.Span.AbsoluteSize.DIP, span.getDip()); + } + + public static AccessibilityClickableSpan createAccessibilityClickableSpanFromProto( + @NonNull ProtoInputStream in) throws Exception { + int originalClickableSpanId = 0; + while (in.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (in.getFieldNumber()) { + case (int) RemoteViewsProto.CharSequence.Span + .AccessibilityClickable.ORIGINAL_CLICKABLE_SPAN_ID: + originalClickableSpanId = in.readInt( + RemoteViewsProto.CharSequence.Span + .AccessibilityClickable.ORIGINAL_CLICKABLE_SPAN_ID); + break; + default: + Log.w("AccessibilityClickable", + "Unhandled field while reading" + " AccessibilityClickableSpan proto!\n" + + ProtoUtils.currentFieldToString(in)); + } + } + return new AccessibilityClickableSpan(originalClickableSpanId); + } + + public static void writeAccessibilityClickableSpanToProto(@NonNull ProtoOutputStream out, + AccessibilityClickableSpan span) { + out.write( + RemoteViewsProto.CharSequence.Span + .AccessibilityClickable.ORIGINAL_CLICKABLE_SPAN_ID, + span.getOriginalClickableSpanId()); + } + + public static AccessibilityReplacementSpan createAccessibilityReplacementSpanFromProto( + @NonNull ProtoInputStream in) throws Exception { + CharSequence contentDescription = null; + while (in.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (in.getFieldNumber()) { + case (int) RemoteViewsProto.CharSequence.Span + .AccessibilityReplacement.CONTENT_DESCRIPTION: + final long token = in.start( + RemoteViewsProto.CharSequence.Span + .AccessibilityReplacement.CONTENT_DESCRIPTION); + contentDescription = createCharSequenceFromProto(in); + in.end(token); + break; + default: + Log.w("AccessibilityReplacemen", "Unhandled field while reading" + + " AccessibilityReplacementSpan proto!\n" + + ProtoUtils.currentFieldToString(in)); + } + } + return new AccessibilityReplacementSpan(contentDescription); + } + + public static void writeAccessibilityReplacementSpanToProto(@NonNull ProtoOutputStream out, + AccessibilityReplacementSpan span) { + final long token = out.start( + RemoteViewsProto.CharSequence.Span.AccessibilityReplacement.CONTENT_DESCRIPTION); + CharSequence description = span.getContentDescription(); + if (description != null) { + writeCharSequenceToProto(out, description); + } + out.end(token); + } + + public static AccessibilityURLSpan createAccessibilityURLSpanFromProto(ProtoInputStream in) + throws Exception { + String url = null; + while (in.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (in.getFieldNumber()) { + case (int) RemoteViewsProto.CharSequence.Span.AccessibilityUrl.URL: + url = in.readString(RemoteViewsProto.CharSequence.Span.AccessibilityUrl.URL); + break; + default: + Log.w("AccessibilityURLSpan", + "Unhandled field while reading AccessibilityURLSpan " + "proto!\n" + + ProtoUtils.currentFieldToString(in)); + } + } + return new AccessibilityURLSpan(new URLSpan(url)); + } + + public static void writeAccessibilityURLSpanToProto(@NonNull ProtoOutputStream out, + AccessibilityURLSpan span) { + out.write(RemoteViewsProto.CharSequence.Span.AccessibilityUrl.URL, span.getURL()); + } + + public static AlignmentSpan.Standard createAlignmentSpanStandardFromProto( + @NonNull ProtoInputStream in) throws Exception { + String alignment = null; + while (in.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (in.getFieldNumber()) { + case (int) RemoteViewsProto.CharSequence.Span.Alignment.ALIGNMENT: + alignment = in.readString( + RemoteViewsProto.CharSequence.Span.Alignment.ALIGNMENT); + break; + default: + Log.w("AlignmentSpan", + "Unhandled field while reading AlignmentSpan " + "proto!\n" + + ProtoUtils.currentFieldToString(in)); + } + } + return new AlignmentSpan.Standard(Layout.Alignment.valueOf(alignment)); + } + + public static void writeAlignmentSpanStandardToProto(@NonNull ProtoOutputStream out, + AlignmentSpan.Standard span) { + out.write(RemoteViewsProto.CharSequence.Span.Alignment.ALIGNMENT, + span.getAlignment().name()); + } + + public static Annotation createAnnotationFromProto(@NonNull ProtoInputStream in) + throws Exception { + String key = null; + String value = null; + while (in.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (in.getFieldNumber()) { + case (int) RemoteViewsProto.CharSequence.Span.Annotation.KEY: + key = in.readString(RemoteViewsProto.CharSequence.Span.Annotation.KEY); + break; + case (int) RemoteViewsProto.CharSequence.Span.Annotation.VALUE: + value = in.readString(RemoteViewsProto.CharSequence.Span.Annotation.VALUE); + break; + default: + Log.w("Annotation", "Unhandled field while reading" + " Annotation proto!\n" + + ProtoUtils.currentFieldToString(in)); + } + } + return new Annotation(key, value); + } + + public static void writeAnnotationToProto(@NonNull ProtoOutputStream out, Annotation span) { + out.write(RemoteViewsProto.CharSequence.Span.Annotation.KEY, span.getKey()); + out.write(RemoteViewsProto.CharSequence.Span.Annotation.VALUE, span.getValue()); + } + + public static BackgroundColorSpan createBackgroundColorSpanFromProto( + @NonNull ProtoInputStream in) throws Exception { + int color = 0; + while (in.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (in.getFieldNumber()) { + case (int) RemoteViewsProto.CharSequence.Span.BackgroundColor.COLOR: + color = in.readInt(RemoteViewsProto.CharSequence.Span.BackgroundColor.COLOR); + break; + default: + Log.w("BackgroundColorSpan", + "Unhandled field while reading" + " BackgroundColorSpan proto!\n" + + ProtoUtils.currentFieldToString(in)); + } + } + return new BackgroundColorSpan(color); + } + + public static void writeBackgroundColorSpanToProto(@NonNull ProtoOutputStream out, + BackgroundColorSpan span) { + out.write(RemoteViewsProto.CharSequence.Span.BackgroundColor.COLOR, + span.getBackgroundColor()); + } + + public static BulletSpan createBulletSpanFromProto(@NonNull ProtoInputStream in) + throws Exception { + int bulletRadius = 0; + int color = 0; + int gapWidth = 0; + boolean wantColor = false; + while (in.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (in.getFieldNumber()) { + case (int) RemoteViewsProto.CharSequence.Span.Bullet.BULLET_RADIUS: + bulletRadius = in.readInt( + RemoteViewsProto.CharSequence.Span.Bullet.BULLET_RADIUS); + break; + case (int) RemoteViewsProto.CharSequence.Span.Bullet.COLOR: + color = in.readInt(RemoteViewsProto.CharSequence.Span.Bullet.COLOR); + break; + case (int) RemoteViewsProto.CharSequence.Span.Bullet.GAP_WIDTH: + gapWidth = in.readInt(RemoteViewsProto.CharSequence.Span.Bullet.GAP_WIDTH); + break; + case (int) RemoteViewsProto.CharSequence.Span.Bullet.WANT_COLOR: + wantColor = in.readBoolean( + RemoteViewsProto.CharSequence.Span.Bullet.WANT_COLOR); + break; + default: + Log.w("BulletSpan", "Unhandled field while reading BulletSpan " + "proto!\n" + + ProtoUtils.currentFieldToString(in)); + } + } + return new BulletSpan(gapWidth, color, wantColor, bulletRadius); + } + + public static void writeBulletSpanToProto(@NonNull ProtoOutputStream out, BulletSpan span) { + out.write(RemoteViewsProto.CharSequence.Span.Bullet.BULLET_RADIUS, span.getBulletRadius()); + out.write(RemoteViewsProto.CharSequence.Span.Bullet.COLOR, span.getColor()); + out.write(RemoteViewsProto.CharSequence.Span.Bullet.GAP_WIDTH, span.getGapWidth()); + out.write(RemoteViewsProto.CharSequence.Span.Bullet.WANT_COLOR, span.getWantColor()); + } + + public static EasyEditSpan createEasyEditSpanFromProto(@NonNull ProtoInputStream in) + throws Exception { + return new EasyEditSpan(); + } + + public static void writeEasyEditSpanToProto(@NonNull ProtoOutputStream out, EasyEditSpan span) { + } + + public static ForegroundColorSpan createForegroundColorSpanFromProto( + @NonNull ProtoInputStream in) throws Exception { + int color = 0; + while (in.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (in.getFieldNumber()) { + case (int) RemoteViewsProto.CharSequence.Span.BackgroundColor.COLOR: + color = in.readInt(RemoteViewsProto.CharSequence.Span.BackgroundColor.COLOR); + break; + default: + Log.w("ForegroundColorSpan", + "Unhandled field while reading" + " ForegroundColorSpan proto!\n" + + ProtoUtils.currentFieldToString(in)); + } + } + return new ForegroundColorSpan(color); + } + + public static LeadingMarginSpan.Standard createLeadingMarginSpanStandardFromProto( + @NonNull ProtoInputStream in) throws Exception { + int first = 0; + int rest = 0; + while (in.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (in.getFieldNumber()) { + case (int) RemoteViewsProto.CharSequence.Span.LeadingMargin.FIRST: + first = in.readInt(RemoteViewsProto.CharSequence.Span.LeadingMargin.FIRST); + break; + case (int) RemoteViewsProto.CharSequence.Span.LeadingMargin.REST: + rest = in.readInt(RemoteViewsProto.CharSequence.Span.LeadingMargin.REST); + break; + default: + Log.w("LeadingMarginSpan", + "Unhandled field while reading LeadingMarginSpan" + "proto!\n" + + ProtoUtils.currentFieldToString(in)); + } + } + return new LeadingMarginSpan.Standard(first, rest); + } + + public static void writeLeadingMarginSpanStandardToProto(@NonNull ProtoOutputStream out, + LeadingMarginSpan.Standard span) { + out.write(RemoteViewsProto.CharSequence.Span.LeadingMargin.FIRST, + span.getLeadingMargin(/* first= */ true)); + out.write(RemoteViewsProto.CharSequence.Span.LeadingMargin.REST, + span.getLeadingMargin(/* first= */ false)); + } + + public static void writeForegroundColorSpanToProto(@NonNull ProtoOutputStream out, + ForegroundColorSpan span) { + out.write(RemoteViewsProto.CharSequence.Span.ForegroundColor.COLOR, + span.getForegroundColor()); + } + + public static LineBackgroundSpan.Standard createLineBackgroundSpanStandardFromProto( + @NonNull ProtoInputStream in) throws Exception { + int color = 0; + while (in.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (in.getFieldNumber()) { + case (int) RemoteViewsProto.CharSequence.Span.LineBackground.COLOR: + color = in.readInt(RemoteViewsProto.CharSequence.Span.LineBackground.COLOR); + break; + default: + Log.w("LineBackgroundSpan", + "Unhandled field while reading" + " LineBackgroundSpan proto!\n" + + ProtoUtils.currentFieldToString(in)); + } + } + return new LineBackgroundSpan.Standard(color); + } + + public static void writeLineBackgroundSpanStandardToProto(@NonNull ProtoOutputStream out, + LineBackgroundSpan.Standard span) { + out.write(RemoteViewsProto.CharSequence.Span.LineBackground.COLOR, span.getColor()); + } + + @FlaggedApi(FLAG_NO_BREAK_NO_HYPHENATION_SPAN) + public static LineBreakConfigSpan createLineBreakConfigSpanFromProto( + @NonNull ProtoInputStream in) throws Exception { + int lineBreakStyle = 0; + int lineBreakWordStyle = 0; + int hyphenation = 0; + while (in.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (in.getFieldNumber()) { + case (int) RemoteViewsProto.CharSequence.Span.LineBreak.LINE_BREAK_STYLE: + lineBreakStyle = in.readInt( + RemoteViewsProto.CharSequence.Span.LineBreak.LINE_BREAK_STYLE); + break; + case (int) RemoteViewsProto.CharSequence.Span.LineBreak.LINE_BREAK_WORD_STYLE: + lineBreakWordStyle = in.readInt( + RemoteViewsProto.CharSequence.Span.LineBreak.LINE_BREAK_WORD_STYLE); + break; + case (int) RemoteViewsProto.CharSequence.Span.LineBreak.HYPHENATION: + hyphenation = in.readInt( + RemoteViewsProto.CharSequence.Span.LineBreak.HYPHENATION); + break; + default: + Log.w("LineBreakConfigSpan", + "Unhandled field while reading " + "LineBreakConfigSpan proto!\n" + + ProtoUtils.currentFieldToString(in)); + } + } + LineBreakConfig lbc = new LineBreakConfig.Builder().setLineBreakStyle( + lineBreakStyle).setLineBreakWordStyle(lineBreakWordStyle).setHyphenation( + hyphenation).build(); + return new LineBreakConfigSpan(lbc); + } + + @FlaggedApi(FLAG_NO_BREAK_NO_HYPHENATION_SPAN) + public static void writeLineBreakConfigSpanToProto(@NonNull ProtoOutputStream out, + LineBreakConfigSpan span) { + out.write(RemoteViewsProto.CharSequence.Span.LineBreak.LINE_BREAK_STYLE, + span.getLineBreakConfig().getLineBreakStyle()); + out.write(RemoteViewsProto.CharSequence.Span.LineBreak.LINE_BREAK_WORD_STYLE, + span.getLineBreakConfig().getLineBreakWordStyle()); + out.write(RemoteViewsProto.CharSequence.Span.LineBreak.HYPHENATION, + span.getLineBreakConfig().getHyphenation()); + } + + public static LineHeightSpan.Standard createLineHeightSpanStandardFromProto( + @NonNull ProtoInputStream in) throws Exception { + int height = 0; + while (in.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (in.getFieldNumber()) { + case (int) RemoteViewsProto.CharSequence.Span.LineHeight.HEIGHT: + height = in.readInt(RemoteViewsProto.CharSequence.Span.LineHeight.HEIGHT); + break; + default: + Log.w("LineHeightSpan.Standard", + "Unhandled field while reading" + " LineHeightSpan.Standard proto!\n" + + ProtoUtils.currentFieldToString(in)); + } + } + return new LineHeightSpan.Standard(height); + } + + public static void writeLineHeightSpanStandardToProto(@NonNull ProtoOutputStream out, + LineHeightSpan.Standard span) { + out.write(RemoteViewsProto.CharSequence.Span.LineHeight.HEIGHT, span.getHeight()); + } + + public static LocaleSpan createLocaleSpanFromProto(@NonNull ProtoInputStream in) + throws Exception { + String languageTags = null; + while (in.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (in.getFieldNumber()) { + case (int) RemoteViewsProto.CharSequence.Span.Locale.LANGUAGE_TAGS: + languageTags = in.readString( + RemoteViewsProto.CharSequence.Span.Locale.LANGUAGE_TAGS); + break; + default: + Log.w("LocaleSpan", "Unhandled field while reading" + " LocaleSpan proto!\n" + + ProtoUtils.currentFieldToString(in)); + } + } + return new LocaleSpan(LocaleList.forLanguageTags(languageTags)); + } + + public static void writeLocaleSpanToProto(@NonNull ProtoOutputStream out, LocaleSpan span) { + out.write(RemoteViewsProto.CharSequence.Span.Locale.LANGUAGE_TAGS, + span.getLocales().toLanguageTags()); + } + + public static QuoteSpan createQuoteSpanFromProto(@NonNull ProtoInputStream in) + throws Exception { + int color = 0; + int stripeWidth = 0; + int gapWidth = 0; + while (in.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (in.getFieldNumber()) { + case (int) RemoteViewsProto.CharSequence.Span.Quote.COLOR: + color = in.readInt(RemoteViewsProto.CharSequence.Span.Quote.COLOR); + break; + case (int) RemoteViewsProto.CharSequence.Span.Quote.STRIPE_WIDTH: + stripeWidth = in.readInt(RemoteViewsProto.CharSequence.Span.Quote.STRIPE_WIDTH); + break; + case (int) RemoteViewsProto.CharSequence.Span.Quote.GAP_WIDTH: + gapWidth = in.readInt(RemoteViewsProto.CharSequence.Span.Quote.GAP_WIDTH); + break; + default: + Log.w("QuoteSpan", "Unhandled field while reading QuoteSpan " + "proto!\n" + + ProtoUtils.currentFieldToString(in)); + } + } + return new QuoteSpan(color, stripeWidth, gapWidth); + } + + public static void writeQuoteSpanToProto(@NonNull ProtoOutputStream out, QuoteSpan span) { + out.write(RemoteViewsProto.CharSequence.Span.Quote.COLOR, span.getColor()); + out.write(RemoteViewsProto.CharSequence.Span.Quote.STRIPE_WIDTH, span.getStripeWidth()); + out.write(RemoteViewsProto.CharSequence.Span.Quote.GAP_WIDTH, span.getGapWidth()); + } + + public static RelativeSizeSpan createRelativeSizeSpanFromProto(@NonNull ProtoInputStream in) + throws Exception { + float proportion = 0; + while (in.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (in.getFieldNumber()) { + case (int) RemoteViewsProto.CharSequence.Span.RelativeSize.PROPORTION: + proportion = in.readFloat( + RemoteViewsProto.CharSequence.Span.RelativeSize.PROPORTION); + break; + default: + Log.w("RelativeSizeSpan", + "Unhandled field while reading" + " RelativeSizeSpan proto!\n" + + ProtoUtils.currentFieldToString(in)); + } + } + return new RelativeSizeSpan(proportion); + } + + public static void writeRelativeSizeSpanToProto(@NonNull ProtoOutputStream out, + RelativeSizeSpan span) { + out.write(RemoteViewsProto.CharSequence.Span.RelativeSize.PROPORTION, span.getSizeChange()); + } + + public static ScaleXSpan createScaleXSpanFromProto(@NonNull ProtoInputStream in) + throws Exception { + float proportion = 0f; + while (in.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (in.getFieldNumber()) { + case (int) RemoteViewsProto.CharSequence.Span.ScaleX.PROPORTION: + proportion = in.readFloat(RemoteViewsProto.CharSequence.Span.ScaleX.PROPORTION); + break; + default: + Log.w("ScaleXSpan", "Unhandled field while reading" + " ScaleXSpan proto!\n" + + ProtoUtils.currentFieldToString(in)); + } + } + return new ScaleXSpan(proportion); + } + + public static void writeScaleXSpanToProto(@NonNull ProtoOutputStream out, ScaleXSpan span) { + out.write(RemoteViewsProto.CharSequence.Span.ScaleX.PROPORTION, span.getScaleX()); + } + + public static SpellCheckSpan createSpellCheckSpanFromProto(@NonNull ProtoInputStream in) { + return new SpellCheckSpan(); + } + + public static void writeSpellCheckSpanToProto(@NonNull ProtoOutputStream out, + SpellCheckSpan span) { + } + + public static StrikethroughSpan createStrikethroughSpanFromProto(@NonNull ProtoInputStream in) { + return new StrikethroughSpan(); + } + + public static void writeStrikethroughSpanToProto(@NonNull ProtoOutputStream out, + StrikethroughSpan span) { + } + + public static StyleSpan createStyleSpanFromProto(@NonNull ProtoInputStream in) + throws Exception { + int style = 0; + int fontWeightAdjustment = 0; + while (in.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (in.getFieldNumber()) { + case (int) RemoteViewsProto.CharSequence.Span.Style.STYLE: + style = in.readInt(RemoteViewsProto.CharSequence.Span.Style.STYLE); + break; + case (int) RemoteViewsProto.CharSequence.Span.Style.FONT_WEIGHT_ADJUSTMENT: + fontWeightAdjustment = in.readInt( + RemoteViewsProto.CharSequence.Span.Style.FONT_WEIGHT_ADJUSTMENT); + break; + default: + Log.w("StyleSpan", "Unhandled field while reading StyleSpan " + "proto!\n" + + ProtoUtils.currentFieldToString(in)); + } + } + return new StyleSpan(style, fontWeightAdjustment); + } + + public static void writeStyleSpanToProto(@NonNull ProtoOutputStream out, StyleSpan span) { + out.write(RemoteViewsProto.CharSequence.Span.Style.STYLE, span.getStyle()); + out.write(RemoteViewsProto.CharSequence.Span.Style.FONT_WEIGHT_ADJUSTMENT, + span.getFontWeightAdjustment()); + } + + public static SubscriptSpan createSubscriptSpanFromProto(@NonNull ProtoInputStream in) { + return new SubscriptSpan(); + } + + public static void writeSubscriptSpanToProto(@NonNull ProtoOutputStream out, + SubscriptSpan span) { + } + + public static SuggestionRangeSpan createSuggestionRangeSpanFromProto( + @NonNull ProtoInputStream in) throws Exception { + int backgroundColor = 0; + while (in.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (in.getFieldNumber()) { + case (int) RemoteViewsProto.CharSequence.Span.SuggestionRange.BACKGROUND_COLOR: + backgroundColor = in.readInt( + RemoteViewsProto.CharSequence.Span.SuggestionRange.BACKGROUND_COLOR); + break; + default: + Log.w("SuggestionRangeSpan", + "Unhandled field while reading" + " SuggestionRangeSpan proto!\n" + + ProtoUtils.currentFieldToString(in)); + } + } + SuggestionRangeSpan span = new SuggestionRangeSpan(); + span.setBackgroundColor(backgroundColor); + return span; + } + + public static void writeSuggestionRangeSpanToProto(@NonNull ProtoOutputStream out, + SuggestionRangeSpan span) { + out.write(RemoteViewsProto.CharSequence.Span.SuggestionRange.BACKGROUND_COLOR, + span.getBackgroundColor()); + } + + public static SuggestionSpan createSuggestionSpanFromProto(@NonNull ProtoInputStream in) + throws Exception { + List<String> suggestions = new ArrayList<>(); + int flags = 0; + String localeStringForCompatibility = null; + String languageTag = null; + int hashCode = 0; + int easyCorrectUnderlineColor = 0; + float easyCorrectUnderlineThickness = 0; + int misspelledUnderlineColor = 0; + float misspelledUnderlineThickness = 0; + int autoCorrectionUnderlineColor = 0; + float autoCorrectionUnderlineThickness = 0; + int grammarErrorUnderlineColor = 0; + float grammarErrorUnderlineThickness = 0; + while (in.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (in.getFieldNumber()) { + case (int) RemoteViewsProto.CharSequence.Span.Suggestion.SUGGESTIONS: + suggestions.add(in.readString( + RemoteViewsProto.CharSequence.Span.Suggestion.SUGGESTIONS)); + break; + case (int) RemoteViewsProto.CharSequence.Span.Suggestion.FLAGS: + flags = in.readInt(RemoteViewsProto.CharSequence.Span.Suggestion.FLAGS); + break; + case (int) RemoteViewsProto.CharSequence.Span + .Suggestion.LOCALE_STRING_FOR_COMPATIBILITY: + localeStringForCompatibility = in.readString( + RemoteViewsProto.CharSequence.Span + .Suggestion.LOCALE_STRING_FOR_COMPATIBILITY); + break; + case (int) RemoteViewsProto.CharSequence.Span.Suggestion.LANGUAGE_TAG: + languageTag = in.readString( + RemoteViewsProto.CharSequence.Span.Suggestion.LANGUAGE_TAG); + break; + case (int) RemoteViewsProto.CharSequence.Span.Suggestion.HASH_CODE: + hashCode = in.readInt(RemoteViewsProto.CharSequence.Span.Suggestion.HASH_CODE); + break; + case (int) RemoteViewsProto.CharSequence.Span + .Suggestion.EASY_CORRECT_UNDERLINE_COLOR: + easyCorrectUnderlineColor = in.readInt( + RemoteViewsProto.CharSequence.Span + .Suggestion.EASY_CORRECT_UNDERLINE_COLOR); + break; + case (int) RemoteViewsProto.CharSequence.Span + .Suggestion.EASY_CORRECT_UNDERLINE_THICKNESS: + easyCorrectUnderlineThickness = in.readFloat( + RemoteViewsProto.CharSequence.Span + .Suggestion.EASY_CORRECT_UNDERLINE_THICKNESS); + break; + case (int) RemoteViewsProto.CharSequence.Span + .Suggestion.MISSPELLED_UNDERLINE_COLOR: + misspelledUnderlineColor = in.readInt( + RemoteViewsProto.CharSequence.Span + .Suggestion.MISSPELLED_UNDERLINE_COLOR); + break; + case (int) RemoteViewsProto.CharSequence.Span + .Suggestion.MISSPELLED_UNDERLINE_THICKNESS: + misspelledUnderlineThickness = in.readFloat( + RemoteViewsProto.CharSequence.Span + .Suggestion.MISSPELLED_UNDERLINE_THICKNESS); + break; + case (int) RemoteViewsProto.CharSequence.Span + .Suggestion.AUTO_CORRECTION_UNDERLINE_COLOR: + autoCorrectionUnderlineColor = in.readInt( + RemoteViewsProto.CharSequence.Span + .Suggestion.AUTO_CORRECTION_UNDERLINE_COLOR); + break; + case (int) RemoteViewsProto.CharSequence.Span + .Suggestion.AUTO_CORRECTION_UNDERLINE_THICKNESS: + autoCorrectionUnderlineThickness = in.readFloat( + RemoteViewsProto.CharSequence.Span + .Suggestion.AUTO_CORRECTION_UNDERLINE_THICKNESS); + break; + case (int) RemoteViewsProto.CharSequence.Span + .Suggestion.GRAMMAR_ERROR_UNDERLINE_COLOR: + grammarErrorUnderlineColor = in.readInt( + RemoteViewsProto.CharSequence.Span + .Suggestion.GRAMMAR_ERROR_UNDERLINE_COLOR); + break; + case (int) RemoteViewsProto.CharSequence.Span + .Suggestion.GRAMMAR_ERROR_UNDERLINE_THICKNESS: + grammarErrorUnderlineThickness = in.readFloat( + RemoteViewsProto.CharSequence.Span + .Suggestion.GRAMMAR_ERROR_UNDERLINE_THICKNESS); + break; + default: + Log.w("SuggestionSpan", + "Unhandled field while reading SuggestionSpan " + "proto!\n" + + ProtoUtils.currentFieldToString(in)); + } + } + String[] suggestionsArray = new String[suggestions.size()]; + suggestions.toArray(suggestionsArray); + return new SuggestionSpan(suggestionsArray, flags, localeStringForCompatibility, + languageTag, hashCode, easyCorrectUnderlineColor, easyCorrectUnderlineThickness, + misspelledUnderlineColor, misspelledUnderlineThickness, + autoCorrectionUnderlineColor, autoCorrectionUnderlineThickness, + grammarErrorUnderlineColor, grammarErrorUnderlineThickness); + } + + public static void writeSuggestionSpanToProto(@NonNull ProtoOutputStream out, + SuggestionSpan span) { + for (String suggestion : span.getSuggestions()) { + out.write(RemoteViewsProto.CharSequence.Span.Suggestion.SUGGESTIONS, suggestion); + } + out.write(RemoteViewsProto.CharSequence.Span.Suggestion.FLAGS, span.getFlags()); + out.write(RemoteViewsProto.CharSequence.Span.Suggestion.LOCALE_STRING_FOR_COMPATIBILITY, + span.getLocale()); + if (span.getLocaleObject() != null) { + out.write(RemoteViewsProto.CharSequence.Span.Suggestion.LANGUAGE_TAG, + span.getLocaleObject().toLanguageTag()); + } + out.write(RemoteViewsProto.CharSequence.Span.Suggestion.HASH_CODE, span.hashCode()); + out.write(RemoteViewsProto.CharSequence.Span.Suggestion.EASY_CORRECT_UNDERLINE_COLOR, + span.getEasyCorrectUnderlineColor()); + out.write(RemoteViewsProto.CharSequence.Span.Suggestion.EASY_CORRECT_UNDERLINE_THICKNESS, + span.getEasyCorrectUnderlineThickness()); + out.write(RemoteViewsProto.CharSequence.Span.Suggestion.MISSPELLED_UNDERLINE_COLOR, + span.getMisspelledUnderlineColor()); + out.write(RemoteViewsProto.CharSequence.Span.Suggestion.MISSPELLED_UNDERLINE_THICKNESS, + span.getMisspelledUnderlineThickness()); + out.write(RemoteViewsProto.CharSequence.Span.Suggestion.AUTO_CORRECTION_UNDERLINE_COLOR, + span.getAutoCorrectionUnderlineColor()); + out.write(RemoteViewsProto.CharSequence.Span.Suggestion.AUTO_CORRECTION_UNDERLINE_THICKNESS, + span.getAutoCorrectionUnderlineThickness()); + out.write(RemoteViewsProto.CharSequence.Span.Suggestion.GRAMMAR_ERROR_UNDERLINE_COLOR, + span.getGrammarErrorUnderlineColor()); + out.write(RemoteViewsProto.CharSequence.Span.Suggestion.GRAMMAR_ERROR_UNDERLINE_THICKNESS, + span.getGrammarErrorUnderlineThickness()); + } + + public static SuperscriptSpan createSuperscriptSpanFromProto(@NonNull ProtoInputStream in) { + return new SuperscriptSpan(); + } + + public static void writeSuperscriptSpanToProto(@NonNull ProtoOutputStream out, + SuperscriptSpan span) { + } + + public static TextAppearanceSpan createTextAppearanceSpanFromProto(@NonNull ProtoInputStream in) + throws Exception { + String familyName = null; + int style = 0; + int textSize = 0; + ColorStateList textColor = null; + ColorStateList textColorLink = null; + int textFontWeight = 0; + LocaleList textLocales = null; + float shadowRadius = 0F; + float shadowDx = 0F; + float shadowDy = 0F; + int shadowColor = 0; + boolean hasElegantTextHeight = false; + boolean elegantTextHeight = false; + boolean hasLetterSpacing = false; + float letterSpacing = 0F; + String fontFeatureSettings = null; + String fontVariationSettings = null; + while (in.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (in.getFieldNumber()) { + case (int) RemoteViewsProto.CharSequence.Span.TextAppearance.FAMILY_NAME: + familyName = in.readString( + RemoteViewsProto.CharSequence.Span.TextAppearance.FAMILY_NAME); + break; + case (int) RemoteViewsProto.CharSequence.Span.TextAppearance.STYLE: + style = in.readInt(RemoteViewsProto.CharSequence.Span.TextAppearance.STYLE); + break; + case (int) RemoteViewsProto.CharSequence.Span.TextAppearance.TEXT_SIZE: + textSize = in.readInt( + RemoteViewsProto.CharSequence.Span.TextAppearance.TEXT_SIZE); + break; + case (int) RemoteViewsProto.CharSequence.Span.TextAppearance.TEXT_COLOR: + final long textColorToken = in.start( + RemoteViewsProto.CharSequence.Span.TextAppearance.TEXT_COLOR); + textColor = ColorStateList.createFromProto(in); + in.end(textColorToken); + break; + case (int) RemoteViewsProto.CharSequence.Span.TextAppearance.TEXT_COLOR_LINK: + final long textColorLinkToken = in.start( + RemoteViewsProto.CharSequence.Span.TextAppearance.TEXT_COLOR_LINK); + textColorLink = ColorStateList.createFromProto(in); + in.end(textColorLinkToken); + break; + case (int) RemoteViewsProto.CharSequence.Span.TextAppearance.TEXT_FONT_WEIGHT: + textFontWeight = in.readInt( + RemoteViewsProto.CharSequence.Span.TextAppearance.TEXT_FONT_WEIGHT); + break; + case (int) RemoteViewsProto.CharSequence.Span.TextAppearance.TEXT_LOCALE: + textLocales = LocaleList.forLanguageTags(in.readString( + RemoteViewsProto.CharSequence.Span.TextAppearance.TEXT_LOCALE)); + break; + case (int) RemoteViewsProto.CharSequence.Span.TextAppearance.SHADOW_RADIUS: + shadowRadius = in.readFloat( + RemoteViewsProto.CharSequence.Span.TextAppearance.SHADOW_RADIUS); + break; + case (int) RemoteViewsProto.CharSequence.Span.TextAppearance.SHADOW_DX: + shadowDx = in.readFloat( + RemoteViewsProto.CharSequence.Span.TextAppearance.SHADOW_DX); + break; + case (int) RemoteViewsProto.CharSequence.Span.TextAppearance.SHADOW_DY: + shadowDy = in.readFloat( + RemoteViewsProto.CharSequence.Span.TextAppearance.SHADOW_DY); + break; + case (int) RemoteViewsProto.CharSequence.Span.TextAppearance.SHADOW_COLOR: + shadowColor = in.readInt( + RemoteViewsProto.CharSequence.Span.TextAppearance.SHADOW_COLOR); + break; + case (int) RemoteViewsProto.CharSequence.Span + .TextAppearance.HAS_ELEGANT_TEXT_HEIGHT_FIELD: + hasElegantTextHeight = in.readBoolean( + RemoteViewsProto.CharSequence.Span + .TextAppearance.HAS_ELEGANT_TEXT_HEIGHT_FIELD); + break; + case (int) RemoteViewsProto.CharSequence.Span.TextAppearance.ELEGANT_TEXT_HEIGHT: + elegantTextHeight = in.readBoolean( + RemoteViewsProto.CharSequence.Span.TextAppearance.ELEGANT_TEXT_HEIGHT); + break; + case (int) RemoteViewsProto.CharSequence.Span + .TextAppearance.HAS_LETTER_SPACING_FIELD: + hasLetterSpacing = in.readBoolean( + RemoteViewsProto.CharSequence.Span + .TextAppearance.HAS_LETTER_SPACING_FIELD); + break; + case (int) RemoteViewsProto.CharSequence.Span.TextAppearance.LETTER_SPACING: + letterSpacing = in.readFloat( + RemoteViewsProto.CharSequence.Span.TextAppearance.LETTER_SPACING); + break; + case (int) RemoteViewsProto.CharSequence.Span.TextAppearance.FONT_FEATURE_SETTINGS: + fontFeatureSettings = in.readString( + RemoteViewsProto.CharSequence.Span + .TextAppearance.FONT_FEATURE_SETTINGS); + break; + case (int) RemoteViewsProto.CharSequence.Span + .TextAppearance.FONT_VARIATION_SETTINGS: + fontVariationSettings = in.readString( + RemoteViewsProto.CharSequence.Span + .TextAppearance.FONT_VARIATION_SETTINGS); + break; + default: + Log.w("TextAppearanceSpan", + "Unhandled field while reading TextAppearanceSpan " + "proto!\n" + + ProtoUtils.currentFieldToString(in)); + } + } + return new TextAppearanceSpan(familyName, style, textSize, textColor, textColorLink, + /* typeface= */ null, textFontWeight, textLocales, shadowRadius, shadowDx, shadowDy, + shadowColor, hasElegantTextHeight, elegantTextHeight, hasLetterSpacing, + letterSpacing, fontFeatureSettings, fontVariationSettings); + } + + public static void writeTextAppearanceSpanToProto(@NonNull ProtoOutputStream out, + TextAppearanceSpan span) { + out.write(RemoteViewsProto.CharSequence.Span.TextAppearance.FAMILY_NAME, span.getFamily()); + out.write(RemoteViewsProto.CharSequence.Span.TextAppearance.STYLE, span.getTextStyle()); + out.write(RemoteViewsProto.CharSequence.Span.TextAppearance.TEXT_SIZE, span.getTextSize()); + out.write(RemoteViewsProto.CharSequence.Span.TextAppearance.TEXT_FONT_WEIGHT, + span.getTextFontWeight()); + if (span.getTextLocales() != null) { + out.write(RemoteViewsProto.CharSequence.Span.TextAppearance.TEXT_LOCALE, + span.getTextLocales().toLanguageTags()); + } + out.write(RemoteViewsProto.CharSequence.Span.TextAppearance.SHADOW_RADIUS, + span.getShadowRadius()); + out.write(RemoteViewsProto.CharSequence.Span.TextAppearance.SHADOW_DX, span.getShadowDx()); + out.write(RemoteViewsProto.CharSequence.Span.TextAppearance.SHADOW_DY, span.getShadowDy()); + out.write(RemoteViewsProto.CharSequence.Span.TextAppearance.SHADOW_COLOR, + span.getShadowColor()); + out.write(RemoteViewsProto.CharSequence.Span.TextAppearance.HAS_ELEGANT_TEXT_HEIGHT_FIELD, + span.hasElegantTextHeight()); + out.write(RemoteViewsProto.CharSequence.Span.TextAppearance.ELEGANT_TEXT_HEIGHT, + span.isElegantTextHeight()); + out.write(RemoteViewsProto.CharSequence.Span.TextAppearance.HAS_LETTER_SPACING_FIELD, + span.hasLetterSpacing()); + out.write(RemoteViewsProto.CharSequence.Span.TextAppearance.LETTER_SPACING, + span.getLetterSpacing()); + out.write(RemoteViewsProto.CharSequence.Span.TextAppearance.FONT_FEATURE_SETTINGS, + span.getFontFeatureSettings()); + out.write(RemoteViewsProto.CharSequence.Span.TextAppearance.FONT_VARIATION_SETTINGS, + span.getFontVariationSettings()); + if (span.getTextColor() != null) { + final long textColorToken = out.start( + RemoteViewsProto.CharSequence.Span.TextAppearance.TEXT_COLOR); + span.getTextColor().writeToProto(out); + out.end(textColorToken); + } + if (span.getLinkTextColor() != null) { + final long textColorLinkToken = out.start( + RemoteViewsProto.CharSequence.Span.TextAppearance.TEXT_COLOR_LINK); + span.getLinkTextColor().writeToProto(out); + out.end(textColorLinkToken); + } + } + + public static TtsSpan createTtsSpanFromProto(@NonNull ProtoInputStream in) throws Exception { + String type = null; + PersistableBundle args = null; + while (in.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (in.getFieldNumber()) { + case (int) RemoteViewsProto.CharSequence.Span.Tts.TYPE: + type = in.readString(RemoteViewsProto.CharSequence.Span.Tts.TYPE); + break; + case (int) RemoteViewsProto.CharSequence.Span.Tts.ARGS: + final byte[] data = in.readString( + RemoteViewsProto.CharSequence.Span.Tts.ARGS).getBytes(); + args = PersistableBundle.readFromStream(new ByteArrayInputStream(data)); + break; + default: + Log.w("TtsSpan", "Unhandled field while reading TtsSpan " + "proto!\n" + + ProtoUtils.currentFieldToString(in)); + } + } + return new TtsSpan(type, args); + } + + public static void writeTtsSpanToProto(@NonNull ProtoOutputStream out, TtsSpan span) { + out.write(RemoteViewsProto.CharSequence.Span.Tts.TYPE, span.getType()); + if (span.getArgs() != null) { + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + try { + span.getArgs().writeToStream(buf); + } catch (IOException e) { + throw new RuntimeException(e); + } + out.write(RemoteViewsProto.CharSequence.Span.Tts.ARGS, buf.toString(UTF_8)); + } + } + + public static TypefaceSpan createTypefaceSpanFromProto(@NonNull ProtoInputStream in) + throws Exception { + String family = null; + while (in.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (in.getFieldNumber()) { + case (int) RemoteViewsProto.CharSequence.Span.Typeface.FAMILY: + family = in.readString(RemoteViewsProto.CharSequence.Span.Typeface.FAMILY); + break; + default: + Log.w("TypefaceSpan", "Unhandled field while reading" + " TypefaceSpan proto!\n" + + ProtoUtils.currentFieldToString(in)); + } + } + return new TypefaceSpan(family); + } + + public static void writeTypefaceSpanToProto(@NonNull ProtoOutputStream out, TypefaceSpan span) { + out.write(RemoteViewsProto.CharSequence.Span.Typeface.FAMILY, span.getFamily()); + } + + public static URLSpan createURLSpanFromProto(@NonNull ProtoInputStream in) throws Exception { + String url = null; + while (in.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (in.getFieldNumber()) { + case (int) RemoteViewsProto.CharSequence.Span.Url.URL: + url = in.readString(RemoteViewsProto.CharSequence.Span.Url.URL); + break; + default: + Log.w("URLSpan", "Unhandled field while reading" + " URLSpan proto!\n" + + ProtoUtils.currentFieldToString(in)); + } + } + return new URLSpan(url); + } + + public static void writeURLSpanToProto(@NonNull ProtoOutputStream out, URLSpan span) { + out.write(RemoteViewsProto.CharSequence.Span.Url.URL, span.getURL()); + } + + public static UnderlineSpan createUnderlineSpanFromProto(@NonNull ProtoInputStream in) { + return new UnderlineSpan(); + } + + public static void writeUnderlineSpanToProto(@NonNull ProtoOutputStream out, + UnderlineSpan span) { + } } 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/TaskSnapshot.java b/core/java/android/window/TaskSnapshot.java index f0144cbf0f4a..20d1b3bd12ae 100644 --- a/core/java/android/window/TaskSnapshot.java +++ b/core/java/android/window/TaskSnapshot.java @@ -72,6 +72,7 @@ public class TaskSnapshot implements Parcelable { int mAppearance; private final boolean mIsTranslucent; private final boolean mHasImeSurface; + private final int mUiMode; // Must be one of the named color spaces, otherwise, always use SRGB color space. private final ColorSpace mColorSpace; private int mInternalReferences; @@ -96,7 +97,7 @@ public class TaskSnapshot implements Parcelable { Rect contentInsets, Rect letterboxInsets, boolean isLowResolution, boolean isRealSnapshot, int windowingMode, @WindowInsetsController.Appearance int appearance, boolean isTranslucent, - boolean hasImeSurface) { + boolean hasImeSurface, int uiMode) { mId = id; mCaptureTime = captureTime; mTopActivityComponent = topActivityComponent; @@ -114,6 +115,7 @@ public class TaskSnapshot implements Parcelable { mAppearance = appearance; mIsTranslucent = isTranslucent; mHasImeSurface = hasImeSurface; + mUiMode = uiMode; } private TaskSnapshot(Parcel source) { @@ -136,6 +138,7 @@ public class TaskSnapshot implements Parcelable { mAppearance = source.readInt(); mIsTranslucent = source.readBoolean(); mHasImeSurface = source.readBoolean(); + mUiMode = source.readInt(); } /** @@ -273,6 +276,13 @@ public class TaskSnapshot implements Parcelable { return mAppearance; } + /** + * @return The uiMode the screenshot was taken in. + */ + public int getUiMode() { + return mUiMode; + } + @Override public int describeContents() { return 0; @@ -295,6 +305,7 @@ public class TaskSnapshot implements Parcelable { dest.writeInt(mAppearance); dest.writeBoolean(mIsTranslucent); dest.writeBoolean(mHasImeSurface); + dest.writeInt(mUiMode); } @Override @@ -318,7 +329,8 @@ public class TaskSnapshot implements Parcelable { + " mAppearance=" + mAppearance + " mIsTranslucent=" + mIsTranslucent + " mHasImeSurface=" + mHasImeSurface - + " mInternalReferences=" + mInternalReferences; + + " mInternalReferences=" + mInternalReferences + + " mUiMode=" + Integer.toHexString(mUiMode); } /** @@ -370,6 +382,7 @@ public class TaskSnapshot implements Parcelable { private boolean mIsTranslucent; private boolean mHasImeSurface; private int mPixelFormat; + private int mUiMode; public Builder setId(long id) { mId = id; @@ -452,6 +465,14 @@ public class TaskSnapshot implements Parcelable { return this; } + /** + * Sets the original uiMode while capture + */ + public Builder setUiMode(int uiMode) { + mUiMode = uiMode; + return this; + } + public int getPixelFormat() { return mPixelFormat; } @@ -481,7 +502,8 @@ public class TaskSnapshot implements Parcelable { mWindowingMode, mAppearance, mIsTranslucent, - mHasImeSurface); + mHasImeSurface, + mUiMode); } } 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/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig index 125a0b242df9..4f848175cd99 100644 --- a/core/java/android/window/flags/lse_desktop_experience.aconfig +++ b/core/java/android/window/flags/lse_desktop_experience.aconfig @@ -218,3 +218,10 @@ flag { description: "Enables desktop windowing app-to-web education" bug: "348205896" } + +flag { + name: "enable_minimize_button" + namespace: "lse_desktop_experience" + description: "Adds a minimize button the the caption bar" + bug: "356843241" +} 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..adbc59867c0c 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" @@ -68,16 +61,6 @@ flag { flag { namespace: "windowing_sdk" - name: "fix_pip_restore_to_overlay" - description: "Restore exit-pip activity back to ActivityEmbedding overlay" - bug: "297887697" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { - namespace: "windowing_sdk" name: "activity_embedding_animation_customization_flag" description: "Whether the animation customization feature for AE is enabled" bug: "293658614" @@ -135,3 +118,11 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + namespace: "windowing_sdk" + name: "ae_back_stack_restore" + description: "Allow the ActivityEmbedding back stack to be restored after process restarted" + bug: "289875940" + is_fixed_read_only: true +} diff --git a/core/java/com/android/internal/graphics/ColorUtils.java b/core/java/com/android/internal/graphics/ColorUtils.java index f72a5ca2bffb..f210741e070b 100644 --- a/core/java/com/android/internal/graphics/ColorUtils.java +++ b/core/java/com/android/internal/graphics/ColorUtils.java @@ -21,7 +21,7 @@ import android.annotation.FloatRange; import android.annotation.IntRange; import android.annotation.NonNull; import android.graphics.Color; - +import android.ravenwood.annotation.RavenwoodKeepWholeClass; import com.android.internal.graphics.cam.Cam; /** @@ -29,6 +29,7 @@ import com.android.internal.graphics.cam.Cam; * * A set of color-related utility methods, building upon those available in {@code Color}. */ +@RavenwoodKeepWholeClass public final class ColorUtils { private static final double XYZ_WHITE_REFERENCE_X = 95.047; @@ -696,4 +697,4 @@ public final class ColorUtils { double calculateContrast(int foreground, int background, int alpha); } -}
\ No newline at end of file +} diff --git a/core/java/com/android/internal/graphics/cam/Cam.java b/core/java/com/android/internal/graphics/cam/Cam.java index 1df85c389322..49fa37bd0ed3 100644 --- a/core/java/com/android/internal/graphics/cam/Cam.java +++ b/core/java/com/android/internal/graphics/cam/Cam.java @@ -18,6 +18,7 @@ package com.android.internal.graphics.cam; import android.annotation.NonNull; import android.annotation.Nullable; +import android.ravenwood.annotation.RavenwoodKeepWholeClass; import com.android.internal.graphics.ColorUtils; @@ -25,6 +26,7 @@ import com.android.internal.graphics.ColorUtils; * A color appearance model, based on CAM16, extended to use L* as the lightness dimension, and * coupled to a gamut mapping algorithm. Creates a color system, enables a digital design system. */ +@RavenwoodKeepWholeClass public class Cam { // The maximum difference between the requested L* and the L* returned. private static final float DL_MAX = 0.2f; diff --git a/core/java/com/android/internal/graphics/cam/CamUtils.java b/core/java/com/android/internal/graphics/cam/CamUtils.java index f54172996168..76fabc6529d8 100644 --- a/core/java/com/android/internal/graphics/cam/CamUtils.java +++ b/core/java/com/android/internal/graphics/cam/CamUtils.java @@ -19,6 +19,7 @@ package com.android.internal.graphics.cam; import android.annotation.NonNull; import android.graphics.Color; +import android.ravenwood.annotation.RavenwoodKeepWholeClass; import com.android.internal.graphics.ColorUtils; @@ -45,6 +46,7 @@ import com.android.internal.graphics.ColorUtils; * consistent, and reasonably good. It worked." - Fairchild, Color Models and Systems: Handbook of * Color Psychology, 2015 */ +@RavenwoodKeepWholeClass public final class CamUtils { private CamUtils() { } diff --git a/core/java/com/android/internal/graphics/cam/Frame.java b/core/java/com/android/internal/graphics/cam/Frame.java index 0ac7cbc2f60e..c419fabc9d89 100644 --- a/core/java/com/android/internal/graphics/cam/Frame.java +++ b/core/java/com/android/internal/graphics/cam/Frame.java @@ -17,6 +17,7 @@ package com.android.internal.graphics.cam; import android.annotation.NonNull; +import android.ravenwood.annotation.RavenwoodKeepWholeClass; import android.util.MathUtils; import com.android.internal.annotations.VisibleForTesting; @@ -33,6 +34,7 @@ import com.android.internal.annotations.VisibleForTesting; * number of calculations during the color => CAM conversion process that depend only on the viewing * conditions. Caching those calculations in a Frame instance saves a significant amount of time. */ +@RavenwoodKeepWholeClass public final class Frame { // Standard viewing conditions assumed in RGB specification - Stokes, Anderson, Chandrasekar, // Motta - A Standard Default Color Space for the Internet: sRGB, 1996. diff --git a/core/java/com/android/internal/graphics/cam/HctSolver.java b/core/java/com/android/internal/graphics/cam/HctSolver.java index d7a869185cd7..6e558e7809a5 100644 --- a/core/java/com/android/internal/graphics/cam/HctSolver.java +++ b/core/java/com/android/internal/graphics/cam/HctSolver.java @@ -16,6 +16,8 @@ package com.android.internal.graphics.cam; +import android.ravenwood.annotation.RavenwoodKeepWholeClass; + /** * An efficient algorithm for determining the closest sRGB color to a set of HCT coordinates, * based on geometrical insights for finding intersections in linear RGB, CAM16, and L*a*b*. @@ -24,6 +26,7 @@ package com.android.internal.graphics.cam; * Copied from //java/com/google/ux/material/libmonet/hct on May 22 2022. * ColorUtils/MathUtils functions that were required were added to CamUtils. */ +@RavenwoodKeepWholeClass public class HctSolver { private HctSolver() {} 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/jank/Cuj.java b/core/java/com/android/internal/jank/Cuj.java index 69d1cb34005d..7bfb8003fd18 100644 --- a/core/java/com/android/internal/jank/Cuj.java +++ b/core/java/com/android/internal/jank/Cuj.java @@ -210,8 +210,16 @@ public class Cuj { */ public static final int CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE = 116; + /** + * Track interaction of exiting desktop mode on closing the last window. + * + * <p>Tracking starts when the last window is closed and finishes when the animation to exit + * desktop mode ends. + */ + public static final int CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE = 117; + // When adding a CUJ, update this and make sure to also update CUJ_TO_STATSD_INTERACTION_TYPE. - @VisibleForTesting static final int LAST_CUJ = CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE; + @VisibleForTesting static final int LAST_CUJ = CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE; /** @hide */ @IntDef({ @@ -319,7 +327,8 @@ public class Cuj { CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_OPEN, CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE, CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH, - CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE + CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE, + CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE }) @Retention(RetentionPolicy.SOURCE) public @interface CujType {} @@ -438,6 +447,7 @@ public class Cuj { CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE; CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH; CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE; + CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE; } private Cuj() { @@ -666,6 +676,8 @@ public class Cuj { return "LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH"; case CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE: return "DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE"; + case CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE: + return "DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE"; } return "UNKNOWN"; } diff --git a/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java b/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java index b316a01c335a..12d326486e77 100644 --- a/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java +++ b/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java @@ -727,11 +727,11 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, this.usesSdkLibraries = CollectionUtils.add(this.usesSdkLibraries, TextUtils.safeIntern(libraryName)); this.usesSdkLibrariesVersionsMajor = ArrayUtils.appendLong( - this.usesSdkLibrariesVersionsMajor, versionMajor, true); + this.usesSdkLibrariesVersionsMajor, versionMajor, /* allowDuplicates= */ true); this.usesSdkLibrariesCertDigests = ArrayUtils.appendElement(String[].class, - this.usesSdkLibrariesCertDigests, certSha256Digests, true); - this.usesSdkLibrariesOptional = ArrayUtils.appendBoolean(this.usesSdkLibrariesOptional, - usesSdkLibrariesOptional); + this.usesSdkLibrariesCertDigests, certSha256Digests, /* allowDuplicates= */ true); + this.usesSdkLibrariesOptional = ArrayUtils.appendBooleanDuplicatesAllowed( + this.usesSdkLibrariesOptional, usesSdkLibrariesOptional); return this; } 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/java/com/android/internal/util/ArrayUtils.java b/core/java/com/android/internal/util/ArrayUtils.java index 8f00f79e7179..1e2cad41065d 100644 --- a/core/java/com/android/internal/util/ArrayUtils.java +++ b/core/java/com/android/internal/util/ArrayUtils.java @@ -620,10 +620,10 @@ public class ArrayUtils { } /** - * Adds value to given array if not already present, providing set-like - * behavior. + * Adds value to given array. The method allows duplicate values. */ - public static boolean[] appendBoolean(@Nullable boolean[] cur, boolean val) { + public static boolean[] appendBooleanDuplicatesAllowed(@Nullable boolean[] cur, + boolean val) { if (cur == null) { return new boolean[] { val }; } diff --git a/core/java/com/android/internal/vibrator/persistence/SerializedAmplitudeStepWaveform.java b/core/java/com/android/internal/vibrator/persistence/SerializedAmplitudeStepWaveform.java index 15ecedd0f59e..cd7dcfdac906 100644 --- a/core/java/com/android/internal/vibrator/persistence/SerializedAmplitudeStepWaveform.java +++ b/core/java/com/android/internal/vibrator/persistence/SerializedAmplitudeStepWaveform.java @@ -29,7 +29,7 @@ import android.os.VibrationEffect; import android.util.IntArray; import android.util.LongArray; -import com.android.internal.vibrator.persistence.SerializedVibrationEffect.SerializedSegment; +import com.android.internal.vibrator.persistence.SerializedComposedEffect.SerializedSegment; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; diff --git a/core/java/com/android/internal/vibrator/persistence/SerializedVibrationEffect.java b/core/java/com/android/internal/vibrator/persistence/SerializedComposedEffect.java index 23df3048e69c..6c562c99565e 100644 --- a/core/java/com/android/internal/vibrator/persistence/SerializedVibrationEffect.java +++ b/core/java/com/android/internal/vibrator/persistence/SerializedComposedEffect.java @@ -29,24 +29,24 @@ import java.io.IOException; import java.util.Arrays; /** - * Serialized representation of a {@link VibrationEffect}. + * Serialized representation of a {@link VibrationEffect.Composed}. * * <p>The vibration is represented by a list of serialized segments that can be added to a * {@link VibrationEffect.Composition} during the {@link #deserialize()} procedure. * * @hide */ -final class SerializedVibrationEffect implements XmlSerializedVibration<VibrationEffect> { +final class SerializedComposedEffect implements XmlSerializedVibration<VibrationEffect.Composed> { @NonNull private final SerializedSegment[] mSegments; - SerializedVibrationEffect(@NonNull SerializedSegment segment) { + SerializedComposedEffect(@NonNull SerializedSegment segment) { requireNonNull(segment); mSegments = new SerializedSegment[]{ segment }; } - SerializedVibrationEffect(@NonNull SerializedSegment[] segments) { + SerializedComposedEffect(@NonNull SerializedSegment[] segments) { requireNonNull(segments); checkArgument(segments.length > 0, "Unsupported empty vibration"); mSegments = segments; @@ -54,12 +54,12 @@ final class SerializedVibrationEffect implements XmlSerializedVibration<Vibratio @NonNull @Override - public VibrationEffect deserialize() { + public VibrationEffect.Composed deserialize() { VibrationEffect.Composition composition = VibrationEffect.startComposition(); for (SerializedSegment segment : mSegments) { segment.deserializeIntoComposition(composition); } - return composition.compose(); + return (VibrationEffect.Composed) composition.compose(); } @Override @@ -79,7 +79,7 @@ final class SerializedVibrationEffect implements XmlSerializedVibration<Vibratio @Override public String toString() { - return "SerializedVibrationEffect{" + return "SerializedComposedEffect{" + "segments=" + Arrays.toString(mSegments) + '}'; } diff --git a/core/java/com/android/internal/vibrator/persistence/SerializedCompositionPrimitive.java b/core/java/com/android/internal/vibrator/persistence/SerializedCompositionPrimitive.java index db5c7ff830b5..862f7cb1d476 100644 --- a/core/java/com/android/internal/vibrator/persistence/SerializedCompositionPrimitive.java +++ b/core/java/com/android/internal/vibrator/persistence/SerializedCompositionPrimitive.java @@ -27,7 +27,7 @@ import android.annotation.Nullable; import android.os.VibrationEffect; import android.os.vibrator.PrimitiveSegment; -import com.android.internal.vibrator.persistence.SerializedVibrationEffect.SerializedSegment; +import com.android.internal.vibrator.persistence.SerializedComposedEffect.SerializedSegment; import com.android.internal.vibrator.persistence.XmlConstants.PrimitiveEffectName; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; diff --git a/core/java/com/android/internal/vibrator/persistence/SerializedPredefinedEffect.java b/core/java/com/android/internal/vibrator/persistence/SerializedPredefinedEffect.java index 8924311f9c33..a6f48a445564 100644 --- a/core/java/com/android/internal/vibrator/persistence/SerializedPredefinedEffect.java +++ b/core/java/com/android/internal/vibrator/persistence/SerializedPredefinedEffect.java @@ -25,7 +25,7 @@ import android.annotation.NonNull; import android.os.VibrationEffect; import android.os.vibrator.PrebakedSegment; -import com.android.internal.vibrator.persistence.SerializedVibrationEffect.SerializedSegment; +import com.android.internal.vibrator.persistence.SerializedComposedEffect.SerializedSegment; import com.android.internal.vibrator.persistence.XmlConstants.PredefinedEffectName; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; diff --git a/core/java/com/android/internal/vibrator/persistence/SerializedVendorEffect.java b/core/java/com/android/internal/vibrator/persistence/SerializedVendorEffect.java new file mode 100644 index 000000000000..aa1b0a236723 --- /dev/null +++ b/core/java/com/android/internal/vibrator/persistence/SerializedVendorEffect.java @@ -0,0 +1,127 @@ +/* + * 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.vibrator.persistence; + +import static com.android.internal.vibrator.persistence.XmlConstants.TAG_VENDOR_EFFECT; + +import static java.util.Objects.requireNonNull; + +import android.annotation.NonNull; +import android.annotation.SuppressLint; +import android.os.PersistableBundle; +import android.os.VibrationEffect; +import android.text.TextUtils; +import android.util.Base64; + +import com.android.modules.utils.TypedXmlPullParser; +import com.android.modules.utils.TypedXmlSerializer; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +/** + * Serialized representation of a {@link VibrationEffect.VendorEffect}. + * + * <p>The vibration is represented by an opaque {@link PersistableBundle} that can be used by + * {@link VibrationEffect#createVendorEffect(PersistableBundle)} during the {@link #deserialize()} + * procedure. + * + * @hide + */ +final class SerializedVendorEffect implements XmlSerializedVibration<VibrationEffect.VendorEffect> { + + @NonNull + private final PersistableBundle mVendorData; + + SerializedVendorEffect(@NonNull PersistableBundle vendorData) { + requireNonNull(vendorData); + mVendorData = vendorData; + } + + @SuppressLint("MissingPermission") + @NonNull + @Override + public VibrationEffect.VendorEffect deserialize() { + return (VibrationEffect.VendorEffect) VibrationEffect.createVendorEffect(mVendorData); + } + + @Override + public void write(@NonNull TypedXmlSerializer serializer) + throws IOException { + serializer.startTag(XmlConstants.NAMESPACE, XmlConstants.TAG_VIBRATION_EFFECT); + writeContent(serializer); + serializer.endTag(XmlConstants.NAMESPACE, XmlConstants.TAG_VIBRATION_EFFECT); + } + + @Override + public void writeContent(@NonNull TypedXmlSerializer serializer) throws IOException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + mVendorData.writeToStream(outputStream); + + serializer.startTag(XmlConstants.NAMESPACE, XmlConstants.TAG_VENDOR_EFFECT); + serializer.text(Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP)); + serializer.endTag(XmlConstants.NAMESPACE, XmlConstants.TAG_VENDOR_EFFECT); + } + + @Override + public String toString() { + return "SerializedVendorEffect{" + + "vendorData=" + mVendorData + + '}'; + } + + /** Parser implementation for {@link SerializedVendorEffect}. */ + static final class Parser { + + @NonNull + static SerializedVendorEffect parseNext(@NonNull TypedXmlPullParser parser, + @XmlConstants.Flags int flags) throws XmlParserException, IOException { + XmlValidator.checkStartTag(parser, TAG_VENDOR_EFFECT); + XmlValidator.checkTagHasNoUnexpectedAttributes(parser); + + PersistableBundle vendorData; + XmlReader.readNextText(parser, TAG_VENDOR_EFFECT); + + try { + String text = parser.getText().trim(); + XmlValidator.checkParserCondition(!text.isEmpty(), + "Expected tag %s to have base64 representation of vendor data, got empty", + TAG_VENDOR_EFFECT); + + vendorData = PersistableBundle.readFromStream( + new ByteArrayInputStream(Base64.decode(text, Base64.DEFAULT))); + XmlValidator.checkParserCondition(!vendorData.isEmpty(), + "Expected tag %s to have non-empty vendor data, got empty bundle", + TAG_VENDOR_EFFECT); + } catch (IllegalArgumentException | NullPointerException e) { + throw new XmlParserException( + TextUtils.formatSimple( + "Expected base64 representation of vendor data in tag %s, got %s", + TAG_VENDOR_EFFECT, parser.getText()), + e); + } catch (IOException e) { + throw new XmlParserException("Error reading vendor data from decoded bytes", e); + } + + // Consume tag + XmlReader.readEndTag(parser); + + return new SerializedVendorEffect(vendorData); + } + } +} diff --git a/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlParser.java b/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlParser.java index 2b8b61d50a6c..a9fbcafa128d 100644 --- a/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlParser.java +++ b/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlParser.java @@ -18,13 +18,15 @@ package com.android.internal.vibrator.persistence; import static com.android.internal.vibrator.persistence.XmlConstants.TAG_PREDEFINED_EFFECT; import static com.android.internal.vibrator.persistence.XmlConstants.TAG_PRIMITIVE_EFFECT; +import static com.android.internal.vibrator.persistence.XmlConstants.TAG_VENDOR_EFFECT; import static com.android.internal.vibrator.persistence.XmlConstants.TAG_VIBRATION_EFFECT; import static com.android.internal.vibrator.persistence.XmlConstants.TAG_WAVEFORM_EFFECT; import android.annotation.NonNull; import android.os.VibrationEffect; +import android.os.vibrator.Flags; -import com.android.internal.vibrator.persistence.SerializedVibrationEffect.SerializedSegment; +import com.android.internal.vibrator.persistence.SerializedComposedEffect.SerializedSegment; import com.android.modules.utils.TypedXmlPullParser; import java.io.IOException; @@ -80,6 +82,16 @@ import java.util.List; * } * </pre> * + * * Vendor vibration effects + * + * <pre> + * {@code + * <vibration-effect> + * <vendor-effect>base64-representation-of-persistable-bundle</vendor-effect> + * </vibration-effect> + * } + * </pre> + * * @hide */ public class VibrationEffectXmlParser { @@ -87,11 +99,9 @@ public class VibrationEffectXmlParser { /** * Parses the current XML tag with all nested tags into a single {@link XmlSerializedVibration} * wrapping a {@link VibrationEffect}. - * - * @see XmlParser#parseTag(TypedXmlPullParser) */ @NonNull - public static XmlSerializedVibration<VibrationEffect> parseTag( + public static XmlSerializedVibration<? extends VibrationEffect> parseTag( @NonNull TypedXmlPullParser parser, @XmlConstants.Flags int flags) throws XmlParserException, IOException { XmlValidator.checkStartTag(parser, TAG_VIBRATION_EFFECT); @@ -107,8 +117,9 @@ public class VibrationEffectXmlParser { * <p>This can be reused for reading a vibration from an XML root tag or from within a combined * vibration, but it should always be called from places that validates the top level tag. */ - static SerializedVibrationEffect parseVibrationContent(TypedXmlPullParser parser, - @XmlConstants.Flags int flags) throws XmlParserException, IOException { + private static XmlSerializedVibration<? extends VibrationEffect> parseVibrationContent( + TypedXmlPullParser parser, @XmlConstants.Flags int flags) + throws XmlParserException, IOException { String vibrationTagName = parser.getName(); int vibrationTagDepth = parser.getDepth(); @@ -116,11 +127,16 @@ public class VibrationEffectXmlParser { XmlReader.readNextTagWithin(parser, vibrationTagDepth), "Unsupported empty vibration tag"); - SerializedVibrationEffect serializedVibration; + XmlSerializedVibration<? extends VibrationEffect> serializedVibration; switch (parser.getName()) { + case TAG_VENDOR_EFFECT: + if (Flags.vendorVibrationEffects()) { + serializedVibration = SerializedVendorEffect.Parser.parseNext(parser, flags); + break; + } // else fall through case TAG_PREDEFINED_EFFECT: - serializedVibration = new SerializedVibrationEffect( + serializedVibration = new SerializedComposedEffect( SerializedPredefinedEffect.Parser.parseNext(parser, flags)); break; case TAG_PRIMITIVE_EFFECT: @@ -128,11 +144,11 @@ public class VibrationEffectXmlParser { do { // First primitive tag already open primitives.add(SerializedCompositionPrimitive.Parser.parseNext(parser)); } while (XmlReader.readNextTagWithin(parser, vibrationTagDepth)); - serializedVibration = new SerializedVibrationEffect( + serializedVibration = new SerializedComposedEffect( primitives.toArray(new SerializedSegment[primitives.size()])); break; case TAG_WAVEFORM_EFFECT: - serializedVibration = new SerializedVibrationEffect( + serializedVibration = new SerializedComposedEffect( SerializedAmplitudeStepWaveform.Parser.parseNext(parser)); break; default: diff --git a/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlSerializer.java b/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlSerializer.java index f561c1485f1d..d74a23d47f4a 100644 --- a/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlSerializer.java +++ b/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlSerializer.java @@ -17,13 +17,15 @@ package com.android.internal.vibrator.persistence; import android.annotation.NonNull; +import android.os.PersistableBundle; import android.os.VibrationEffect; +import android.os.vibrator.Flags; import android.os.vibrator.PrebakedSegment; import android.os.vibrator.PrimitiveSegment; import android.os.vibrator.StepSegment; import android.os.vibrator.VibrationEffectSegment; -import com.android.internal.vibrator.persistence.SerializedVibrationEffect.SerializedSegment; +import com.android.internal.vibrator.persistence.SerializedComposedEffect.SerializedSegment; import com.android.internal.vibrator.persistence.XmlConstants.PredefinedEffectName; import com.android.internal.vibrator.persistence.XmlConstants.PrimitiveEffectName; @@ -41,6 +43,7 @@ import java.util.List; * <li>{@link VibrationEffect#createWaveform(long[], int[], int)} * <li>A composition created exclusively via * {@link VibrationEffect.Composition#addPrimitive(int, float, int)} + * <li>{@link VibrationEffect#createVendorEffect(PersistableBundle)} * </ul> * * @hide @@ -49,13 +52,16 @@ public final class VibrationEffectXmlSerializer { /** * Creates a serialized representation of the input {@code vibration}. - * - * @see XmlSerializer#serialize */ @NonNull - public static XmlSerializedVibration<VibrationEffect> serialize( + public static XmlSerializedVibration<? extends VibrationEffect> serialize( @NonNull VibrationEffect vibration, @XmlConstants.Flags int flags) throws XmlSerializerException { + if (Flags.vendorVibrationEffects() + && (vibration instanceof VibrationEffect.VendorEffect vendorEffect)) { + return serializeVendorEffect(vendorEffect); + } + XmlValidator.checkSerializerCondition(vibration instanceof VibrationEffect.Composed, "Unsupported VibrationEffect type %s", vibration); @@ -73,7 +79,7 @@ public final class VibrationEffectXmlSerializer { return serializeWaveformEffect(composed); } - private static SerializedVibrationEffect serializePredefinedEffect( + private static SerializedComposedEffect serializePredefinedEffect( VibrationEffect.Composed effect, @XmlConstants.Flags int flags) throws XmlSerializerException { List<VibrationEffectSegment> segments = effect.getSegments(); @@ -81,10 +87,15 @@ public final class VibrationEffectXmlSerializer { "Unsupported repeating predefined effect %s", effect); XmlValidator.checkSerializerCondition(segments.size() == 1, "Unsupported multiple segments in predefined effect %s", effect); - return new SerializedVibrationEffect(serializePrebakedSegment(segments.get(0), flags)); + return new SerializedComposedEffect(serializePrebakedSegment(segments.get(0), flags)); + } + + private static SerializedVendorEffect serializeVendorEffect( + VibrationEffect.VendorEffect effect) { + return new SerializedVendorEffect(effect.getVendorData()); } - private static SerializedVibrationEffect serializePrimitiveEffect( + private static SerializedComposedEffect serializePrimitiveEffect( VibrationEffect.Composed effect) throws XmlSerializerException { List<VibrationEffectSegment> segments = effect.getSegments(); XmlValidator.checkSerializerCondition(effect.getRepeatIndex() == -1, @@ -95,10 +106,10 @@ public final class VibrationEffectXmlSerializer { primitives[i] = serializePrimitiveSegment(segments.get(i)); } - return new SerializedVibrationEffect(primitives); + return new SerializedComposedEffect(primitives); } - private static SerializedVibrationEffect serializeWaveformEffect( + private static SerializedComposedEffect serializeWaveformEffect( VibrationEffect.Composed effect) throws XmlSerializerException { SerializedAmplitudeStepWaveform.Builder serializedWaveformBuilder = new SerializedAmplitudeStepWaveform.Builder(); @@ -120,7 +131,7 @@ public final class VibrationEffectXmlSerializer { segment.getDuration(), toAmplitudeInt(segment.getAmplitude())); } - return new SerializedVibrationEffect(serializedWaveformBuilder.build()); + return new SerializedComposedEffect(serializedWaveformBuilder.build()); } private static SerializedPredefinedEffect serializePrebakedSegment( diff --git a/core/java/com/android/internal/vibrator/persistence/XmlConstants.java b/core/java/com/android/internal/vibrator/persistence/XmlConstants.java index 8b92153b27db..2a55d999bc0f 100644 --- a/core/java/com/android/internal/vibrator/persistence/XmlConstants.java +++ b/core/java/com/android/internal/vibrator/persistence/XmlConstants.java @@ -40,6 +40,7 @@ public final class XmlConstants { public static final String TAG_PREDEFINED_EFFECT = "predefined-effect"; public static final String TAG_PRIMITIVE_EFFECT = "primitive-effect"; + public static final String TAG_VENDOR_EFFECT = "vendor-effect"; public static final String TAG_WAVEFORM_EFFECT = "waveform-effect"; public static final String TAG_WAVEFORM_ENTRY = "waveform-entry"; public static final String TAG_REPEATING = "repeating"; diff --git a/core/java/com/android/internal/vibrator/persistence/XmlParser.java b/core/java/com/android/internal/vibrator/persistence/XmlParser.java deleted file mode 100644 index 6712f1c46a50..000000000000 --- a/core/java/com/android/internal/vibrator/persistence/XmlParser.java +++ /dev/null @@ -1,51 +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.internal.vibrator.persistence; - -import android.annotation.NonNull; - -import com.android.modules.utils.TypedXmlPullParser; - -import java.io.IOException; - -/** - * Parse XML tags into valid {@link XmlSerializedVibration} instances. - * - * @param <T> The vibration type that will be parsed. - * @see XmlSerializedVibration - * @hide - */ -@FunctionalInterface -public interface XmlParser<T> { - - /** - * Parses the current XML tag with all nested tags into a single {@link XmlSerializedVibration}. - * - * <p>This method will consume nested XML tags until it finds the - * {@link TypedXmlPullParser#END_TAG} for the current tag. - * - * <p>The vibration reconstructed by the returned {@link XmlSerializedVibration#deserialize()} - * is guaranteed to be valid. This method will throw an exception otherwise. - * - * @param pullParser The {@link TypedXmlPullParser} with the input XML. - * @return The parsed vibration wrapped in a {@link XmlSerializedVibration} representation. - * @throws IOException On any I/O error while reading the input XML - * @throws XmlParserException If the XML content does not represent a valid vibration. - */ - XmlSerializedVibration<T> parseTag(@NonNull TypedXmlPullParser pullParser) - throws XmlParserException, IOException; -} diff --git a/core/java/com/android/internal/vibrator/persistence/XmlParserException.java b/core/java/com/android/internal/vibrator/persistence/XmlParserException.java index 7507864eea38..e2b30e74b0d5 100644 --- a/core/java/com/android/internal/vibrator/persistence/XmlParserException.java +++ b/core/java/com/android/internal/vibrator/persistence/XmlParserException.java @@ -23,7 +23,6 @@ import org.xmlpull.v1.XmlPullParserException; /** * Represents an error while parsing a vibration XML input. * - * @see XmlParser * @hide */ public final class XmlParserException extends Exception { diff --git a/core/java/com/android/internal/vibrator/persistence/XmlReader.java b/core/java/com/android/internal/vibrator/persistence/XmlReader.java index a5ace8438142..0ac6fefc8cb2 100644 --- a/core/java/com/android/internal/vibrator/persistence/XmlReader.java +++ b/core/java/com/android/internal/vibrator/persistence/XmlReader.java @@ -130,6 +130,25 @@ public final class XmlReader { } /** + * Read the next element, ignoring comments and ignorable whitespace, and returns only if it's a + * {@link XmlPullParser#TEXT}. Any other tag will fail this check. + * + * <p>The parser will be pointing to the first next element after skipping comments, + * instructions and ignorable whitespace. + */ + public static void readNextText(TypedXmlPullParser parser, String tagName) + throws XmlParserException, IOException { + try { + int type = parser.next(); // skips comments, instruction tokens and ignorable whitespace + XmlValidator.checkParserCondition(type == XmlPullParser.TEXT, + "Unexpected event %s of type %d, expected text event inside tag %s", + parser.getName(), type, tagName); + } catch (XmlPullParserException e) { + throw XmlParserException.createFromPullParserException("text event", e); + } + } + + /** * Check parser has a {@link XmlPullParser#END_TAG} as the next tag, with no nested tags. * * <p>The parser will be pointing to the end tag after this method. diff --git a/core/java/com/android/internal/vibrator/persistence/XmlSerializedVibration.java b/core/java/com/android/internal/vibrator/persistence/XmlSerializedVibration.java index 3233fa224694..c20b7d2026ec 100644 --- a/core/java/com/android/internal/vibrator/persistence/XmlSerializedVibration.java +++ b/core/java/com/android/internal/vibrator/persistence/XmlSerializedVibration.java @@ -26,8 +26,7 @@ import java.io.IOException; * Serialized representation of a generic vibration. * * <p>This can be used to represent a {@link android.os.CombinedVibration} or a - * {@link android.os.VibrationEffect}. Instances can be created from vibration objects via - * {@link XmlSerializer}, or from XML content via {@link XmlParser}. + * {@link android.os.VibrationEffect}. * * <p>The separation of serialization and writing procedures enables configurable rules to define * which vibrations can be successfully serialized before any data is written to the output stream. diff --git a/core/java/com/android/internal/vibrator/persistence/XmlSerializer.java b/core/java/com/android/internal/vibrator/persistence/XmlSerializer.java deleted file mode 100644 index 102e6c1db395..000000000000 --- a/core/java/com/android/internal/vibrator/persistence/XmlSerializer.java +++ /dev/null @@ -1,40 +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.internal.vibrator.persistence; - -import android.annotation.NonNull; - -/** - * Creates a {@link XmlSerializedVibration} instance representing a vibration. - * - * @param <T> The vibration type that will be serialized. - * @see XmlSerializedVibration - * @hide - */ -@FunctionalInterface -public interface XmlSerializer<T> { - - /** - * Creates a serialized representation of the input {@code vibration}. - * - * @param vibration The vibration to be serialized - * @return The serialized representation of the input vibration - * @throws XmlSerializerException If the input vibration cannot be serialized - */ - @NonNull - XmlSerializedVibration<T> serialize(@NonNull T vibration) throws XmlSerializerException; -} diff --git a/core/java/com/android/internal/vibrator/persistence/XmlSerializerException.java b/core/java/com/android/internal/vibrator/persistence/XmlSerializerException.java index c57ff5d50cd2..2e7ad090cf0f 100644 --- a/core/java/com/android/internal/vibrator/persistence/XmlSerializerException.java +++ b/core/java/com/android/internal/vibrator/persistence/XmlSerializerException.java @@ -19,7 +19,6 @@ package com.android.internal.vibrator.persistence; /** * Represents an error while serializing a vibration input. * - * @see XmlSerializer * @hide */ public final class XmlSerializerException extends Exception { diff --git a/core/java/com/android/internal/vibrator/persistence/XmlValidator.java b/core/java/com/android/internal/vibrator/persistence/XmlValidator.java index 84d4f3f49e8a..1b5a3561c3ef 100644 --- a/core/java/com/android/internal/vibrator/persistence/XmlValidator.java +++ b/core/java/com/android/internal/vibrator/persistence/XmlValidator.java @@ -18,7 +18,7 @@ package com.android.internal.vibrator.persistence; import static java.util.Objects.requireNonNull; -import android.annotation.NonNull; +import android.os.VibrationEffect; import android.text.TextUtils; import com.android.internal.util.ArrayUtils; @@ -82,11 +82,11 @@ public final class XmlValidator { * Check given {@link XmlSerializedVibration} represents the expected {@code vibration} object * when it's deserialized. */ - @NonNull - public static <T> void checkSerializedVibration( - XmlSerializedVibration<T> serializedVibration, T expectedVibration) + public static void checkSerializedVibration( + XmlSerializedVibration<? extends VibrationEffect> serializedVibration, + VibrationEffect expectedVibration) throws XmlSerializerException { - T deserializedVibration = requireNonNull(serializedVibration.deserialize()); + VibrationEffect deserializedVibration = requireNonNull(serializedVibration.deserialize()); checkSerializerCondition(Objects.equals(expectedVibration, deserializedVibration), "Unexpected serialized vibration %s: found deserialization %s, expected %s", serializedVibration, deserializedVibration, expectedVibration); diff --git a/core/proto/android/server/vibrator/vibratormanagerservice.proto b/core/proto/android/server/vibrator/vibratormanagerservice.proto index 12804d43b04e..e7f0560612cc 100644 --- a/core/proto/android/server/vibrator/vibratormanagerservice.proto +++ b/core/proto/android/server/vibrator/vibratormanagerservice.proto @@ -163,7 +163,7 @@ message VibratorManagerServiceDumpProto { optional bool vibrator_under_external_control = 5; optional bool low_power_mode = 6; optional bool vibrate_on = 24; - optional bool keyboard_vibration_on = 25; + reserved 25; // prev keyboard_vibration_on optional int32 default_vibration_amplitude = 26; optional int32 alarm_intensity = 18; optional int32 alarm_default_intensity = 19; diff --git a/core/proto/android/widget/remoteviews.proto b/core/proto/android/widget/remoteviews.proto index 37d1c5b03ee5..5892396bddc4 100644 --- a/core/proto/android/widget/remoteviews.proto +++ b/core/proto/android/widget/remoteviews.proto @@ -89,6 +89,205 @@ message RemoteViewsProto { bytes adaptive_bitmap = 8; }; } + + /** + * Represents a CharSequence with Spans. + */ + message CharSequence { + optional string text = 1; + repeated Span spans = 2; + + message Span { + optional int32 start = 1; + optional int32 end = 2; + optional int32 flags = 3; + // We use `repeated` for the following fields so that ProtoOutputStream does not omit + // empty messages (e.g. EasyEdit, Superscript). In practice, only one of the following + // fields will be written per Span message. We cannot use `oneof` here because + // ProtoOutputStream will omit empty messages. + repeated AbsoluteSize absolute_size = 4; + repeated AccessibilityClickable accessibility_clickable = 5; + repeated AccessibilityReplacement accessibility_replacement = 6; + repeated AccessibilityUrl accessibility_url = 7; + repeated Alignment alignment = 8; + repeated Annotation annotation = 9; + repeated BackgroundColor background_color = 10; + repeated Bullet bullet = 11; + repeated EasyEdit easy_edit = 12; + repeated ForegroundColor foreground_color = 13; + repeated LeadingMargin leading_margin = 14; + repeated LineBackground line_background = 15; + repeated LineBreak line_break = 16; + repeated LineHeight line_height = 17; + repeated Locale locale = 18; + repeated Quote quote = 19; + repeated RelativeSize relative_size = 20; + repeated ScaleX scale_x = 21; + repeated SpellCheck spell_check = 22; + repeated Strikethrough strikethrough = 23; + repeated Style style = 24; + repeated Subscript subscript = 25; + repeated Suggestion suggestion = 26; + repeated SuggestionRange suggestion_range = 27; + repeated Superscript superscript = 28; + repeated TextAppearance text_appearance = 29; + repeated Tts tts = 30; + repeated Typeface typeface = 31; + repeated Underline underline = 32; + repeated Url url = 33; + + message AbsoluteSize { + optional int32 size = 1; + optional bool dip = 2; + } + + message AccessibilityClickable { + optional int32 original_clickable_span_id = 1; + } + + message AccessibilityReplacement { + optional CharSequence content_description = 1; + } + + message AccessibilityUrl { + optional string url = 1; + } + + message Alignment { + optional string alignment = 1; + } + + message Annotation { + optional string key = 1; + optional string value = 2; + } + + message BackgroundColor { + optional int32 color = 1; + } + + message Bullet { + optional int32 gap_width = 1; + optional int32 color = 2; + optional int32 bullet_radius = 3; + optional bool want_color = 4; + } + + message EasyEdit {} + + message ForegroundColor { + optional int32 color = 1; + } + + message LeadingMargin { + optional int32 first = 1; + optional int32 rest = 2; + } + + message LineBackground { + optional int32 color = 1; + } + + message LineBreak { + optional int32 line_break_style = 1; + optional int32 line_break_word_style = 2; + optional int32 hyphenation = 3; + } + + message LineHeight { + optional int32 height = 1; + } + + message Locale { + optional string language_tags = 1; + } + + message Quote { + optional int32 color = 1; + optional int32 stripe_width = 2; + optional int32 gap_width = 3; + } + + message RelativeSize { + optional float proportion = 1; + } + + message ScaleX { + optional float proportion = 1; + } + + message SpellCheck { + optional bool in_progress = 1; + } + + message Strikethrough {} + + message Style { + optional int32 style = 1; + optional int32 font_weight_adjustment = 2; + } + + message Subscript {} + + message Suggestion { + repeated string suggestions = 1; + optional int32 flags = 2; + optional string locale_string_for_compatibility = 3; + optional string language_tag = 4; + optional int32 hash_code = 5; + optional int32 easy_correct_underline_color = 6; + optional float easy_correct_underline_thickness = 7; + optional int32 misspelled_underline_color = 8; + optional float misspelled_underline_thickness = 9; + optional int32 auto_correction_underline_color = 10; + optional float auto_correction_underline_thickness = 11; + optional int32 grammar_error_underline_color = 12; + optional float grammar_error_underline_thickness = 13; + } + + message SuggestionRange { + optional int32 background_color = 1; + } + + message Superscript {} + + // Typeface is omitted + message TextAppearance { + optional string family_name = 1; + optional int32 style = 2; + optional int32 text_size = 3; + optional android.content.res.ColorStateListProto text_color = 4; + optional android.content.res.ColorStateListProto text_color_link = 5; + optional int32 text_font_weight = 7; + optional string text_locale = 8; + optional float shadow_radius = 9; + optional float shadow_dx = 10; + optional float shadow_dy = 11; + optional int32 shadow_color = 12; + optional bool has_elegant_text_height_field = 13; + optional bool elegant_text_height = 14; + optional bool has_letter_spacing_field = 15; + optional float letter_spacing = 16; + optional string font_feature_settings = 17; + optional string font_variation_settings = 18; + } + + message Tts { + optional string type = 1; + optional string args = 2; + } + + message Typeface { + optional string family = 1; + } + + message Underline {} + + message Url { + optional string url = 1; + } + } + } } 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/input_method_switch_dialog_new.xml b/core/res/res/layout/input_method_switch_dialog_new.xml index ab5d38f52f7d..610a212bbd4e 100644 --- a/core/res/res/layout/input_method_switch_dialog_new.xml +++ b/core/res/res/layout/input_method_switch_dialog_new.xml @@ -17,6 +17,7 @@ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/container" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> 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/layout/time_picker_text_input_material.xml b/core/res/res/layout/time_picker_text_input_material.xml index 4988842cb99c..86070b1773dc 100644 --- a/core/res/res/layout/time_picker_text_input_material.xml +++ b/core/res/res/layout/time_picker_text_input_material.xml @@ -34,19 +34,29 @@ android:layoutDirection="ltr"> <EditText android:id="@+id/input_hour" - android:layout_width="50dp" + android:layout_width="50sp" android:layout_height="wrap_content" + android:layout_alignEnd="@id/hour_label_holder" android:inputType="number" android:textAppearance="@style/TextAppearance.Material.TimePicker.InputField" android:imeOptions="actionNext"/> - <TextView - android:id="@+id/label_hour" + <!-- Ensure the label_hour takes up at least 50sp of space --> + <FrameLayout + android:id="@+id/hour_label_holder" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_below="@id/input_hour" - android:layout_alignStart="@id/input_hour" - android:labelFor="@+id/input_hour" - android:text="@string/time_picker_hour_label"/> + android:layout_below="@id/input_hour"> + <TextView + android:id="@+id/label_hour" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:labelFor="@+id/input_hour" + android:text="@string/time_picker_hour_label"/> + <Space + android:layout_width="50sp" + android:layout_height="0dp"/> + </FrameLayout> <TextView android:id="@+id/input_separator" @@ -58,21 +68,30 @@ <EditText android:id="@+id/input_minute" - android:layout_width="50dp" + android:layout_width="50sp" android:layout_height="wrap_content" android:layout_alignBaseline="@id/input_hour" android:layout_toEndOf="@id/input_separator" android:inputType="number" android:textAppearance="@style/TextAppearance.Material.TimePicker.InputField" /> - <TextView - android:id="@+id/label_minute" + <!-- Ensure the label_minute takes up at least 50sp of space --> + <FrameLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@id/input_minute" android:layout_alignStart="@id/input_minute" - android:labelFor="@+id/input_minute" - android:text="@string/time_picker_minute_label"/> - + > + <TextView + android:id="@+id/label_minute" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:labelFor="@+id/input_minute" + android:text="@string/time_picker_minute_label"/> + <Space + android:layout_width="50sp" + android:layout_height="0dp"/> + </FrameLayout> <TextView android:visibility="invisible" android:id="@+id/label_error" 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/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index bdcba9daa5ad..0d16e9c939d9 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -5462,7 +5462,9 @@ <java-symbol type="bool" name="config_enable_a11y_fullscreen_magnification_overscroll_handler" /> <java-symbol type="dimen" name="accessibility_fullscreen_magnification_gesture_edge_slop" /> + <!-- For HapticFeedbackConstants configurability defined at HapticFeedbackCustomization --> <java-symbol type="string" name="config_hapticFeedbackCustomizationFile" /> + <java-symbol type="xml" name="haptic_feedback_customization" /> <!-- For ActivityManager PSS profiling configurability --> <java-symbol type="bool" name="config_am_disablePssProfiling" /> diff --git a/core/res/res/xml/haptic_feedback_customization.xml b/core/res/res/xml/haptic_feedback_customization.xml new file mode 100644 index 000000000000..7ac0787ab7a0 --- /dev/null +++ b/core/res/res/xml/haptic_feedback_customization.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> + +<haptic-feedback-constants/> diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp index 5793bbe306f1..2bbaf9cb0cda 100644 --- a/core/tests/coretests/Android.bp +++ b/core/tests/coretests/Android.bp @@ -249,6 +249,7 @@ android_ravenwood_test { ], srcs: [ "src/android/app/ActivityManagerTest.java", + "src/android/colormodel/CamTest.java", "src/android/content/ContextTest.java", "src/android/content/pm/PackageManagerTest.java", "src/android/content/pm/UserInfoTest.java", 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/res/values/styles.xml b/core/tests/coretests/res/values/styles.xml index 78cd1e1e47e8..e7009d143374 100644 --- a/core/tests/coretests/res/values/styles.xml +++ b/core/tests/coretests/res/values/styles.xml @@ -61,4 +61,28 @@ <style name="IsFrameRatePowerSavingsBalancedEnabled"> <item name="android:windowIsFrameRatePowerSavingsBalanced">true</item> </style> + <style name="customFont"> + <item name="android:fontFamily">@font/samplefont</item> + </style> + <style name="customFontWithStyle"> + <item name="android:fontFamily">@font/samplefont</item> + <item name="android:textStyle">bold|italic</item> + </style> + <style name="textAppearanceWithAllAttributes"> + <item name="android:fontFamily">@font/samplefont</item> + <item name="android:textStyle">bold|italic</item> + <item name="android:textSize">160dp</item> + <item name="android:textColor">#FF00FF</item> + <item name="android:textColorLink">#00FFFF</item> + <item name="android:textLocale">ja-JP,zh-CN</item> + <item name="android:shadowColor">#00FFFF</item> + <item name="android:shadowDx">1.0</item> + <item name="android:shadowDy">2.0</item> + <item name="android:shadowRadius">3.0</item> + <item name="android:elegantTextHeight">true</item> + <item name="android:letterSpacing">1.0</item> + <item name="android:fontFeatureSettings">\"smcp\"</item> + <item name="android:fontVariationSettings">\'wdth\' 150</item> + </style> + </resources> diff --git a/core/tests/coretests/src/android/colormodel/CamTest.java b/core/tests/coretests/src/android/colormodel/CamTest.java index 05fc0e04515c..cf398db22d16 100644 --- a/core/tests/coretests/src/android/colormodel/CamTest.java +++ b/core/tests/coretests/src/android/colormodel/CamTest.java @@ -18,9 +18,12 @@ package com.android.internal.graphics.cam; import static org.junit.Assert.assertEquals; +import android.platform.test.ravenwood.RavenwoodRule; + import androidx.test.filters.LargeTest; import org.junit.Assert; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -35,6 +38,9 @@ public final class CamTest { static final int GREEN = 0xff00ff00; static final int BLUE = 0xff0000ff; + @Rule + public final RavenwoodRule mRavenwood = new RavenwoodRule(); + @Test public void camFromIntToInt() { Cam cam = Cam.fromInt(RED); diff --git a/core/tests/coretests/src/android/text/method/InsertModeTransformationMethodTest.java b/core/tests/coretests/src/android/text/method/InsertModeTransformationMethodTest.java index 2f336ab692f6..e2c19024a840 100644 --- a/core/tests/coretests/src/android/text/method/InsertModeTransformationMethodTest.java +++ b/core/tests/coretests/src/android/text/method/InsertModeTransformationMethodTest.java @@ -20,6 +20,10 @@ import static com.google.common.truth.Truth.assertThat; import android.content.Context; import android.platform.test.annotations.Presubmit; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.text.SpannableString; import android.text.SpannableStringBuilder; import android.text.Spanned; @@ -30,7 +34,10 @@ import androidx.test.InstrumentationRegistry; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.text.flags.Flags; + import org.junit.BeforeClass; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -41,6 +48,9 @@ public class InsertModeTransformationMethodTest { private static View sView; private static final String TEXT = "abc def"; + @Rule + public CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + @BeforeClass public static void setupClass() { final Context context = InstrumentationRegistry.getTargetContext(); @@ -76,11 +86,13 @@ public class InsertModeTransformationMethodTest { } @Test + @RequiresFlagsDisabled(Flags.FLAG_INSERT_MODE_HIGHLIGHT_RANGE) public void transformedText_charAt_editing() { transformedText_charAt_editing(false, "\n\n"); } @Test + @RequiresFlagsDisabled(Flags.FLAG_INSERT_MODE_HIGHLIGHT_RANGE) public void transformedText_charAt_singleLine_editing() { transformedText_charAt_editing(true, "\uFFFD"); } @@ -132,6 +144,64 @@ public class InsertModeTransformationMethodTest { } @Test + @RequiresFlagsEnabled(Flags.FLAG_INSERT_MODE_HIGHLIGHT_RANGE) + public void transformedText_charAt_editing_stickyHighlightRange() { + transformedText_charAt_editing_stickyHighlightRange(false, "\n\n"); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_INSERT_MODE_HIGHLIGHT_RANGE) + public void transformedText_charAt_singleLine_editing_stickyHighlightRange() { + transformedText_charAt_editing_stickyHighlightRange(true, "\uFFFD"); + } + + private void transformedText_charAt_editing_stickyHighlightRange(boolean singleLine, + String placeholder) { + final SpannableStringBuilder text = new SpannableStringBuilder(TEXT); + final InsertModeTransformationMethod transformationMethod = + new InsertModeTransformationMethod(3, singleLine, null); + final CharSequence transformedText = transformationMethod.getTransformation(text, sView); + // TransformationMethod is set on the original text as a TextWatcher in the TextView. + text.setSpan(transformationMethod, 0, text.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + assertCharSequence(transformedText, "abc" + placeholder + " def"); + + // original text is "abcxx def" after insertion. + text.insert(3, "xx"); + assertCharSequence(transformedText, "abcxx" + placeholder + " def"); + + // original text is "abcxx vvdef" after insertion. + text.insert(6, "vv"); + assertCharSequence(transformedText, "abcxx" + placeholder + " vvdef"); + + // original text is "abc vvdef" after deletion. + text.delete(3, 5); + assertCharSequence(transformedText, "abc" + placeholder + " vvdef"); + + // original text is "abc def" after deletion. + text.delete(4, 6); + assertCharSequence(transformedText, "abc" + placeholder + " def"); + + // original text is "abdef" after deletion. + // deletion range covers the placeholder's insertion point. It'll try to stay the same, + // which is still at index 3. + text.delete(2, 4); + assertCharSequence(transformedText, "abd" + placeholder + "ef"); + + // original text is "axxdef" after replace. + // this time the replaced range is ahead of the placeholder's insertion point. It updates to + // index 4. + text.replace(1, 2, "xx"); + assertCharSequence(transformedText, "axxd" + placeholder + "ef"); + + // original text is "ax" after replace. + // the deleted range covers the placeholder's insertion point. It tries to stay at index 4. + // However, 4 out of bounds now. So placeholder is inserted at the end of the string. + text.delete(2, 6); + assertCharSequence(transformedText, "ax" + placeholder); + } + + @Test public void transformedText_subSequence() { for (int offset = 0; offset < TEXT.length(); ++offset) { final InsertModeTransformationMethod transformationMethod = @@ -697,7 +767,7 @@ public class InsertModeTransformationMethodTest { } @Test - public void transformedText_getHighlightStartAndEnd_insertion_singleLine() { + public void transformedText_getHighlightStartAndEnd_singleLine_insertion() { transformedText_getHighlightStartAndEnd_insertion(true, "\uFDDD"); } @@ -751,16 +821,18 @@ public class InsertModeTransformationMethodTest { } @Test + @RequiresFlagsDisabled(Flags.FLAG_INSERT_MODE_HIGHLIGHT_RANGE) public void transformedText_getHighlightStartAndEnd_deletion() { transformedText_getHighlightStartAndEnd_deletion(false, "\n\n"); } @Test - public void transformedText_getHighlightStartAndEnd_insertion_deletion() { + @RequiresFlagsDisabled(Flags.FLAG_INSERT_MODE_HIGHLIGHT_RANGE) + public void transformedText_getHighlightStartAndEnd_singleLine_deletion() { transformedText_getHighlightStartAndEnd_deletion(true, "\uFDDD"); } - public void transformedText_getHighlightStartAndEnd_deletion(boolean singleLine, + private void transformedText_getHighlightStartAndEnd_deletion(boolean singleLine, String placeholder) { final SpannableStringBuilder text = new SpannableStringBuilder(TEXT); final InsertModeTransformationMethod transformationMethod = @@ -816,14 +888,93 @@ public class InsertModeTransformationMethodTest { assertThat(transformedText.getHighlightEnd()).isEqualTo(1 + placeholder.length()); } + @Test + @RequiresFlagsEnabled(Flags.FLAG_INSERT_MODE_HIGHLIGHT_RANGE) + public void transformedText_getHighlightStartAndEnd_deletion_stickyHighlightRange() { + transformedText_getHighlightStartAndEnd_deletion_stickyHighlightRange(false, "\n\n"); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_INSERT_MODE_HIGHLIGHT_RANGE) + public void transformedText_getHighlightStartAndEnd_singleLine_deletion_stickyHighlightRange() { + transformedText_getHighlightStartAndEnd_deletion_stickyHighlightRange(true, "\uFDDD"); + } + + private void transformedText_getHighlightStartAndEnd_deletion_stickyHighlightRange( + boolean singleLine, String placeholder) { + final SpannableStringBuilder text = new SpannableStringBuilder(TEXT); + final InsertModeTransformationMethod transformationMethod = + new InsertModeTransformationMethod(3, singleLine, null); + final InsertModeTransformationMethod.TransformedText transformedText = + (InsertModeTransformationMethod.TransformedText) transformationMethod + .getTransformation(text, sView); + // TransformationMethod is set on the original text as a TextWatcher in the TextView. + text.setSpan(transformationMethod, 0, text.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + // note: the placeholder text is also highlighted. + assertThat(transformedText.getHighlightStart()).isEqualTo(3); + assertThat(transformedText.getHighlightEnd()).isEqualTo(3 + placeholder.length()); + + // original text is "abcxxxxxx def" after insertion. + // the placeholder is now inserted at index 9. + // the highlight start is still 3. + // the highlight end now is 9 + placeholder.length(). + text.insert(3, "xxxxxx"); + assertThat(transformedText.getHighlightStart()).isEqualTo(3); + assertThat(transformedText.getHighlightEnd()).isEqualTo(9 + placeholder.length()); + + // original text is "abxxxxxx def" after deletion. + // the placeholder is now inserted at index 6. + // the highlight start is 2, since the deletion happens before the highlight range. + // the highlight end now is 8 + placeholder.length(). + text.delete(2, 3); + assertThat(transformedText.getHighlightStart()).isEqualTo(2); + assertThat(transformedText.getHighlightEnd()).isEqualTo(8 + placeholder.length()); + + // original text is "abxxx def" after deletion. + // the placeholder is now inserted at index 5. + // the highlight start is still 2, since the deletion happens in the highlight range. + // the highlight end now is 5 + placeholder.length(). + text.delete(2, 5); + assertThat(transformedText.getHighlightStart()).isEqualTo(2); + assertThat(transformedText.getHighlightEnd()).isEqualTo(5 + placeholder.length()); + + // original text is "abxxx d" after deletion. + // the placeholder is now inserted at index 5. + // the highlight start is still 2, since the deletion happens after the highlight range. + // the highlight end now is still 5 + placeholder.length(). + text.delete(7, 9); + assertThat(transformedText.getHighlightStart()).isEqualTo(2); + assertThat(transformedText.getHighlightEnd()).isEqualTo(5 + placeholder.length()); + + // original text is "axx d" after deletion. + // the placeholder is now inserted at index 3. + // the highlight start is at 2, since the deletion range covers the start. + // the highlight end is 3 + placeholder.length(). + text.delete(1, 3); + assertThat(transformedText.getHighlightStart()).isEqualTo(2); + assertThat(transformedText.getHighlightEnd()).isEqualTo(3 + placeholder.length()); + + // original text is "ax" after deletion. + // the placeholder is now inserted at index 2. + // the highlight start is at 2. + // the highlight end is 2 + placeholder.length(). It wants to stay at 3, but it'll be out + // of bounds, so it'll be 2 instead. + text.delete(2, 5); + assertThat(transformedText.getHighlightStart()).isEqualTo(2); + assertThat(transformedText.getHighlightEnd()).isEqualTo(2 + placeholder.length()); + } + @Test + @RequiresFlagsDisabled(Flags.FLAG_INSERT_MODE_HIGHLIGHT_RANGE) public void transformedText_getHighlightStartAndEnd_replace() { transformedText_getHighlightStartAndEnd_replace(false, "\n\n"); } @Test - public void transformedText_getHighlightStartAndEnd_insertion__replace() { + @RequiresFlagsDisabled(Flags.FLAG_INSERT_MODE_HIGHLIGHT_RANGE) + public void transformedText_getHighlightStartAndEnd_singleLine_replace() { transformedText_getHighlightStartAndEnd_replace(true, "\uFDDD"); } @@ -908,6 +1059,99 @@ public class InsertModeTransformationMethodTest { assertThat(transformedText.getHighlightEnd()).isEqualTo(3 + placeholder.length()); } + @Test + @RequiresFlagsEnabled(Flags.FLAG_INSERT_MODE_HIGHLIGHT_RANGE) + public void transformedText_getHighlightStartAndEnd_replace_stickyHighlightRange() { + transformedText_getHighlightStartAndEnd_replace_stickyHighlightRange(false, "\n\n"); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_INSERT_MODE_HIGHLIGHT_RANGE) + public void transformedText_getHighlightStartAndEnd_singleLine_replace_stickyHighlightRange() { + transformedText_getHighlightStartAndEnd_replace_stickyHighlightRange(true, "\uFDDD"); + } + + private void transformedText_getHighlightStartAndEnd_replace_stickyHighlightRange( + boolean singleLine, String placeholder) { + final SpannableStringBuilder text = new SpannableStringBuilder(TEXT); + final InsertModeTransformationMethod transformationMethod = + new InsertModeTransformationMethod(3, singleLine, null); + final InsertModeTransformationMethod.TransformedText transformedText = + (InsertModeTransformationMethod.TransformedText) transformationMethod + .getTransformation(text, sView); + // TransformationMethod is set on the original text as a TextWatcher in the TextView. + text.setSpan(transformationMethod, 0, text.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + // note: the placeholder text is also highlighted. + assertThat(transformedText.getHighlightStart()).isEqualTo(3); + assertThat(transformedText.getHighlightEnd()).isEqualTo(3 + placeholder.length()); + + // original text is "abcxxxxxx def" after insertion. + // the placeholder is now inserted at index 9. + // the highlight start is still 3. + // the highlight end now is 9 + placeholder.length(). + text.insert(3, "xxxxxx"); + assertThat(transformedText.getHighlightStart()).isEqualTo(3); + assertThat(transformedText.getHighlightEnd()).isEqualTo(9 + placeholder.length()); + + // original text is "abvvxxxxxx def" after replace. + // the replacement happens before the highlight range; highlight range is offset by 1 + // the placeholder is now inserted at index 10, + // the highlight start is 4. + // the highlight end is 10 + placeholder.length(). + text.replace(2, 3, "vv"); + assertThat(transformedText.getHighlightStart()).isEqualTo(4); + assertThat(transformedText.getHighlightEnd()).isEqualTo(10 + placeholder.length()); + + // original text is "abvvxxx def" after replace. + // the replacement happens in the highlight range; highlight end is offset by -3 + // the placeholder is now inserted at index 7, + // the highlight start is still 4. + // the highlight end is 7 + placeholder.length(). + text.replace(5, 9, "x"); + assertThat(transformedText.getHighlightStart()).isEqualTo(4); + assertThat(transformedText.getHighlightEnd()).isEqualTo(7 + placeholder.length()); + + // original text is "abvvxxxvvv" after replace. + // the replacement happens after the highlight range; highlight is not changed + // the placeholder is now inserted at index 7, + // the highlight start is still 4. + // the highlight end is 7 + placeholder.length(). + text.replace(7, 11, "vvv"); + assertThat(transformedText.getHighlightStart()).isEqualTo(4); + assertThat(transformedText.getHighlightEnd()).isEqualTo(7 + placeholder.length()); + + // original text is "abxxxxvvv" after replace. + // the replacement covers the highlight start; highlight start stays the same; + // highlight end is offset by -1 + // the placeholder is now inserted at index 6, + // the highlight start is 4. + // the highlight end is 6 + placeholder.length(). + text.replace(2, 5, "xx"); + assertThat(transformedText.getHighlightStart()).isEqualTo(4); + assertThat(transformedText.getHighlightEnd()).isEqualTo(6 + placeholder.length()); + + // original text is "abxxxxxvv" after replace. + // the replacement covers the highlight end; highlight end stays the same; + // highlight start stays the same + // the placeholder is now inserted at index 6, + // the highlight start is 2. + // the highlight end is 6 + placeholder.length(). + text.replace(5, 7, "xx"); + assertThat(transformedText.getHighlightStart()).isEqualTo(4); + assertThat(transformedText.getHighlightEnd()).isEqualTo(6 + placeholder.length()); + + // original text is "axxv" after replace. + // the replacement covers the highlight range; highlight start stays the same. + // highlight end shrink to the text length. + // the placeholder is now inserted at index 3, + // the highlight start is 2. + // the highlight end is 4 + placeholder.length(). + text.replace(1, 8, "xx"); + assertThat(transformedText.getHighlightStart()).isEqualTo(4); + assertThat(transformedText.getHighlightEnd()).isEqualTo(4 + placeholder.length()); + } + private static <T> void assertNextSpanTransition(Spanned spanned, int[] transitions, Class<T> type) { int currentTransition = 0; diff --git a/core/tests/coretests/src/android/view/HapticScrollFeedbackProviderTest.java b/core/tests/coretests/src/android/view/HapticScrollFeedbackProviderTest.java index b9a9557df0bc..ac6c19e79fcb 100644 --- a/core/tests/coretests/src/android/view/HapticScrollFeedbackProviderTest.java +++ b/core/tests/coretests/src/android/view/HapticScrollFeedbackProviderTest.java @@ -16,16 +16,20 @@ package android.view; +import static android.os.vibrator.Flags.FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED; import static android.view.HapticFeedbackConstants.SCROLL_ITEM_FOCUS; import static android.view.HapticFeedbackConstants.SCROLL_LIMIT; import static android.view.HapticFeedbackConstants.SCROLL_TICK; + import static com.google.common.truth.Truth.assertThat; + import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import android.content.Context; import android.platform.test.annotations.Presubmit; +import android.platform.test.flag.junit.SetFlagsRule; import android.view.flags.FeatureFlags; import androidx.test.InstrumentationRegistry; @@ -33,17 +37,24 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.Objects; +// TODO(b/353625893): update old tests to use new infra like those with "inputDeviceCustomized". @SmallTest @RunWith(AndroidJUnit4.class) @Presubmit public final class HapticScrollFeedbackProviderTest { + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); private static final int INPUT_DEVICE_1 = 1; private static final int INPUT_DEVICE_2 = 2; @@ -64,6 +75,7 @@ public final class HapticScrollFeedbackProviderTest { mView = new TestView(InstrumentationRegistry.getContext()); mProvider = new HapticScrollFeedbackProvider(mView, mMockViewConfig, /* disabledIfViewPlaysScrollHaptics= */ true); + mSetFlagsRule.disableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); } @Test @@ -85,6 +97,26 @@ public final class HapticScrollFeedbackProviderTest { } @Test + public void testRotaryEncoder_inputDeviceCustomized_noFeedbackWhenViewBasedFeedbackIsEnabled() { + mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); + + when(mMockViewConfig.isViewBasedRotaryEncoderHapticScrollFeedbackEnabled()) + .thenReturn(true); + setHapticScrollTickInterval(5); + + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ 10); + mProvider.onSnapToItem( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ true); + + assertThat(mView.mHapticFeedbackRequests).hasSize(0); + } + + @Test public void testRotaryEncoder_feedbackWhenDisregardingViewBasedScrollHaptics() { mProvider = new HapticScrollFeedbackProvider(mView, mMockViewConfig, /* disabledIfViewPlaysScrollHaptics= */ false); @@ -107,6 +139,35 @@ public final class HapticScrollFeedbackProviderTest { } @Test + public void testRotaryEncoder_inputDeviceCustomized_feedbackWhenDisregardingViewBasedScrollHaptics() { + mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); + List<HapticFeedbackRequest> requests = new ArrayList<>(); + + mProvider = new HapticScrollFeedbackProvider(mView, mMockViewConfig, + /* disabledIfViewPlaysScrollHaptics= */ false); + when(mMockViewConfig.isViewBasedRotaryEncoderHapticScrollFeedbackEnabled()) + .thenReturn(true); + setHapticScrollTickInterval(5); + + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ 10); + requests.add(new HapticFeedbackRequest( + SCROLL_TICK, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + mProvider.onSnapToItem( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); + requests.add(new HapticFeedbackRequest( + SCROLL_ITEM_FOCUS, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ true); + requests.add(new HapticFeedbackRequest( + SCROLL_LIMIT, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + + assertThat(mView.mHapticFeedbackRequests).containsExactlyElementsIn(requests).inOrder(); + } + + @Test public void testNoFeedbackWhenFeedbackIsDisabled() { setHapticScrollFeedbackEnabled(false); // Call different types scroll feedback methods; non of them should produce feedback because @@ -130,6 +191,31 @@ public final class HapticScrollFeedbackProviderTest { } @Test + public void testNoFeedbackWhenFeedbackIsDisabled_inputDeviceCustomized() { + mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); + + setHapticScrollFeedbackEnabled(false); + // Call different types scroll feedback methods; non of them should produce feedback because + // feedback has been disabled. + mProvider.onSnapToItem( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ true); + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ 300); + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ -300); + + assertThat(mView.mHapticFeedbackRequests).hasSize(0); + } + + @Test public void testSnapToItem() { mProvider.onSnapToItem( INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); @@ -138,6 +224,25 @@ public final class HapticScrollFeedbackProviderTest { } @Test + public void testSnapToItem_inputDeviceCustomized() { + mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); + List<HapticFeedbackRequest> requests = new ArrayList<>(); + + mProvider.onSnapToItem( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); + requests.add(new HapticFeedbackRequest( + SCROLL_ITEM_FOCUS, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + + mProvider.onSnapToItem( + INPUT_DEVICE_2, InputDevice.SOURCE_TOUCHSCREEN, MotionEvent.AXIS_SCROLL); + requests.add( + new HapticFeedbackRequest( + SCROLL_ITEM_FOCUS, INPUT_DEVICE_2, InputDevice.SOURCE_TOUCHSCREEN)); + + assertThat(mView.mHapticFeedbackRequests).containsExactlyElementsIn(requests).inOrder(); + } + + @Test public void testScrollLimit_start() { mProvider.onSnapToItem( INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); @@ -150,6 +255,24 @@ public final class HapticScrollFeedbackProviderTest { } @Test + public void testScrollLimit_inputDeviceCustomized_start() { + mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); + List<HapticFeedbackRequest> requests = new ArrayList<>(); + + mProvider.onSnapToItem( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); + requests.add(new HapticFeedbackRequest( + SCROLL_ITEM_FOCUS, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ true); + requests.add(new HapticFeedbackRequest( + SCROLL_LIMIT, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + + assertThat(mView.mHapticFeedbackRequests).containsExactlyElementsIn(requests).inOrder(); + } + + @Test public void testScrollLimit_stop() { mProvider.onSnapToItem( INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); @@ -162,6 +285,24 @@ public final class HapticScrollFeedbackProviderTest { } @Test + public void testScrollLimit_inputDeviceCustomized_stop() { + mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); + List<HapticFeedbackRequest> requests = new ArrayList<>(); + + mProvider.onSnapToItem( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); + requests.add(new HapticFeedbackRequest( + SCROLL_ITEM_FOCUS, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + requests.add(new HapticFeedbackRequest( + SCROLL_LIMIT, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + + assertThat(mView.mHapticFeedbackRequests).containsExactlyElementsIn(requests).inOrder(); + } + + @Test public void testScrollProgress_zeroTickInterval() { setHapticScrollTickInterval(0); @@ -176,6 +317,22 @@ public final class HapticScrollFeedbackProviderTest { } @Test + public void testScrollProgress_inputDeviceCustomized_zeroTickInterval() { + mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); + + setHapticScrollTickInterval(0); + + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ 30); + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ 20); + + assertThat(mView.mHapticFeedbackRequests).hasSize(0); + } + + @Test public void testScrollProgress_progressEqualsOrExceedsPositiveThreshold() { setHapticScrollTickInterval(100); mProvider.onScrollProgress( @@ -198,6 +355,32 @@ public final class HapticScrollFeedbackProviderTest { } @Test + public void testScrollProgress_inputDeviceCustomized_progressEqualsOrExceedsPositiveThreshold() { + mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); + List<HapticFeedbackRequest> requests = new ArrayList<>(); + setHapticScrollTickInterval(100); + + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ 20); + + assertThat(mView.mHapticFeedbackRequests).hasSize(0); + + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ 80); + requests.add(new HapticFeedbackRequest( + SCROLL_TICK, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ 120); + requests.add(new HapticFeedbackRequest( + SCROLL_TICK, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + + assertThat(mView.mHapticFeedbackRequests).containsExactlyElementsIn(requests).inOrder(); + } + + @Test public void testScrollProgress_progressEqualsOrExceedsNegativeThreshold() { setHapticScrollTickInterval(100); @@ -224,6 +407,35 @@ public final class HapticScrollFeedbackProviderTest { } @Test + public void testScrollProgress_inputDeviceCustomized_progressEqualsOrExceedsNegativeThreshold() { + mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); + List<HapticFeedbackRequest> requests = new ArrayList<>(); + setHapticScrollTickInterval(100); + + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ -20); + + assertThat(mView.mHapticFeedbackRequests).hasSize(0); + + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ -80); + requests.add(new HapticFeedbackRequest( + SCROLL_TICK, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ -70); + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ -40); + requests.add(new HapticFeedbackRequest( + SCROLL_TICK, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + + assertThat(mView.mHapticFeedbackRequests).containsExactlyElementsIn(requests).inOrder(); + } + + @Test public void testScrollProgress_positiveAndNegativeProgresses() { setHapticScrollTickInterval(100); @@ -262,6 +474,54 @@ public final class HapticScrollFeedbackProviderTest { } @Test + public void testScrollProgress_inputDeviceCustomized_positiveAndNegativeProgresses() { + mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); + List<HapticFeedbackRequest> requests = new ArrayList<>(); + setHapticScrollTickInterval(100); + + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ 20); + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ -90); + + // total pixel abs = 70 + assertThat(mView.mHapticFeedbackRequests).hasSize(0); + + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ 10); + + // total pixel abs = 60 + assertThat(mView.mHapticFeedbackRequests).hasSize(0); + + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ -50); + // total pixel abs = 110. Passed threshold. total pixel reduced to -10. + requests.add(new HapticFeedbackRequest( + SCROLL_TICK, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + + assertThat(mView.mHapticFeedbackRequests).containsExactlyElementsIn(requests).inOrder(); + + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ 40); + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ 50); + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ 60); + // total pixel abs = 140. Passed threshold. total pixel reduced to 40. + requests.add(new HapticFeedbackRequest( + SCROLL_TICK, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + + assertThat(mView.mHapticFeedbackRequests).containsExactlyElementsIn(requests).inOrder(); + } + + @Test public void testScrollProgress_singleProgressExceedsThreshold() { setHapticScrollTickInterval(100); @@ -273,6 +533,21 @@ public final class HapticScrollFeedbackProviderTest { } @Test + public void testScrollProgress_inputDeviceCustomized_singleProgressExceedsThreshold() { + mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); + List<HapticFeedbackRequest> requests = new ArrayList<>(); + setHapticScrollTickInterval(100); + + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ 1000); + requests.add(new HapticFeedbackRequest( + SCROLL_TICK, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + + assertThat(mView.mHapticFeedbackRequests).containsExactlyElementsIn(requests).inOrder(); + } + + @Test public void testScrollLimit_startAndEndLimit_playsOnlyOneFeedback() { mProvider.onSnapToItem( INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); @@ -288,6 +563,29 @@ public final class HapticScrollFeedbackProviderTest { } @Test + public void testScrollLimit_startAndEndLimit_inputDeviceCustomized_playsOnlyOneFeedback() { + mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); + List<HapticFeedbackRequest> requests = new ArrayList<>(); + + mProvider.onSnapToItem( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); + requests.add(new HapticFeedbackRequest( + SCROLL_ITEM_FOCUS, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + // end played. + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + requests.add(new HapticFeedbackRequest( + SCROLL_LIMIT, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + // start after end NOT played. + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ true); + + assertThat(mView.mHapticFeedbackRequests).containsExactlyElementsIn(requests).inOrder(); + } + + @Test public void testScrollLimit_doubleStartLimit_playsOnlyOneFeedback() { mProvider.onSnapToItem( INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); @@ -303,6 +601,29 @@ public final class HapticScrollFeedbackProviderTest { } @Test + public void testScrollLimit_doubleStartLimit_inputDeviceCustomized_playsOnlyOneFeedback() { + mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); + List<HapticFeedbackRequest> requests = new ArrayList<>(); + + mProvider.onSnapToItem( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); + requests.add(new HapticFeedbackRequest( + SCROLL_ITEM_FOCUS, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + // 1st start played. + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ true); + requests.add(new HapticFeedbackRequest( + SCROLL_LIMIT, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + // 2nd start NOT played. + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ true); + + assertThat(mView.mHapticFeedbackRequests).containsExactlyElementsIn(requests).inOrder(); + } + + @Test public void testScrollLimit_doubleEndLimit_playsOnlyOneFeedback() { mProvider.onSnapToItem( INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); @@ -318,6 +639,29 @@ public final class HapticScrollFeedbackProviderTest { } @Test + public void testScrollLimit_doubleEndLimit_inputDeviceCustomized_playsOnlyOneFeedback() { + mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); + List<HapticFeedbackRequest> requests = new ArrayList<>(); + + mProvider.onSnapToItem( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); + requests.add(new HapticFeedbackRequest( + SCROLL_ITEM_FOCUS, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + // 1st end played. + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + requests.add(new HapticFeedbackRequest( + SCROLL_LIMIT, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + // 2nd end NOT played. + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + + assertThat(mView.mHapticFeedbackRequests).containsExactlyElementsIn(requests).inOrder(); + } + + @Test public void testScrollLimit_notEnabledWithZeroProgress() { mProvider.onSnapToItem( INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); @@ -339,6 +683,36 @@ public final class HapticScrollFeedbackProviderTest { } @Test + public void testScrollLimit_inputDeviceCustomized_notEnabledWithZeroProgress() { + mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); + List<HapticFeedbackRequest> requests = new ArrayList<>(); + + mProvider.onSnapToItem( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); + requests.add(new HapticFeedbackRequest( + SCROLL_ITEM_FOCUS, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + // end played. + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + requests.add(new HapticFeedbackRequest( + SCROLL_LIMIT, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + + // progress 0. scroll not started. + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ 0); + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ true); + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + + assertThat(mView.mHapticFeedbackRequests).containsExactlyElementsIn(requests).inOrder(); + } + + @Test public void testScrollLimit_enabledWithProgress() { mProvider.onSnapToItem( INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); @@ -357,6 +731,36 @@ public final class HapticScrollFeedbackProviderTest { } @Test + public void testScrollLimit_inputDeviceCustomized_enabledWithProgress() { + mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); + List<HapticFeedbackRequest> requests = new ArrayList<>(); + + mProvider.onSnapToItem( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); + requests.add(new HapticFeedbackRequest( + SCROLL_ITEM_FOCUS, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + // end played. + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + requests.add(new HapticFeedbackRequest( + SCROLL_LIMIT, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + // No tick since tick-interval is default 0, which means no tick. + // But still re-enable next limit feedback. + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ 80); + // scroll pixel not 0, so end played. + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + requests.add(new HapticFeedbackRequest( + SCROLL_LIMIT, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + + assertThat(mView.mHapticFeedbackRequests).containsExactlyElementsIn(requests).inOrder(); + } + + @Test public void testScrollLimit_enabledWithSnap() { mProvider.onSnapToItem( INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); @@ -374,6 +778,35 @@ public final class HapticScrollFeedbackProviderTest { } @Test + public void testScrollLimit_inputDeviceCustomized_enabledWithSnap() { + mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); + List<HapticFeedbackRequest> requests = new ArrayList<>(); + + // 1st enabled limit by snap + mProvider.onSnapToItem( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); + requests.add(new HapticFeedbackRequest( + SCROLL_ITEM_FOCUS, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + requests.add(new HapticFeedbackRequest( + SCROLL_LIMIT, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + // 2nd enabled limit by snap + mProvider.onSnapToItem( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); + requests.add(new HapticFeedbackRequest( + SCROLL_ITEM_FOCUS, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + requests.add(new HapticFeedbackRequest( + SCROLL_LIMIT, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + + assertThat(mView.mHapticFeedbackRequests).containsExactlyElementsIn(requests).inOrder(); + } + + @Test public void testScrollLimit_notEnabledWithDissimilarSnap() { mProvider.onSnapToItem( INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); @@ -391,6 +824,33 @@ public final class HapticScrollFeedbackProviderTest { } @Test + public void testScrollLimit_inputDeviceCustomized_notEnabledWithDissimilarSnap() { + mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); + List<HapticFeedbackRequest> requests = new ArrayList<>(); + + mProvider.onSnapToItem( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); + requests.add(new HapticFeedbackRequest( + SCROLL_ITEM_FOCUS, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + requests.add(new HapticFeedbackRequest( + SCROLL_LIMIT, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + + mProvider.onSnapToItem( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_X); + requests.add(new HapticFeedbackRequest( + SCROLL_ITEM_FOCUS, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + // Last snap is on AXIS_X, so end on AXIS_SCROLL is NOT played. + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + + assertThat(mView.mHapticFeedbackRequests).containsExactlyElementsIn(requests).inOrder(); + } + + @Test public void testScrollLimit_enabledWithDissimilarProgress() { mProvider.onSnapToItem( INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); @@ -408,6 +868,33 @@ public final class HapticScrollFeedbackProviderTest { assertFeedbackCount(mView, HapticFeedbackConstants.SCROLL_LIMIT, 2); } + @Test + public void testScrollLimit_inputDeviceCustomized_enabledWithDissimilarProgress() { + mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); + List<HapticFeedbackRequest> requests = new ArrayList<>(); + + mProvider.onSnapToItem( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); + requests.add(new HapticFeedbackRequest( + SCROLL_ITEM_FOCUS, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + requests.add(new HapticFeedbackRequest( + SCROLL_LIMIT, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + // No tick since tick-interval is default 0, which means no tick. + // But still re-enable next limit feedback. + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ 80); + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + requests.add(new HapticFeedbackRequest( + SCROLL_LIMIT, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + + assertThat(mView.mHapticFeedbackRequests).containsExactlyElementsIn(requests).inOrder(); + } @Test public void testScrollLimit_doesNotEnabledWithMotionFromDifferentDeviceId() { @@ -457,9 +944,65 @@ public final class HapticScrollFeedbackProviderTest { assertFeedbackCount(mView, HapticFeedbackConstants.SCROLL_TICK, 2); } + @Test + public void testScrollLimit_inputDeviceCustomized_doesNotEnabledWithMotionFromDifferentDeviceId() { + mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); + List<HapticFeedbackRequest> requests = new ArrayList<>(); + + mProvider.onSnapToItem( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); + requests.add(new HapticFeedbackRequest( + SCROLL_ITEM_FOCUS, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + requests.add(new HapticFeedbackRequest( + SCROLL_LIMIT, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + + mProvider.onSnapToItem( + INPUT_DEVICE_2, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); + requests.add(new HapticFeedbackRequest( + SCROLL_ITEM_FOCUS, INPUT_DEVICE_2, InputDevice.SOURCE_ROTARY_ENCODER)); + // last snap was for input device #2, so limit for input device #1 not re-enabled. + mProvider.onScrollLimit( + INPUT_DEVICE_1, + InputDevice.SOURCE_ROTARY_ENCODER, + MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + + assertThat(mView.mHapticFeedbackRequests).containsExactlyElementsIn(requests).inOrder(); + } + + @Test + public void testScrollLimit_inputDeviceCustomized_doesNotEnabledWithMotionFromDifferentSource() { + mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); + List<HapticFeedbackRequest> requests = new ArrayList<>(); + + mProvider.onSnapToItem( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); + requests.add(new HapticFeedbackRequest( + SCROLL_ITEM_FOCUS, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + requests.add(new HapticFeedbackRequest( + SCROLL_LIMIT, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + mProvider.onSnapToItem( + INPUT_DEVICE_1, InputDevice.SOURCE_TOUCHSCREEN, MotionEvent.AXIS_SCROLL); + requests.add(new HapticFeedbackRequest( + SCROLL_ITEM_FOCUS, INPUT_DEVICE_1, InputDevice.SOURCE_TOUCHSCREEN)); + // last snap was for input source touch screen, so rotary's limit is NOT re-enabled. + mProvider.onScrollLimit( + INPUT_DEVICE_1, + InputDevice.SOURCE_ROTARY_ENCODER, + MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + + assertThat(mView.mHapticFeedbackRequests).containsExactlyElementsIn(requests).inOrder(); + } private void assertNoFeedback(TestView view) { - for (int feedback : new int[] {SCROLL_ITEM_FOCUS, SCROLL_LIMIT, SCROLL_TICK}) { + for (int feedback : new int[]{SCROLL_ITEM_FOCUS, SCROLL_LIMIT, SCROLL_TICK}) { assertFeedbackCount(view, feedback, 0); } } @@ -469,7 +1012,7 @@ public final class HapticScrollFeedbackProviderTest { } private void assertOnlyFeedback(TestView view, int expectedFeedback, int expectedCount) { - for (int feedback : new int[] {SCROLL_ITEM_FOCUS, SCROLL_LIMIT, SCROLL_TICK}) { + for (int feedback : new int[]{SCROLL_ITEM_FOCUS, SCROLL_LIMIT, SCROLL_TICK}) { assertFeedbackCount(view, feedback, (feedback == expectedFeedback) ? expectedCount : 0); } } @@ -489,8 +1032,9 @@ public final class HapticScrollFeedbackProviderTest { .thenReturn(enabled); } - private static class TestView extends View { + private static class TestView extends View { final Map<Integer, Integer> mFeedbackCount = new HashMap<>(); + final List<HapticFeedbackRequest> mHapticFeedbackRequests = new ArrayList<>(); TestView(Context context) { super(context); @@ -504,5 +1048,47 @@ public final class HapticScrollFeedbackProviderTest { mFeedbackCount.put(feedback, mFeedbackCount.get(feedback) + 1); return true; } + + @Override + public void performHapticFeedbackForInputDevice(int feedback, int inputDeviceId, + int inputSource, int flags) { + mHapticFeedbackRequests.add( + new HapticFeedbackRequest(feedback, inputDeviceId, inputSource)); + } + } + + private static class HapticFeedbackRequest { + // <feedback, inputDeviceId, inputSource> + private final int[] mArgs = new int[3]; + + private HapticFeedbackRequest(int feedback, int inputDeviceId, int inputSource) { + mArgs[0] = feedback; + mArgs[1] = inputDeviceId; + mArgs[2] = inputSource; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + HapticFeedbackRequest other = (HapticFeedbackRequest) obj; + return Arrays.equals(this.mArgs, other.mArgs); + } + + @Override + public int hashCode() { + // Shouldn't depend on hash. Should explicitly match mArgs. + return Objects.hash(mArgs[0], mArgs[1], mArgs[2]); + } + + @Override + public String toString() { + return String.format("<feedback=%d; inputDeviceId=%d; inputSource=%d>", + mArgs[0], mArgs[1], mArgs[2]); + } } }
\ No newline at end of file diff --git a/core/tests/coretests/src/android/view/ViewRootImplTest.java b/core/tests/coretests/src/android/view/ViewRootImplTest.java index c5b75ff50da7..e240a0853f46 100644 --- a/core/tests/coretests/src/android/view/ViewRootImplTest.java +++ b/core/tests/coretests/src/android/view/ViewRootImplTest.java @@ -18,6 +18,7 @@ package android.view; import static android.util.SequenceUtils.getInitSeq; import static android.view.HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING; +import static android.view.InputDevice.SOURCE_ROTARY_ENCODER; import static android.view.Surface.FRAME_RATE_CATEGORY_DEFAULT; import static android.view.Surface.FRAME_RATE_CATEGORY_HIGH; import static android.view.Surface.FRAME_RATE_CATEGORY_HIGH_HINT; @@ -501,6 +502,20 @@ public class ViewRootImplTest { assertThat(result).isFalse(); } + @UiThreadTest + @Test + public void performHapticFeedbackForInputDevice_touchFeedbackDisabled_doNothing() { + DisplayInfo displayInfo = new DisplayInfo(); + displayInfo.flags = Display.FLAG_TOUCH_FEEDBACK_DISABLED; + Display display = new Display(DisplayManagerGlobal.getInstance(), /* displayId= */ + 0, displayInfo, new DisplayAdjustments()); + ViewRootImpl viewRootImpl = new ViewRootImpl(sContext, display); + + viewRootImpl.performHapticFeedbackForInputDevice(HapticFeedbackConstants.CONTEXT_CLICK, + 1 /* inputDeviceId */, SOURCE_ROTARY_ENCODER /* inputSource */, + FLAG_IGNORE_GLOBAL_SETTING, 0 /* privFlags */); + } + /** * Test the default values are properly set */ @@ -1419,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/android/widget/RemoteViewsSerializersTest.kt b/core/tests/coretests/src/android/widget/RemoteViewsSerializersTest.kt index 44d10d32606c..b999df4da33b 100644 --- a/core/tests/coretests/src/android/widget/RemoteViewsSerializersTest.kt +++ b/core/tests/coretests/src/android/widget/RemoteViewsSerializersTest.kt @@ -21,17 +21,121 @@ import android.content.res.ColorStateList import android.graphics.Bitmap import android.graphics.BlendMode import android.graphics.Color +import android.graphics.Typeface import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Icon +import android.graphics.text.LineBreakConfig +import android.os.LocaleList +import android.text.Layout +import android.text.ParcelableSpan +import android.text.SpannableStringBuilder +import android.text.Spanned +import android.text.TextPaint +import android.text.style.AbsoluteSizeSpan +import android.text.style.AccessibilityClickableSpan +import android.text.style.AccessibilityReplacementSpan +import android.text.style.AccessibilityURLSpan +import android.text.style.AlignmentSpan +import android.text.style.BackgroundColorSpan +import android.text.style.BulletSpan +import android.text.style.EasyEditSpan +import android.text.style.ForegroundColorSpan +import android.text.style.LeadingMarginSpan +import android.text.style.LineBackgroundSpan +import android.text.style.LineBreakConfigSpan +import android.text.style.LineHeightSpan +import android.text.style.LocaleSpan +import android.text.style.QuoteSpan +import android.text.style.RelativeSizeSpan +import android.text.style.ScaleXSpan +import android.text.style.SpellCheckSpan +import android.text.style.StrikethroughSpan +import android.text.style.StyleSpan +import android.text.style.SubscriptSpan +import android.text.style.SuggestionRangeSpan +import android.text.style.SuggestionSpan +import android.text.style.SuperscriptSpan +import android.text.style.TextAppearanceSpan +import android.text.style.TtsSpan +import android.text.style.TypefaceSpan +import android.text.style.URLSpan +import android.text.style.UnderlineSpan import android.util.proto.ProtoInputStream import android.util.proto.ProtoOutputStream +import android.widget.RemoteViewsSerializers.createAbsoluteSizeSpanFromProto +import android.widget.RemoteViewsSerializers.createAccessibilityClickableSpanFromProto +import android.widget.RemoteViewsSerializers.createAccessibilityReplacementSpanFromProto +import android.widget.RemoteViewsSerializers.createAccessibilityURLSpanFromProto +import android.widget.RemoteViewsSerializers.createAnnotationFromProto +import android.widget.RemoteViewsSerializers.createBackgroundColorSpanFromProto +import android.widget.RemoteViewsSerializers.createBulletSpanFromProto +import android.widget.RemoteViewsSerializers.createCharSequenceFromProto +import android.widget.RemoteViewsSerializers.createEasyEditSpanFromProto +import android.widget.RemoteViewsSerializers.createForegroundColorSpanFromProto import android.widget.RemoteViewsSerializers.createIconFromProto +import android.widget.RemoteViewsSerializers.createLeadingMarginSpanStandardFromProto +import android.widget.RemoteViewsSerializers.createLineBackgroundSpanStandardFromProto +import android.widget.RemoteViewsSerializers.createLineBreakConfigSpanFromProto +import android.widget.RemoteViewsSerializers.createLineHeightSpanStandardFromProto +import android.widget.RemoteViewsSerializers.createLocaleSpanFromProto +import android.widget.RemoteViewsSerializers.createQuoteSpanFromProto +import android.widget.RemoteViewsSerializers.createRelativeSizeSpanFromProto +import android.widget.RemoteViewsSerializers.createScaleXSpanFromProto +import android.widget.RemoteViewsSerializers.createStrikethroughSpanFromProto +import android.widget.RemoteViewsSerializers.createStyleSpanFromProto +import android.widget.RemoteViewsSerializers.createSubscriptSpanFromProto +import android.widget.RemoteViewsSerializers.createSuggestionRangeSpanFromProto +import android.widget.RemoteViewsSerializers.createSuggestionSpanFromProto +import android.widget.RemoteViewsSerializers.createSuperscriptSpanFromProto +import android.widget.RemoteViewsSerializers.createTextAppearanceSpanFromProto +import android.widget.RemoteViewsSerializers.createTtsSpanFromProto +import android.widget.RemoteViewsSerializers.createTypefaceSpanFromProto +import android.widget.RemoteViewsSerializers.createURLSpanFromProto +import android.widget.RemoteViewsSerializers.createUnderlineSpanFromProto +import android.widget.RemoteViewsSerializers.writeAbsoluteSizeSpanToProto +import android.widget.RemoteViewsSerializers.writeAccessibilityClickableSpanToProto +import android.widget.RemoteViewsSerializers.writeAccessibilityReplacementSpanToProto +import android.widget.RemoteViewsSerializers.writeAccessibilityURLSpanToProto +import android.widget.RemoteViewsSerializers.writeAlignmentSpanStandardToProto +import android.widget.RemoteViewsSerializers.writeAnnotationToProto +import android.widget.RemoteViewsSerializers.writeBackgroundColorSpanToProto +import android.widget.RemoteViewsSerializers.writeBulletSpanToProto +import android.widget.RemoteViewsSerializers.writeCharSequenceToProto +import android.widget.RemoteViewsSerializers.writeEasyEditSpanToProto +import android.widget.RemoteViewsSerializers.writeForegroundColorSpanToProto import android.widget.RemoteViewsSerializers.writeIconToProto +import android.widget.RemoteViewsSerializers.writeLeadingMarginSpanStandardToProto +import android.widget.RemoteViewsSerializers.writeLineBackgroundSpanStandardToProto +import android.widget.RemoteViewsSerializers.writeLineBreakConfigSpanToProto +import android.widget.RemoteViewsSerializers.writeLineHeightSpanStandardToProto +import android.widget.RemoteViewsSerializers.writeLocaleSpanToProto +import android.widget.RemoteViewsSerializers.writeQuoteSpanToProto +import android.widget.RemoteViewsSerializers.writeRelativeSizeSpanToProto +import android.widget.RemoteViewsSerializers.writeScaleXSpanToProto +import android.widget.RemoteViewsSerializers.writeStrikethroughSpanToProto +import android.widget.RemoteViewsSerializers.writeStyleSpanToProto +import android.widget.RemoteViewsSerializers.writeSubscriptSpanToProto +import android.widget.RemoteViewsSerializers.writeSuggestionRangeSpanToProto +import android.widget.RemoteViewsSerializers.writeSuggestionSpanToProto +import android.widget.RemoteViewsSerializers.writeSuperscriptSpanToProto +import android.widget.RemoteViewsSerializers.writeTextAppearanceSpanToProto +import android.widget.RemoteViewsSerializers.writeTtsSpanToProto +import android.widget.RemoteViewsSerializers.writeTypefaceSpanToProto +import android.widget.RemoteViewsSerializers.writeURLSpanToProto +import android.widget.RemoteViewsSerializers.writeUnderlineSpanToProto +import androidx.core.os.persistableBundleOf import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.frameworks.coretests.R import com.google.common.truth.Truth.assertThat import java.io.ByteArrayOutputStream +import java.util.Locale +import kotlin.random.Random +import kotlin.test.assertIs +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue import org.junit.Test import org.junit.runner.RunWith @@ -84,6 +188,511 @@ class RemoteViewsSerializersTest { } } } + + @Test + fun testWriteToProto() { + // This test checks that all of the supported spans are written with their start, end and + // flags. Span-specific data is tested in other tests. + val string = "0123456789" + data class SpanSpec( + val span: ParcelableSpan, + val start: Int = Random.nextInt(0, string.length), + val end: Int = Random.nextInt(start, string.length), + val flags: Int = Random.nextInt(0, 256).shl(Spanned.SPAN_USER_SHIFT), + ) + + val specs = listOf( + AbsoluteSizeSpan(0), + AccessibilityClickableSpan(0), + AccessibilityReplacementSpan(null as String?), + AccessibilityURLSpan(URLSpan(null)), + AlignmentSpan.Standard(Layout.Alignment.ALIGN_LEFT), + android.text.Annotation(null, null), + BackgroundColorSpan(0), + BulletSpan(0), + EasyEditSpan(), + ForegroundColorSpan(0), + LeadingMarginSpan.Standard(0), + LineBackgroundSpan.Standard(0), + LineBreakConfigSpan(LineBreakConfig.NONE), + LineHeightSpan.Standard(1), + LocaleSpan(LocaleList.getDefault()), + QuoteSpan(), + RelativeSizeSpan(0f), + ScaleXSpan(0f), + SpellCheckSpan(), + StrikethroughSpan(), + StyleSpan(0), + SubscriptSpan(), + SuggestionRangeSpan(), + SuggestionSpan(context, arrayOf(), 0), + SuperscriptSpan(), + TextAppearanceSpan(context, android.R.style.TextAppearance), + TtsSpan(null, persistableBundleOf()), + TypefaceSpan(null), + UnderlineSpan(), + URLSpan(null), + ).map { SpanSpec(it) } + + val original = SpannableStringBuilder(string) + for (spec in specs) { + original.setSpan(spec.span, spec.start, spec.end, spec.flags) + } + + val out = ProtoOutputStream() + writeCharSequenceToProto(out, original) + val input = ProtoInputStream(out.bytes) + val copy = createCharSequenceFromProto(input) + + assertIs<Spanned>(copy) + for (spec in specs) { + val spans = copy.getSpans(spec.start, spec.end, Object::class.java) + android.util.Log.e("TestRunner", "Can I find $spec") + val span = spans.single { spec.span::class.java.name == it::class.java.name } + assertEquals(spec.flags, copy.getSpanFlags(span)) + } + } + + @Test + fun writeToProto_notSpanned() { + val string = "Hello World" + val out = ProtoOutputStream() + writeCharSequenceToProto(out, string) + val input = ProtoInputStream(out.bytes) + val copy = createCharSequenceFromProto(input) + assertIs<String>(copy) + assertEquals(copy, string) + } + + @Test + fun testAbsoluteSizeSpan() { + for (span in arrayOf(AbsoluteSizeSpan(0, false), AbsoluteSizeSpan(2, true))) { + val out = ProtoOutputStream() + writeAbsoluteSizeSpanToProto(out, span) + val input = ProtoInputStream(out.bytes) + val copy = createAbsoluteSizeSpanFromProto(input) + assertEquals(span.size, copy.size) + assertEquals(span.dip, copy.dip) + } + } + + @Test + fun testAccessibilityClickableSpan() { + for (id in 0..1) { + val span = AccessibilityClickableSpan(id) + val out = ProtoOutputStream() + writeAccessibilityClickableSpanToProto(out, span) + val input = ProtoInputStream(out.bytes) + val copy = createAccessibilityClickableSpanFromProto(input) + assertEquals(span.originalClickableSpanId, copy.originalClickableSpanId) + } + } + + @Test + fun testAccessibilityReplacementSpan() { + for (contentDescription in arrayOf(null, "123")) { + val span = AccessibilityReplacementSpan(contentDescription) + val out = ProtoOutputStream() + writeAccessibilityReplacementSpanToProto(out, span) + val input = ProtoInputStream(out.bytes) + val copy = createAccessibilityReplacementSpanFromProto(input) + assertEquals(span.contentDescription, copy.contentDescription) + } + } + + @Test + fun testAccessibilityURLSpan() { + for (url in arrayOf(null, "123")) { + val span = AccessibilityURLSpan(URLSpan(url)) + val out = ProtoOutputStream() + writeAccessibilityURLSpanToProto(out, span) + val input = ProtoInputStream(out.bytes) + val copy = createAccessibilityURLSpanFromProto(input) + assertEquals(span.url, copy.url) + } + } + + @Test + fun testAlignmentSpanStandard() { + for (alignment in arrayOf( + Layout.Alignment.ALIGN_CENTER, + Layout.Alignment.ALIGN_LEFT, + Layout.Alignment.ALIGN_NORMAL, + Layout.Alignment.ALIGN_OPPOSITE)) { + val span = AlignmentSpan.Standard(alignment) + val out = ProtoOutputStream() + writeAlignmentSpanStandardToProto(out, span) + val input = ProtoInputStream(out.bytes) + val copy = RemoteViewsSerializers.createAlignmentSpanStandardFromProto(input) + assertEquals(span.alignment, copy.alignment) + } + } + + @Test + fun testAnnotation() { + for ((key, value) in arrayOf(null to null, "key" to "value")) { + val span = android.text.Annotation(key, value) + val out = ProtoOutputStream() + writeAnnotationToProto(out, span) + val input = ProtoInputStream(out.bytes) + val copy = createAnnotationFromProto(input) + assertEquals(span.key, copy.key) + assertEquals(span.value, copy.value) + } + } + + @Test + fun testBackgroundColorSpan() { + for (color in intArrayOf(Color.RED, Color.MAGENTA)) { + val span = BackgroundColorSpan(color) + val out = ProtoOutputStream() + writeBackgroundColorSpanToProto(out, span) + val input = ProtoInputStream(out.bytes) + val copy = createBackgroundColorSpanFromProto(input) + assertEquals(span.backgroundColor, copy.backgroundColor) + } + } + + @Test + fun testBulletSpan() { + for (span in arrayOf(BulletSpan(), BulletSpan(2, Color.RED, 5))) { + val out = ProtoOutputStream() + writeBulletSpanToProto(out, span) + val input = ProtoInputStream(out.bytes) + val copy = createBulletSpanFromProto(input) + assertEquals(span.getLeadingMargin(true), copy.getLeadingMargin(true)) + assertEquals(span.color, copy.color) + assertEquals(span.color, copy.color) + assertEquals(span.gapWidth, copy.gapWidth) + } + } + + @Test + fun testEasyEditSpan() { + val span = EasyEditSpan() + val out = ProtoOutputStream() + writeEasyEditSpanToProto(out, span) + val input = ProtoInputStream(out.bytes) + createEasyEditSpanFromProto(input) + } + + @Test + fun testForegroundColorSpan() { + for (color in intArrayOf(0, Color.RED, Color.MAGENTA)) { + val span = ForegroundColorSpan(color) + val out = ProtoOutputStream() + writeForegroundColorSpanToProto(out, span) + val input = ProtoInputStream(out.bytes) + val copy = createForegroundColorSpanFromProto(input) + assertEquals(span.foregroundColor.toLong(), copy.foregroundColor.toLong()) + } + } + + @Test + fun testLeadingMarginSpanStandard() { + for (span in arrayOf(LeadingMarginSpan.Standard(10, 20), LeadingMarginSpan.Standard(0))) { + val out = ProtoOutputStream() + writeLeadingMarginSpanStandardToProto(out, span) + val input = ProtoInputStream(out.bytes) + val copy = createLeadingMarginSpanStandardFromProto(input) + assertEquals(span.getLeadingMargin(true), copy.getLeadingMargin(true)) + assertEquals(span.getLeadingMargin(false), copy.getLeadingMargin(false)) + } + } + + @Test + fun testLineBackgroundSpan() { + for (color in intArrayOf(0, Color.RED, Color.MAGENTA)) { + val span = LineBackgroundSpan.Standard(color) + val out = ProtoOutputStream() + writeLineBackgroundSpanStandardToProto(out, span) + val input = ProtoInputStream(out.bytes) + val copy = createLineBackgroundSpanStandardFromProto(input) + assertEquals(span.color, copy.color) + } + } + + @Test + fun testLineBreakConfigSpan() { + val config = LineBreakConfig.Builder() + .setLineBreakStyle(LineBreakConfig.LINE_BREAK_STYLE_STRICT) + .setLineBreakWordStyle(LineBreakConfig.LINE_BREAK_WORD_STYLE_AUTO) + .setHyphenation(LineBreakConfig.HYPHENATION_ENABLED) + .build() + val span = LineBreakConfigSpan(config) + val out = ProtoOutputStream() + writeLineBreakConfigSpanToProto(out, span) + val input = ProtoInputStream(out.bytes) + val copy = createLineBreakConfigSpanFromProto(input).lineBreakConfig + assertEquals(copy.lineBreakStyle, config.lineBreakStyle) + assertEquals(copy.lineBreakWordStyle, config.lineBreakWordStyle) + assertEquals(copy.hyphenation, config.hyphenation) + } + + @Test + fun testLineHeightSpanStandard() { + for (height in 1..2) { + val span = LineHeightSpan.Standard(height) + val out = ProtoOutputStream() + writeLineHeightSpanStandardToProto(out, span) + val input = ProtoInputStream(out.bytes) + val copy = createLineHeightSpanStandardFromProto(input) + assertEquals(span.height, copy.height) + } + } + + @Test + fun testLocaleSpan() { + for (list in arrayOf( + LocaleList.getEmptyLocaleList(), + LocaleList.forLanguageTags("en"), + LocaleList.forLanguageTags("en-GB,en"), + )) { + val span = LocaleSpan(list) + val out = ProtoOutputStream() + writeLocaleSpanToProto(out, span) + val input = ProtoInputStream(out.bytes) + val copy = createLocaleSpanFromProto(input) + assertEquals(span.locales[0], copy.locale) + assertEquals(span.locales, copy.locales) + } + } + + @Test + fun testQuoteSpan() { + for (color in intArrayOf(0, Color.RED, Color.MAGENTA)) { + val span = QuoteSpan(color) + val out = ProtoOutputStream() + writeQuoteSpanToProto(out, span) + val input = ProtoInputStream(out.bytes) + val copy = createQuoteSpanFromProto(input) + assertEquals(span.color, copy.color) + assertTrue(span.gapWidth > 0) + assertTrue(span.stripeWidth > 0) + } + } + + @Test + fun testRelativeSizeSpan() { + for (size in arrayOf(0f, 1.0f)) { + val span = RelativeSizeSpan(size) + val out = ProtoOutputStream() + writeRelativeSizeSpanToProto(out, span) + val input = ProtoInputStream(out.bytes) + val copy = createRelativeSizeSpanFromProto(input) + assertEquals(span.sizeChange, copy.sizeChange) + } + } + + @Test + fun testScaleXSpan() { + for (scale in arrayOf(0f, 1.0f)) { + val span = ScaleXSpan(scale) + val out = ProtoOutputStream() + writeScaleXSpanToProto(out, span) + val input = ProtoInputStream(out.bytes) + val copy = createScaleXSpanFromProto(input) + assertEquals(span.scaleX, copy.scaleX, 0.0f) + } + } + + @Test + fun testStrikethroughSpan() { + val span = StrikethroughSpan() + val out = ProtoOutputStream() + writeStrikethroughSpanToProto(out, span) + val input = ProtoInputStream(out.bytes) + createStrikethroughSpanFromProto(input) + } + + @Test + fun testStyleSpan() { + for (style in arrayOf(Typeface.BOLD, Typeface.NORMAL)) { + val span = StyleSpan(style) + val out = ProtoOutputStream() + writeStyleSpanToProto(out, span) + val input = ProtoInputStream(out.bytes) + val copy = createStyleSpanFromProto(input) + assertEquals(span.style, copy.style) + } + } + + @Test + fun testSubscriptSpan() { + val span = SubscriptSpan() + val out = ProtoOutputStream() + writeSubscriptSpanToProto(out, span) + val input = ProtoInputStream(out.bytes) + createSubscriptSpanFromProto(input) + } + + @Test + fun testSuggestionSpan() { + val suggestions = arrayOf("suggestion1", "suggestion2") + val span = SuggestionSpan( + Locale.forLanguageTag("en"), suggestions, + SuggestionSpan.FLAG_AUTO_CORRECTION) + + val out = ProtoOutputStream() + writeSuggestionSpanToProto(out, span) + val input = ProtoInputStream(out.bytes) + val copy = createSuggestionSpanFromProto(input) + assertArrayEquals("Should (de)serialize suggestions", + suggestions, copy.suggestions) + } + + @Test + fun testSuggestionRangeSpan() { + for (backgroundColor in 0..1) { + val span = SuggestionRangeSpan() + span.backgroundColor = backgroundColor + val out = ProtoOutputStream() + writeSuggestionRangeSpanToProto(out, span) + val input = ProtoInputStream(out.bytes) + val copy = createSuggestionRangeSpanFromProto(input) + assertEquals(span.backgroundColor, copy.backgroundColor) + } + } + + @Test + fun testSuperscriptSpan() { + val span = SuperscriptSpan() + val out = ProtoOutputStream() + writeSuperscriptSpanToProto(out, span) + val input = ProtoInputStream(out.bytes) + createSuperscriptSpanFromProto(input) + } + + + @Test + fun testTextAppearanceSpan_FontResource() { + val span = TextAppearanceSpan(context, R.style.customFont) + val out = ProtoOutputStream() + writeTextAppearanceSpanToProto(out, span) + val input = ProtoInputStream(out.bytes) + val copy = createTextAppearanceSpanFromProto(input) + val tp = TextPaint() + span.updateDrawState(tp) + val originalSpanTextWidth = tp.measureText("a") + copy.updateDrawState(tp) + assertEquals(originalSpanTextWidth, tp.measureText("a"), 0.0f) + } + + @Test + fun testTextAppearanceSpan_FontResource_WithStyle() { + val span = TextAppearanceSpan(context, R.style.customFontWithStyle) + val out = ProtoOutputStream() + writeTextAppearanceSpanToProto(out, span) + val input = ProtoInputStream(out.bytes) + val copy = createTextAppearanceSpanFromProto(input) + val tp = TextPaint() + span.updateDrawState(tp) + val originalSpanTextWidth = tp.measureText("a") + copy.updateDrawState(tp) + assertEquals(originalSpanTextWidth, tp.measureText("a"), 0.0f) + } + + @Test + fun testTextAppearanceSpan_WithAllAttributes() { + val span = TextAppearanceSpan(context, R.style.textAppearanceWithAllAttributes) + val out = ProtoOutputStream() + writeTextAppearanceSpanToProto(out, span) + val input = ProtoInputStream(out.bytes) + val copy = createTextAppearanceSpanFromProto(input) + val originalTextColor = span.textColor + val copyTextColor = copy.textColor + val originalLinkTextColor = span.linkTextColor + val copyLinkTextColor = copy.linkTextColor + assertEquals(span.family, copy.family) + // ColorStateList doesn't implement equals(), so we borrow this code + // from ColorStateListTest.java to test correctness of parceling. + assertEquals(originalTextColor.isStateful, copyTextColor.isStateful) + assertEquals(originalTextColor.defaultColor, copyTextColor.defaultColor) + assertEquals(originalLinkTextColor.isStateful, + copyLinkTextColor.isStateful) + assertEquals(originalLinkTextColor.defaultColor, + copyLinkTextColor.defaultColor) + assertEquals(span.textSize.toLong(), copy.textSize.toLong()) + assertEquals(span.textStyle.toLong(), copy.textStyle.toLong()) + assertEquals(span.textFontWeight.toLong(), copy.textFontWeight.toLong()) + assertEquals(span.textLocales, copy.textLocales) + assertEquals(span.shadowColor.toLong(), copy.shadowColor.toLong()) + assertEquals(span.shadowDx, copy.shadowDx, 0.0f) + assertEquals(span.shadowDy, copy.shadowDy, 0.0f) + assertEquals(span.shadowRadius, copy.shadowRadius, 0.0f) + assertEquals(span.fontFeatureSettings, copy.fontFeatureSettings) + assertEquals(span.fontVariationSettings, copy.fontVariationSettings) + assertEquals(span.isElegantTextHeight, copy.isElegantTextHeight) + assertEquals(span.letterSpacing, copy.letterSpacing, 0f) + // typeface is omitted from TextAppearanceSpan proto + } + + @Test + fun testTtsSpan() { + val bundle = persistableBundleOf( + "argument.one" to "value.one", + "argument.two" to "value.two", + "argument.three" to 3L, + "argument.four" to 4L, + ) + val span = TtsSpan("test.type.five", bundle) + val out = ProtoOutputStream() + writeTtsSpanToProto(out, span) + val input = ProtoInputStream(out.bytes) + val copy = createTtsSpanFromProto(input) + assertEquals("test.type.five", copy.type) + val args = copy.args + assertEquals(4, args.size()) + assertEquals("value.one", args.getString("argument.one")) + assertEquals("value.two", args.getString("argument.two")) + assertEquals(3, args.getLong("argument.three")) + assertEquals(4, args.getLong("argument.four")) + } + + + @Test + fun testTtsSpan_null() { + val span = TtsSpan(null, null) + val out = ProtoOutputStream() + writeTtsSpanToProto(out, span) + val input = ProtoInputStream(out.bytes) + val copy = createTtsSpanFromProto(input) + assertNull(copy.type) + assertNull(copy.args) + } + + @Test + fun testTypefaceSpan() { + for (family in arrayOf(null, "monospace")) { + val span = TypefaceSpan(family) + val out = ProtoOutputStream() + writeTypefaceSpanToProto(out, span) + val input = ProtoInputStream(out.bytes) + val copy = createTypefaceSpanFromProto(input) + assertEquals(span.family, copy.family) + } + } + + @Test + fun testUnderlineSpan() { + val span = UnderlineSpan() + val out = ProtoOutputStream() + writeUnderlineSpanToProto(out, span) + val input = ProtoInputStream(out.bytes) + createUnderlineSpanFromProto(input) + } + + @Test + fun testURLSpan() { + for (url in arrayOf(null, "content://url")) { + val span = URLSpan(url) + val out = ProtoOutputStream() + writeURLSpanToProto(out, span) + val input = ProtoInputStream(out.bytes) + val copy = createURLSpanFromProto(input) + assertEquals(span.url, copy.url) + } + } } fun equalColorStateLists(a: ColorStateList?, b: ColorStateList?): Boolean { diff --git a/core/tests/coretests/src/android/window/SnapshotDrawerUtilsTest.java b/core/tests/coretests/src/android/window/SnapshotDrawerUtilsTest.java index 6c8dcd39e223..fdc00ba65255 100644 --- a/core/tests/coretests/src/android/window/SnapshotDrawerUtilsTest.java +++ b/core/tests/coretests/src/android/window/SnapshotDrawerUtilsTest.java @@ -93,7 +93,8 @@ public class SnapshotDrawerUtilsTest { ColorSpace.get(ColorSpace.Named.SRGB), ORIENTATION_PORTRAIT, Surface.ROTATION_0, taskSize, contentInsets, new Rect() /* letterboxInsets */, false, true /* isRealSnapshot */, WINDOWING_MODE_FULLSCREEN, - 0 /* systemUiVisibility */, false /* isTranslucent */, false /* hasImeSurface */); + 0 /* systemUiVisibility */, false /* isTranslucent */, false /* hasImeSurface */, + 0 /* uiMode */); } private static TaskDescription createTaskDescription(int background, 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/core/tests/resourceflaggingtests/Android.bp b/core/tests/resourceflaggingtests/Android.bp index dd86094127da..efb843735aef 100644 --- a/core/tests/resourceflaggingtests/Android.bp +++ b/core/tests/resourceflaggingtests/Android.bp @@ -22,54 +22,6 @@ package { default_team: "trendy_team_android_resources", } -genrule { - name: "resource-flagging-test-app-resources-compile", - tools: ["aapt2"], - srcs: [ - "flagged_resources_res/values/bools.xml", - ], - out: ["values_bools.arsc.flat"], - cmd: "$(location aapt2) compile $(in) -o $(genDir) " + - "--feature-flags test.package.falseFlag:ro=false,test.package.trueFlag:ro=true", -} - -genrule { - name: "resource-flagging-test-app-resources-compile2", - tools: ["aapt2"], - srcs: [ - "flagged_resources_res/values/bools2.xml", - ], - out: ["values_bools2.arsc.flat"], - cmd: "$(location aapt2) compile $(in) -o $(genDir) " + - "--feature-flags test.package.falseFlag:ro=false,test.package.trueFlag:ro=true", -} - -genrule { - name: "resource-flagging-test-app-apk", - tools: ["aapt2"], - // The first input file in the list must be the manifest - srcs: [ - "TestAppAndroidManifest.xml", - ":resource-flagging-test-app-resources-compile", - ":resource-flagging-test-app-resources-compile2", - ], - out: ["resapp.apk"], - cmd: "$(location aapt2) link -o $(out) --manifest $(in)", -} - -java_genrule { - name: "resource-flagging-apk-as-resource", - srcs: [ - ":resource-flagging-test-app-apk", - ], - out: ["apks_as_resources.res.zip"], - tools: ["soong_zip"], - - cmd: "mkdir -p $(genDir)/res/raw && " + - "cp $(in) $(genDir)/res/raw/$$(basename $(in)) && " + - "$(location soong_zip) -o $(out) -C $(genDir)/res -D $(genDir)/res", -} - android_test { name: "ResourceFlaggingTests", srcs: [ @@ -82,6 +34,6 @@ android_test { "testng", "compatibility-device-util-axt", ], - resource_zips: [":resource-flagging-apk-as-resource"], + resource_zips: [":resource-flagging-test-app-apk-as-resource"], test_suites: ["device-tests"], } diff --git a/core/tests/resourceflaggingtests/src/com/android/resourceflaggingtests/ResourceFlaggingTest.java b/core/tests/resourceflaggingtests/src/com/android/resourceflaggingtests/ResourceFlaggingTest.java index ad8542e0f6b3..c1e357864fff 100644 --- a/core/tests/resourceflaggingtests/src/com/android/resourceflaggingtests/ResourceFlaggingTest.java +++ b/core/tests/resourceflaggingtests/src/com/android/resourceflaggingtests/ResourceFlaggingTest.java @@ -69,11 +69,23 @@ public class ResourceFlaggingTest { } private boolean getBoolean(String name) { - int resId = mResources.getIdentifier(name, "bool", "com.android.intenal.flaggedresources"); + int resId = mResources.getIdentifier( + name, + "bool", + "com.android.intenal.flaggedresources"); assertThat(resId).isNotEqualTo(0); return mResources.getBoolean(resId); } + private String getString(String name) { + int resId = mResources.getIdentifier( + name, + "string", + "com.android.intenal.flaggedresources"); + assertThat(resId).isNotEqualTo(0); + return mResources.getString(resId); + } + private String extractApkAndGetPath(int id) throws Exception { final Resources resources = mContext.getResources(); try (InputStream is = resources.openRawResource(id)) { diff --git a/core/tests/utiltests/src/com/android/internal/util/ArrayUtilsTest.java b/core/tests/utiltests/src/com/android/internal/util/ArrayUtilsTest.java index fc233fba082e..3b9f35b1eb68 100644 --- a/core/tests/utiltests/src/com/android/internal/util/ArrayUtilsTest.java +++ b/core/tests/utiltests/src/com/android/internal/util/ArrayUtilsTest.java @@ -161,15 +161,15 @@ public class ArrayUtilsTest { } @Test - public void testAppendBoolean() throws Exception { + public void testAppendBooleanDuplicatesAllowed() throws Exception { assertArrayEquals(new boolean[] { true }, - ArrayUtils.appendBoolean(null, true)); + ArrayUtils.appendBooleanDuplicatesAllowed(null, true)); assertArrayEquals(new boolean[] { true }, - ArrayUtils.appendBoolean(new boolean[] { }, true)); + ArrayUtils.appendBooleanDuplicatesAllowed(new boolean[] { }, true)); assertArrayEquals(new boolean[] { true, false }, - ArrayUtils.appendBoolean(new boolean[] { true }, false)); + ArrayUtils.appendBooleanDuplicatesAllowed(new boolean[] { true }, false)); assertArrayEquals(new boolean[] { true, true }, - ArrayUtils.appendBoolean(new boolean[] { true }, true)); + ArrayUtils.appendBooleanDuplicatesAllowed(new boolean[] { true }, true)); } @Test diff --git a/core/tests/vibrator/src/android/os/vibrator/PrimitiveSegmentTest.java b/core/tests/vibrator/src/android/os/vibrator/PrimitiveSegmentTest.java index e9a08aef4856..97f1d5e77ddb 100644 --- a/core/tests/vibrator/src/android/os/vibrator/PrimitiveSegmentTest.java +++ b/core/tests/vibrator/src/android/os/vibrator/PrimitiveSegmentTest.java @@ -27,7 +27,11 @@ import android.hardware.vibrator.IVibrator; import android.os.Parcel; import android.os.VibrationEffect; import android.os.VibratorInfo; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -36,6 +40,9 @@ import org.junit.runners.JUnit4; public class PrimitiveSegmentTest { private static final float TOLERANCE = 1e-2f; + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @Test public void testCreation() { PrimitiveSegment primitive = new PrimitiveSegment( @@ -87,7 +94,8 @@ public class PrimitiveSegmentTest { } @Test - public void testScale_fullPrimitiveScaleValue() { + @DisableFlags(Flags.FLAG_HAPTICS_SCALE_V2_ENABLED) + public void testScale_withLegacyScaling_fullPrimitiveScaleValue() { PrimitiveSegment initial = new PrimitiveSegment( VibrationEffect.Composition.PRIMITIVE_CLICK, 1, 0); @@ -102,7 +110,24 @@ public class PrimitiveSegmentTest { } @Test - public void testScale_halfPrimitiveScaleValue() { + @EnableFlags(Flags.FLAG_HAPTICS_SCALE_V2_ENABLED) + public void testScale_withScalingV2_fullPrimitiveScaleValue() { + PrimitiveSegment initial = new PrimitiveSegment( + VibrationEffect.Composition.PRIMITIVE_CLICK, 1, 0); + + assertEquals(1f, initial.scale(1).getScale(), TOLERANCE); + assertEquals(0.5f, initial.scale(0.5f).getScale(), TOLERANCE); + // The original value was not scaled up, so this only scales it down. + assertEquals(1f, initial.scale(1.5f).getScale(), TOLERANCE); + assertEquals(2 / 3f, initial.scale(1.5f).scale(2 / 3f).getScale(), TOLERANCE); + // Does not restore to the exact original value because scale up is a bit offset. + assertEquals(0.8f, initial.scale(0.8f).getScale(), TOLERANCE); + assertEquals(0.86f, initial.scale(0.8f).scale(1.25f).getScale(), TOLERANCE); + } + + @Test + @DisableFlags(Flags.FLAG_HAPTICS_SCALE_V2_ENABLED) + public void testScale_withLegacyScaling_halfPrimitiveScaleValue() { PrimitiveSegment initial = new PrimitiveSegment( VibrationEffect.Composition.PRIMITIVE_CLICK, 0.5f, 0); @@ -117,6 +142,22 @@ public class PrimitiveSegmentTest { } @Test + @EnableFlags(Flags.FLAG_HAPTICS_SCALE_V2_ENABLED) + public void testScale_withScalingV2_halfPrimitiveScaleValue() { + PrimitiveSegment initial = new PrimitiveSegment( + VibrationEffect.Composition.PRIMITIVE_CLICK, 0.5f, 0); + + assertEquals(0.5f, initial.scale(1).getScale(), TOLERANCE); + assertEquals(0.25f, initial.scale(0.5f).getScale(), TOLERANCE); + // The original value was not scaled up, so this only scales it down. + assertEquals(0.66f, initial.scale(1.5f).getScale(), TOLERANCE); + assertEquals(0.44f, initial.scale(1.5f).scale(2 / 3f).getScale(), TOLERANCE); + // Does not restore to the exact original value because scale up is a bit offset. + assertEquals(0.4f, initial.scale(0.8f).getScale(), TOLERANCE); + assertEquals(0.48f, initial.scale(0.8f).scale(1.25f).getScale(), TOLERANCE); + } + + @Test public void testScale_zeroPrimitiveScaleValue() { PrimitiveSegment initial = new PrimitiveSegment( VibrationEffect.Composition.PRIMITIVE_CLICK, 0, 0); diff --git a/core/tests/vibrator/src/android/os/vibrator/RampSegmentTest.java b/core/tests/vibrator/src/android/os/vibrator/RampSegmentTest.java index 01013ab69f85..bea82931dda7 100644 --- a/core/tests/vibrator/src/android/os/vibrator/RampSegmentTest.java +++ b/core/tests/vibrator/src/android/os/vibrator/RampSegmentTest.java @@ -29,7 +29,11 @@ import android.hardware.vibrator.IVibrator; import android.os.Parcel; import android.os.VibrationEffect; import android.os.VibratorInfo; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -38,6 +42,9 @@ import org.junit.runners.JUnit4; public class RampSegmentTest { private static final float TOLERANCE = 1e-2f; + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @Test public void testCreation() { RampSegment ramp = new RampSegment(/* startAmplitude= */ 1, /* endAmplitude= */ 0, @@ -97,14 +104,18 @@ public class RampSegmentTest { } @Test - public void testScale() { - RampSegment initial = new RampSegment(0, 1, 0, 0, 0); + @DisableFlags(Flags.FLAG_HAPTICS_SCALE_V2_ENABLED) + public void testScale_withLegacyScaling_halfAndFullAmplitudes() { + RampSegment initial = new RampSegment(0.5f, 1, 0, 0, 0); - assertEquals(0f, initial.scale(1).getStartAmplitude(), TOLERANCE); - assertEquals(0f, initial.scale(0.5f).getStartAmplitude(), TOLERANCE); - assertEquals(0f, initial.scale(1.5f).getStartAmplitude(), TOLERANCE); - assertEquals(0f, initial.scale(1.5f).scale(2 / 3f).getStartAmplitude(), TOLERANCE); - assertEquals(0f, initial.scale(0.8f).scale(1.25f).getStartAmplitude(), TOLERANCE); + assertEquals(0.5f, initial.scale(1).getStartAmplitude(), TOLERANCE); + assertEquals(0.17f, initial.scale(0.5f).getStartAmplitude(), TOLERANCE); + // The original value was not scaled up, so this only scales it down. + assertEquals(0.86f, initial.scale(1.5f).getStartAmplitude(), TOLERANCE); + assertEquals(0.47f, initial.scale(1.5f).scale(2 / 3f).getStartAmplitude(), TOLERANCE); + // Does not restore to the exact original value because scale up is a bit offset. + assertEquals(0.35f, initial.scale(0.8f).getStartAmplitude(), TOLERANCE); + assertEquals(0.5f, initial.scale(0.8f).scale(1.25f).getStartAmplitude(), TOLERANCE); assertEquals(1f, initial.scale(1).getEndAmplitude(), TOLERANCE); assertEquals(0.34f, initial.scale(0.5f).getEndAmplitude(), TOLERANCE); @@ -117,17 +128,38 @@ public class RampSegmentTest { } @Test - public void testScale_halfPrimitiveScaleValue() { + @EnableFlags(Flags.FLAG_HAPTICS_SCALE_V2_ENABLED) + public void testScale_withScalingV2_halfAndFullAmplitudes() { RampSegment initial = new RampSegment(0.5f, 1, 0, 0, 0); assertEquals(0.5f, initial.scale(1).getStartAmplitude(), TOLERANCE); - assertEquals(0.17f, initial.scale(0.5f).getStartAmplitude(), TOLERANCE); + assertEquals(0.25f, initial.scale(0.5f).getStartAmplitude(), TOLERANCE); + // The original value was not scaled up, so this only scales it down. + assertEquals(0.66f, initial.scale(1.5f).getStartAmplitude(), TOLERANCE); + assertEquals(0.44f, initial.scale(1.5f).scale(2 / 3f).getStartAmplitude(), TOLERANCE); // Does not restore to the exact original value because scale up is a bit offset. - assertEquals(0.86f, initial.scale(1.5f).getStartAmplitude(), TOLERANCE); - assertEquals(0.47f, initial.scale(1.5f).scale(2 / 3f).getStartAmplitude(), TOLERANCE); + assertEquals(0.4f, initial.scale(0.8f).getStartAmplitude(), TOLERANCE); + assertEquals(0.48f, initial.scale(0.8f).scale(1.25f).getStartAmplitude(), TOLERANCE); + + assertEquals(1f, initial.scale(1).getEndAmplitude(), TOLERANCE); + assertEquals(0.5f, initial.scale(0.5f).getEndAmplitude(), TOLERANCE); + // The original value was not scaled up, so this only scales it down. + assertEquals(1f, initial.scale(1.5f).getEndAmplitude(), TOLERANCE); + assertEquals(2 / 3f, initial.scale(1.5f).scale(2 / 3f).getEndAmplitude(), TOLERANCE); // Does not restore to the exact original value because scale up is a bit offset. - assertEquals(0.35f, initial.scale(0.8f).getStartAmplitude(), TOLERANCE); - assertEquals(0.5f, initial.scale(0.8f).scale(1.25f).getStartAmplitude(), TOLERANCE); + assertEquals(0.81f, initial.scale(0.8f).getEndAmplitude(), TOLERANCE); + assertEquals(0.86f, initial.scale(0.8f).scale(1.25f).getEndAmplitude(), TOLERANCE); + } + + @Test + public void testScale_zeroAmplitude() { + RampSegment initial = new RampSegment(0, 1, 0, 0, 0); + + assertEquals(0f, initial.scale(1).getStartAmplitude(), TOLERANCE); + assertEquals(0f, initial.scale(0.5f).getStartAmplitude(), TOLERANCE); + assertEquals(0f, initial.scale(1.5f).getStartAmplitude(), TOLERANCE); + assertEquals(0f, initial.scale(1.5f).scale(2 / 3f).getStartAmplitude(), TOLERANCE); + assertEquals(0f, initial.scale(0.8f).scale(1.25f).getStartAmplitude(), TOLERANCE); } @Test diff --git a/core/tests/vibrator/src/android/os/vibrator/StepSegmentTest.java b/core/tests/vibrator/src/android/os/vibrator/StepSegmentTest.java index 40776abc0291..411074a75e2e 100644 --- a/core/tests/vibrator/src/android/os/vibrator/StepSegmentTest.java +++ b/core/tests/vibrator/src/android/os/vibrator/StepSegmentTest.java @@ -27,7 +27,11 @@ import android.hardware.vibrator.IVibrator; import android.os.Parcel; import android.os.VibrationEffect; import android.os.VibratorInfo; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -35,6 +39,10 @@ import org.junit.runners.JUnit4; @RunWith(JUnit4.class) public class StepSegmentTest { private static final float TOLERANCE = 1e-2f; + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @Test public void testCreation() { StepSegment step = new StepSegment(/* amplitude= */ 1f, /* frequencyHz= */ 1f, @@ -93,7 +101,8 @@ public class StepSegmentTest { } @Test - public void testScale_fullAmplitude() { + @DisableFlags(Flags.FLAG_HAPTICS_SCALE_V2_ENABLED) + public void testScale_withLegacyScaling_fullAmplitude() { StepSegment initial = new StepSegment(1f, 0, 0); assertEquals(1f, initial.scale(1).getAmplitude(), TOLERANCE); @@ -107,7 +116,23 @@ public class StepSegmentTest { } @Test - public void testScale_halfAmplitude() { + @EnableFlags(Flags.FLAG_HAPTICS_SCALE_V2_ENABLED) + public void testScale_withScalingV2_fullAmplitude() { + StepSegment initial = new StepSegment(1f, 0, 0); + + assertEquals(1f, initial.scale(1).getAmplitude(), TOLERANCE); + assertEquals(0.5f, initial.scale(0.5f).getAmplitude(), TOLERANCE); + // The original value was not scaled up, so this only scales it down. + assertEquals(1f, initial.scale(1.5f).getAmplitude(), TOLERANCE); + assertEquals(2 / 3f, initial.scale(1.5f).scale(2 / 3f).getAmplitude(), TOLERANCE); + // Does not restore to the exact original value because scale up is a bit offset. + assertEquals(0.8f, initial.scale(0.8f).getAmplitude(), TOLERANCE); + assertEquals(0.86f, initial.scale(0.8f).scale(1.25f).getAmplitude(), TOLERANCE); + } + + @Test + @DisableFlags(Flags.FLAG_HAPTICS_SCALE_V2_ENABLED) + public void testScale_withLegacyScaling_halfAmplitude() { StepSegment initial = new StepSegment(0.5f, 0, 0); assertEquals(0.5f, initial.scale(1).getAmplitude(), TOLERANCE); @@ -121,6 +146,21 @@ public class StepSegmentTest { } @Test + @EnableFlags(Flags.FLAG_HAPTICS_SCALE_V2_ENABLED) + public void testScale_withScalingV2_halfAmplitude() { + StepSegment initial = new StepSegment(0.5f, 0, 0); + + assertEquals(0.5f, initial.scale(1).getAmplitude(), TOLERANCE); + assertEquals(0.25f, initial.scale(0.5f).getAmplitude(), TOLERANCE); + // The original value was not scaled up, so this only scales it down. + assertEquals(0.66f, initial.scale(1.5f).getAmplitude(), TOLERANCE); + assertEquals(0.44f, initial.scale(1.5f).scale(2 / 3f).getAmplitude(), TOLERANCE); + // Does not restore to the exact original value because scale up is a bit offset. + assertEquals(0.4f, initial.scale(0.8f).getAmplitude(), TOLERANCE); + assertEquals(0.48f, initial.scale(0.8f).scale(1.25f).getAmplitude(), TOLERANCE); + } + + @Test public void testScale_zeroAmplitude() { StepSegment initial = new StepSegment(0, 0, 0); diff --git a/core/tests/vibrator/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java b/core/tests/vibrator/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java index bf9a820aca5c..1cc38ded1c2c 100644 --- a/core/tests/vibrator/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java +++ b/core/tests/vibrator/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java @@ -24,22 +24,32 @@ import static android.os.vibrator.persistence.VibrationXmlParser.isSupportedMime import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertThrows; +import android.os.PersistableBundle; import android.os.VibrationEffect; +import android.os.vibrator.Flags; import android.os.vibrator.PrebakedSegment; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import android.util.Xml; import com.android.modules.utils.TypedXmlPullParser; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.xmlpull.v1.XmlPullParser; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.io.StringReader; import java.io.StringWriter; import java.util.Arrays; +import java.util.Base64; import java.util.HashMap; import java.util.Map; import java.util.Set; @@ -53,6 +63,9 @@ import java.util.Set; @RunWith(JUnit4.class) public class VibrationEffectXmlSerializationTest { + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @Test public void isSupportedMimeType_onlySupportsVibrationXmlMimeType() { // Single MIME type supported @@ -422,6 +435,97 @@ public class VibrationEffectXmlSerializationTest { } } + @Test + @EnableFlags(Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + public void testVendorEffect_featureFlagEnabled_allSucceed() throws Exception { + PersistableBundle vendorData = new PersistableBundle(); + vendorData.putInt("id", 1); + vendorData.putDouble("scale", 0.5); + vendorData.putBoolean("loop", false); + vendorData.putLongArray("amplitudes", new long[] { 0, 255, 128 }); + vendorData.putString("label", "vibration"); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + vendorData.writeToStream(outputStream); + String vendorDataStr = Base64.getEncoder().encodeToString(outputStream.toByteArray()); + + VibrationEffect effect = VibrationEffect.createVendorEffect(vendorData); + String xml = "<vibration-effect><vendor-effect> " // test trailing whitespace is ignored + + vendorDataStr + + " \n </vendor-effect></vibration-effect>"; + + assertPublicApisParserSucceeds(xml, effect); + assertPublicApisSerializerSucceeds(effect, vendorDataStr); + assertPublicApisRoundTrip(effect); + + assertHiddenApisParserSucceeds(xml, effect); + assertHiddenApisSerializerSucceeds(effect, vendorDataStr); + assertHiddenApisRoundTrip(effect); + + // Check PersistableBundle from round-trip + PersistableBundle parsedVendorData = + ((VibrationEffect.VendorEffect) parseVibrationEffect(serialize(effect), + /* flags= */ 0)).getVendorData(); + assertThat(parsedVendorData.size()).isEqualTo(vendorData.size()); + assertThat(parsedVendorData.getInt("id")).isEqualTo(1); + assertThat(parsedVendorData.getDouble("scale")).isEqualTo(0.5); + assertThat(parsedVendorData.getBoolean("loop")).isFalse(); + assertArrayEquals(parsedVendorData.getLongArray("amplitudes"), new long[] { 0, 255, 128 }); + assertThat(parsedVendorData.getString("label")).isEqualTo("vibration"); + } + + @Test + @EnableFlags(Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + public void testInvalidVendorEffect_featureFlagEnabled_allFail() throws IOException { + String emptyTag = "<vibration-effect><vendor-effect/></vibration-effect>"; + assertPublicApisParserFails(emptyTag); + assertHiddenApisParserFails(emptyTag); + + String emptyStringTag = + "<vibration-effect><vendor-effect> \n </vendor-effect></vibration-effect>"; + assertPublicApisParserFails(emptyStringTag); + assertHiddenApisParserFails(emptyStringTag); + + String invalidString = + "<vibration-effect><vendor-effect>invalid</vendor-effect></vibration-effect>"; + assertPublicApisParserFails(invalidString); + assertHiddenApisParserFails(invalidString); + + String validBase64String = + "<vibration-effect><vendor-effect>c29tZXNh</vendor-effect></vibration-effect>"; + assertPublicApisParserFails(validBase64String); + assertHiddenApisParserFails(validBase64String); + + PersistableBundle emptyData = new PersistableBundle(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + emptyData.writeToStream(outputStream); + String emptyBundleString = "<vibration-effect><vendor-effect>" + + Base64.getEncoder().encodeToString(outputStream.toByteArray()) + + "</vendor-effect></vibration-effect>"; + assertPublicApisParserFails(emptyBundleString); + assertHiddenApisParserFails(emptyBundleString); + } + + @Test + @DisableFlags(Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + public void testVendorEffect_featureFlagDisabled_allFail() throws Exception { + PersistableBundle vendorData = new PersistableBundle(); + vendorData.putInt("id", 1); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + vendorData.writeToStream(outputStream); + String vendorDataStr = Base64.getEncoder().encodeToString(outputStream.toByteArray()); + String xml = "<vibration-effect><vendor-effect>" + + vendorDataStr + + "</vendor-effect></vibration-effect>"; + VibrationEffect vendorEffect = VibrationEffect.createVendorEffect(vendorData); + + assertPublicApisParserFails(xml); + assertPublicApisSerializerFails(vendorEffect); + + assertHiddenApisParserFails(xml); + assertHiddenApisSerializerFails(vendorEffect); + } + private void assertPublicApisParserFails(String xml) { assertThrows("Expected parseVibrationEffect to fail for " + xml, VibrationXmlParser.ParseFailedException.class, @@ -493,6 +597,12 @@ public class VibrationEffectXmlSerializationTest { () -> serialize(effect)); } + private void assertHiddenApisSerializerFails(VibrationEffect effect) { + assertThrows("Expected serialization to fail for " + effect, + VibrationXmlSerializer.SerializationFailedException.class, + () -> serialize(effect, VibrationXmlSerializer.FLAG_ALLOW_HIDDEN_APIS)); + } + private void assertPublicApisSerializerSucceeds(VibrationEffect effect, String... expectedSegments) throws Exception { assertSerializationContainsSegments(serialize(effect), expectedSegments); diff --git a/core/xsd/vibrator/vibration/schema/current.txt b/core/xsd/vibrator/vibration/schema/current.txt index f0e13c4e8ca3..280b40516b7e 100644 --- a/core/xsd/vibrator/vibration/schema/current.txt +++ b/core/xsd/vibrator/vibration/schema/current.txt @@ -41,9 +41,11 @@ package com.android.internal.vibrator.persistence { ctor public VibrationEffect(); method public com.android.internal.vibrator.persistence.PredefinedEffect getPredefinedEffect_optional(); method public com.android.internal.vibrator.persistence.PrimitiveEffect getPrimitiveEffect_optional(); + method public byte[] getVendorEffect_optional(); method public com.android.internal.vibrator.persistence.WaveformEffect getWaveformEffect_optional(); method public void setPredefinedEffect_optional(com.android.internal.vibrator.persistence.PredefinedEffect); method public void setPrimitiveEffect_optional(com.android.internal.vibrator.persistence.PrimitiveEffect); + method public void setVendorEffect_optional(byte[]); method public void setWaveformEffect_optional(com.android.internal.vibrator.persistence.WaveformEffect); } diff --git a/core/xsd/vibrator/vibration/vibration-plus-hidden-apis.xsd b/core/xsd/vibrator/vibration/vibration-plus-hidden-apis.xsd index fcd250b4fc06..21a6facad242 100644 --- a/core/xsd/vibrator/vibration/vibration-plus-hidden-apis.xsd +++ b/core/xsd/vibrator/vibration/vibration-plus-hidden-apis.xsd @@ -46,6 +46,9 @@ <!-- Predefined vibration effect --> <xs:element name="predefined-effect" type="PredefinedEffect"/> + <!-- Vendor vibration effect --> + <xs:element name="vendor-effect" type="VendorEffect"/> + <!-- Primitive composition effect --> <xs:sequence> <xs:element name="primitive-effect" type="PrimitiveEffect"/> @@ -136,6 +139,10 @@ </xs:restriction> </xs:simpleType> + <xs:simpleType name="VendorEffect"> + <xs:restriction base="xs:base64Binary"/> + </xs:simpleType> + <xs:complexType name="PrimitiveEffect"> <xs:attribute name="name" type="PrimitiveEffectName" use="required"/> <xs:attribute name="scale" type="PrimitiveScale"/> diff --git a/core/xsd/vibrator/vibration/vibration.xsd b/core/xsd/vibrator/vibration/vibration.xsd index b9de6914b9dc..d35d777d4450 100644 --- a/core/xsd/vibrator/vibration/vibration.xsd +++ b/core/xsd/vibrator/vibration/vibration.xsd @@ -44,6 +44,9 @@ <!-- Predefined vibration effect --> <xs:element name="predefined-effect" type="PredefinedEffect"/> + <!-- Vendor vibration effect --> + <xs:element name="vendor-effect" type="VendorEffect"/> + <!-- Primitive composition effect --> <xs:sequence> <xs:element name="primitive-effect" type="PrimitiveEffect"/> @@ -113,6 +116,10 @@ </xs:restriction> </xs:simpleType> + <xs:simpleType name="VendorEffect"> + <xs:restriction base="xs:base64Binary"/> + </xs:simpleType> + <xs:complexType name="PrimitiveEffect"> <xs:attribute name="name" type="PrimitiveEffectName" use="required"/> <xs:attribute name="scale" type="PrimitiveScale"/> diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java index 7be14724643c..26d180cdcb1a 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -695,12 +695,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen break; case TYPE_ACTIVITY_REPARENTED_TO_TASK: final IBinder candidateAssociatedActToken, lastOverlayToken; - if (Flags.fixPipRestoreToOverlay()) { - candidateAssociatedActToken = change.getOtherActivityToken(); - lastOverlayToken = change.getTaskFragmentToken(); - } else { - candidateAssociatedActToken = lastOverlayToken = null; - } + candidateAssociatedActToken = change.getOtherActivityToken(); + lastOverlayToken = change.getTaskFragmentToken(); onActivityReparentedToTask( wct, taskId, @@ -1023,10 +1019,6 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @Nullable OverlayContainerRestoreParams getOverlayContainerRestoreParams( @Nullable IBinder associatedActivityToken, @Nullable IBinder overlayToken) { - if (!Flags.fixPipRestoreToOverlay()) { - return null; - } - if (associatedActivityToken == null || overlayToken == null) { return null; } @@ -2725,15 +2717,19 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen TaskFragmentContainer createOrUpdateOverlayTaskFragmentIfNeeded( @NonNull WindowContainerTransaction wct, @NonNull Bundle options, @NonNull Intent intent, @NonNull Activity launchActivity) { + final String overlayTag = Objects.requireNonNull(options.getString(KEY_OVERLAY_TAG)); if (isActivityFromSplit(launchActivity)) { // We restrict to launch the overlay from split. Fallback to treat it as normal // launch. + Log.w(TAG, "It's not allowed to launch overlay container with tag=" + overlayTag + + " from activity in Activity Embedding split." + + " Launching activity=" + launchActivity + + " Fallback to launch the activity as normal launch."); return null; } final List<TaskFragmentContainer> overlayContainers = getAllNonFinishingOverlayContainers(); - final String overlayTag = Objects.requireNonNull(options.getString(KEY_OVERLAY_TAG)); final boolean associateLaunchingActivity = options .getBoolean(KEY_OVERLAY_ASSOCIATE_WITH_LAUNCHING_ACTIVITY, true); 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/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java index d0e2c998e961..ee3e6f368505 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java @@ -36,7 +36,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; -import com.android.window.flags.Flags; import java.util.ArrayList; import java.util.Collections; @@ -257,7 +256,7 @@ class TaskFragmentContainer { mPendingAppearedIntent = pendingAppearedIntent; // Save the information necessary for restoring the overlay when needed. - if (Flags.fixPipRestoreToOverlay() && overlayTag != null && pendingAppearedIntent != null + if (overlayTag != null && pendingAppearedIntent != null && associatedActivity != null && !associatedActivity.isFinishing()) { final IBinder associatedActivityToken = associatedActivity.getActivityToken(); final OverlayContainerRestoreParams params = new OverlayContainerRestoreParams(mToken, diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java index 475475b05272..90eeb583d070 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java @@ -874,8 +874,6 @@ public class OverlayPresentationTest { @Test public void testOnActivityReparentedToTask_overlayRestoration() { - mSetFlagRule.enableFlags(Flags.FLAG_FIX_PIP_RESTORE_TO_OVERLAY); - // Prepares and mock the data necessary for the test. final IBinder activityToken = mActivity.getActivityToken(); final Intent intent = new Intent(); diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp index 5135e9ee14bc..1c3d9c30c91c 100644 --- a/libs/WindowManager/Shell/Android.bp +++ b/libs/WindowManager/Shell/Android.bp @@ -166,6 +166,16 @@ java_library { }, } +java_library { + name: "WindowManager-Shell-lite-proto", + + srcs: ["src/com/android/wm/shell/desktopmode/education/data/proto/**/*.proto"], + + proto: { + type: "lite", + }, +} + filegroup { name: "wm_shell-shared-aidls", @@ -215,6 +225,7 @@ android_library { "androidx.core_core-animation", "androidx.core_core-ktx", "androidx.arch.core_core-runtime", + "androidx.datastore_datastore", "androidx.compose.material3_material3", "androidx-constraintlayout_constraintlayout", "androidx.dynamicanimation_dynamicanimation", @@ -225,6 +236,7 @@ android_library { "//frameworks/libs/systemui:iconloader_base", "com_android_wm_shell_flags_lib", "WindowManager-Shell-proto", + "WindowManager-Shell-lite-proto", "WindowManager-Shell-shared", "perfetto_trace_java_protos", "dagger2", diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt index 5e673338bad3..84f7bb27ca82 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt @@ -53,6 +53,7 @@ import org.junit.runner.RunWith import org.mockito.kotlin.mock import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags +import com.android.wm.shell.common.bubbles.BubbleBarLocation import java.util.concurrent.Semaphore import java.util.concurrent.TimeUnit import java.util.function.Consumer @@ -458,5 +459,7 @@ class BubbleStackViewTest { override fun isShowingAsBubbleBar(): Boolean = false override fun hideCurrentInputMethod() {} + + override fun updateBubbleBarLocation(location: BubbleBarLocation) {} } } diff --git a/libs/WindowManager/Shell/res/layout/bubble_bar_menu_item.xml b/libs/WindowManager/Shell/res/layout/bubble_bar_menu_item.xml index ddcd5c60d9c8..e3217811ca29 100644 --- a/libs/WindowManager/Shell/res/layout/bubble_bar_menu_item.xml +++ b/libs/WindowManager/Shell/res/layout/bubble_bar_menu_item.xml @@ -16,6 +16,7 @@ --> <com.android.wm.shell.bubbles.bar.BubbleBarMenuItemView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="@dimen/bubble_bar_manage_menu_item_height" @@ -35,7 +36,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="16dp" - android:textColor="?android:attr/textColorPrimary" + android:textColor="?androidprv:attr/materialColorOnSurface" android:textAppearance="@*android:style/TextAppearance.DeviceDefault" /> </com.android.wm.shell.bubbles.bar.BubbleBarMenuItemView>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/layout/bubble_bar_menu_view.xml b/libs/WindowManager/Shell/res/layout/bubble_bar_menu_view.xml index 1cbd0e614e42..f1ecde49ce78 100644 --- a/libs/WindowManager/Shell/res/layout/bubble_bar_menu_view.xml +++ b/libs/WindowManager/Shell/res/layout/bubble_bar_menu_view.xml @@ -17,6 +17,7 @@ <com.android.wm.shell.bubbles.bar.BubbleBarMenuView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" @@ -51,7 +52,7 @@ android:layout_height="wrap_content" android:layout_marginStart="8dp" android:layout_weight="1" - android:textColor="?android:attr/textColorPrimary" + android:textColor="?androidprv:attr/materialColorOnSurface" android:textAppearance="@*android:style/TextAppearance.DeviceDefault" /> <ImageView @@ -61,7 +62,7 @@ android:layout_marginStart="8dp" android:contentDescription="@null" android:src="@drawable/ic_expand_less" - app:tint="?android:attr/textColorPrimary" /> + app:tint="?androidprv:attr/materialColorOnSurface" /> </LinearLayout> diff --git a/libs/WindowManager/Shell/res/values/ids.xml b/libs/WindowManager/Shell/res/values/ids.xml index bc59a235517d..debcba071d9c 100644 --- a/libs/WindowManager/Shell/res/values/ids.xml +++ b/libs/WindowManager/Shell/res/values/ids.xml @@ -42,6 +42,8 @@ <item type="id" name="action_move_top_right"/> <item type="id" name="action_move_bottom_left"/> <item type="id" name="action_move_bottom_right"/> + <item type="id" name="action_move_bubble_bar_left"/> + <item type="id" name="action_move_bubble_bar_right"/> <item type="id" name="dismiss_view"/> </resources> diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml index 0a8166fb1a8d..6a62d7a373c8 100644 --- a/libs/WindowManager/Shell/res/values/strings.xml +++ b/libs/WindowManager/Shell/res/values/strings.xml @@ -139,6 +139,14 @@ <string name="bubble_accessibility_action_move_bottom_left">Move bottom left</string> <!-- Action in accessibility menu to move the stack of bubbles to the bottom right of the screen. [CHAR LIMIT=30]--> <string name="bubble_accessibility_action_move_bottom_right">Move bottom right</string> + <!-- Click action label for bubbles to expand menu. [CHAR LIMIT=30]--> + <string name="bubble_accessibility_action_expand_menu">expand menu</string> + <!-- Click action label for bubbles to collapse menu. [CHAR LIMIT=30]--> + <string name="bubble_accessibility_action_collapse_menu">collapse menu</string> + <!-- Action in accessibility menu to move the bubble bar to the left side of the screen. [CHAR_LIMIT=30] --> + <string name="bubble_accessibility_action_move_bar_left">Move left</string> + <!-- Action in accessibility menu to move the bubble bar to the right side of the screen. [CHAR_LIMIT=30] --> + <string name="bubble_accessibility_action_move_bar_right">Move right</string> <!-- Accessibility announcement when the stack of bubbles expands. [CHAR LIMIT=NONE]--> <string name="bubble_accessibility_announce_expand">expand <xliff:g id="bubble_title" example="Messages">%1$s</xliff:g></string> <!-- Accessibility announcement when the stack of bubbles collapses. [CHAR LIMIT=NONE]--> 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/activityembedding/ActivityEmbeddingAnimationAdapter.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java index 8d30db64a3e5..53551387230c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java @@ -18,6 +18,7 @@ package com.android.wm.shell.activityembedding; import static android.graphics.Matrix.MTRANS_X; import static android.graphics.Matrix.MTRANS_Y; +import static android.window.TransitionInfo.FLAG_TRANSLUCENT; import android.annotation.CallSuper; import android.graphics.Point; @@ -146,6 +147,14 @@ class ActivityEmbeddingAnimationAdapter { /** To be overridden by subclasses to adjust the animation surface change. */ void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) { // Update the surface position and alpha. + if (com.android.graphics.libgui.flags.Flags.edgeExtensionShader() + && mAnimation.getExtensionEdges() != 0x0 + && !(mChange.hasFlags(FLAG_TRANSLUCENT) + && mChange.getActivityComponent() != null)) { + // Extend non-translucent activities + t.setEdgeExtensionEffect(mLeash, mAnimation.getExtensionEdges()); + } + mTransformation.getMatrix().postTranslate(mContentRelOffset.x, mContentRelOffset.y); t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix); t.setAlpha(mLeash, mTransformation.getAlpha()); @@ -165,7 +174,7 @@ class ActivityEmbeddingAnimationAdapter { if (!cropRect.intersect(mWholeAnimationBounds)) { // Hide the surface when it is outside of the animation area. t.setAlpha(mLeash, 0); - } else if (mAnimation.hasExtension()) { + } else if (mAnimation.getExtensionEdges() != 0) { // Allow the surface to be shown in its original bounds in case we want to use edge // extensions. cropRect.union(mContentBounds); @@ -180,6 +189,10 @@ class ActivityEmbeddingAnimationAdapter { @CallSuper void onAnimationEnd(@NonNull SurfaceControl.Transaction t) { onAnimationUpdate(t, mAnimation.getDuration()); + if (com.android.graphics.libgui.flags.Flags.edgeExtensionShader() + && mAnimation.getExtensionEdges() != 0x0) { + t.setEdgeExtensionEffect(mLeash, /* edge */ 0); + } } final long getDurationHint() { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java index 5696a544152c..d2cef4baf798 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java @@ -144,8 +144,10 @@ class ActivityEmbeddingAnimationRunner { // ending states. prepareForJumpCut(info, startTransaction); } else { - addEdgeExtensionIfNeeded(startTransaction, finishTransaction, - postStartTransactionCallbacks, adapters); + if (!com.android.graphics.libgui.flags.Flags.edgeExtensionShader()) { + addEdgeExtensionIfNeeded(startTransaction, finishTransaction, + postStartTransactionCallbacks, adapters); + } addBackgroundColorIfNeeded(info, startTransaction, finishTransaction, adapters); for (ActivityEmbeddingAnimationAdapter adapter : adapters) { duration = Math.max(duration, adapter.getDurationHint()); @@ -341,7 +343,7 @@ class ActivityEmbeddingAnimationRunner { @NonNull List<ActivityEmbeddingAnimationAdapter> adapters) { for (ActivityEmbeddingAnimationAdapter adapter : adapters) { final Animation animation = adapter.mAnimation; - if (!animation.hasExtension()) { + if (animation.getExtensionEdges() == 0) { continue; } if (adapter.mChange.hasFlags(FLAG_TRANSLUCENT) 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..12422874ca5d 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; @@ -88,6 +95,7 @@ import com.android.wm.shell.transition.Transitions; import java.io.PrintWriter; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Predicate; /** * Controls the window animation run when a user initiates a back gesture. @@ -837,8 +845,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 +865,10 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont // start post animation dispatchOnBackInvoked(mActiveCallback); } else { + if (migrateBackToTransition + && mBackTransitionHandler.mPrepareOpenTransition != null) { + mBackTransitionHandler.createClosePrepareTransition(); + } tryDispatchOnBackCancelled(mActiveCallback); } } @@ -960,6 +973,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 +1142,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 +1178,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 +1199,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)) { + && isNotGestureBackTransition(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 +1243,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 (isNotGestureBackTransition(info) || shouldCancelAnimation(info) + || !mCloseTransitionRequested) { + if (mPrepareOpenTransition != null) { applyFinishOpenTransition(); } if (mQueuedTransition != null) { @@ -1222,7 +1383,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 +1394,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 = !mCloseTransitionRequested + && 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. @@ -1245,11 +1456,17 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont if (info.getType() != WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION) { return false; } - + // Must have open target, must not have close target. + if (hasAnimationInMode(info, TransitionUtil::isClosingMode) + || !hasAnimationInMode(info, TransitionUtil::isOpeningMode)) { + return false; + } 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,27 +1476,17 @@ 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; } - private boolean isGestureBackTransition(@NonNull TransitionInfo info) { - for (int i = info.getChanges().size() - 1; i >= 0; --i) { - final TransitionInfo.Change c = info.getChanges().get(i); - if (c.hasFlags(FLAG_BACK_GESTURE_ANIMATED) - && (TransitionUtil.isOpeningMode(c.getMode()) - || TransitionUtil.isClosingMode(c.getMode()))) { - return true; - } - } - return false; - } /** * Check whether this transition is triggered from back gesture commitment. * Reparent the transition targets to animation leashes, so the animation won't be broken. @@ -1288,6 +1495,18 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont @NonNull SurfaceControl.Transaction st, @NonNull SurfaceControl.Transaction ft, @NonNull Transitions.TransitionFinishCallback finishCallback) { + if (!mCloseTransitionRequested) { + return false; + } + // must have close target + if (!hasAnimationInMode(info, TransitionUtil::isClosingMode)) { + return false; + } + if (mApps == null) { + // animation is done + applyAndFinish(st, ft, finishCallback); + return true; + } SurfaceControl openingLeash = null; SurfaceControl closingLeash = null; for (int i = mApps.length - 1; i >= 0; --i) { @@ -1305,6 +1524,7 @@ 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); } else if (TransitionUtil.isClosingMode(c.getMode())) { st.reparent(c.getLeash(), closingLeash); } @@ -1325,7 +1545,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 +1594,51 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } } } + + private static boolean isNotGestureBackTransition(@NonNull TransitionInfo info) { + return !hasAnimationInMode(info, TransitionUtil::isOpenOrCloseMode); + } + + private static boolean hasAnimationInMode(@NonNull TransitionInfo info, + Predicate<Integer> mode) { + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change c = info.getChanges().get(i); + if (c.hasFlags(FLAG_BACK_GESTURE_ANIMATED) && mode.test(c.getMode())) { + return true; + } + } + return false; + } + + 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/bubbles/BadgedImageView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java index f9a1d940c734..dc511be59764 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java @@ -357,7 +357,9 @@ public class BadgedImageView extends ConstraintLayout { void showBadge() { Bitmap appBadgeBitmap = mBubble.getAppBadge(); - if (appBadgeBitmap == null) { + final boolean isAppLaunchIntent = (mBubble instanceof Bubble) + && ((Bubble) mBubble).isAppLaunchIntent(); + if (appBadgeBitmap == null || isAppLaunchIntent) { mAppIcon.setVisibility(GONE); return; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java index 7dbbb04e4406..5cd2cb7d51d5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java @@ -50,6 +50,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.InstanceId; import com.android.internal.protolog.ProtoLog; import com.android.launcher3.icons.BubbleIconFactory; +import com.android.wm.shell.Flags; import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView; import com.android.wm.shell.bubbles.bar.BubbleBarLayerView; import com.android.wm.shell.common.bubbles.BubbleInfo; @@ -246,7 +247,23 @@ public class Bubble implements BubbleViewProvider { mAppIntent = intent; mDesiredHeight = Integer.MAX_VALUE; mPackageName = intent.getPackage(); + } + private Bubble(ShortcutInfo info, Executor mainExecutor) { + mGroupKey = null; + mLocusId = null; + mFlags = 0; + mUser = info.getUserHandle(); + mIcon = info.getIcon(); + mIsAppBubble = false; + mKey = getBubbleKeyForShortcut(info); + mShowBubbleUpdateDot = false; + mMainExecutor = mainExecutor; + mTaskId = INVALID_TASK_ID; + mAppIntent = null; + mDesiredHeight = Integer.MAX_VALUE; + mPackageName = info.getPackage(); + mShortcutInfo = info; } /** Creates an app bubble. */ @@ -263,6 +280,13 @@ public class Bubble implements BubbleViewProvider { mainExecutor); } + /** Creates a shortcut bubble. */ + public static Bubble createShortcutBubble( + ShortcutInfo info, + Executor mainExecutor) { + return new Bubble(info, mainExecutor); + } + /** * Returns the key for an app bubble from an app with package name, {@code packageName} on an * Android user, {@code user}. @@ -273,6 +297,14 @@ public class Bubble implements BubbleViewProvider { return KEY_APP_BUBBLE + ":" + user.getIdentifier() + ":" + packageName; } + /** + * Returns the key for a shortcut bubble using {@code packageName}, {@code user}, and the + * {@code shortcutInfo} id. + */ + public static String getBubbleKeyForShortcut(ShortcutInfo info) { + return info.getPackage() + ":" + info.getUserId() + ":" + info.getId(); + } + @VisibleForTesting(visibility = PRIVATE) public Bubble(@NonNull final BubbleEntry entry, final Bubbles.BubbleMetadataFlagListener listener, @@ -888,6 +920,17 @@ public class Bubble implements BubbleViewProvider { return mIntent; } + /** + * Whether this bubble represents the full app, i.e. the intent used is the launch + * intent for an app. In this case we don't show a badge on the icon. + */ + public boolean isAppLaunchIntent() { + if (Flags.enableBubbleAnything() && mAppIntent != null) { + return mAppIntent.hasCategory("android.intent.category.LAUNCHER"); + } + return false; + } + @Nullable PendingIntent getDeleteIntent() { return mDeleteIntent; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index 949a7236434a..29520efd70b0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -1335,6 +1335,40 @@ public class BubbleController implements ConfigurationChangeListener, } /** + * Expands and selects a bubble created or found via the provided shortcut info. + * + * @param info the shortcut info for the bubble. + */ + public void expandStackAndSelectBubble(ShortcutInfo info) { + if (!Flags.enableBubbleAnything()) return; + Bubble b = mBubbleData.getOrCreateBubble(info); // Removes from overflow + ProtoLog.v(WM_SHELL_BUBBLES, "expandStackAndSelectBubble - shortcut=%s", info); + if (b.isInflated()) { + mBubbleData.setSelectedBubbleAndExpandStack(b); + } else { + b.enable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE); + inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false); + } + } + + /** + * Expands and selects a bubble created or found for this app. + * + * @param intent the intent for the bubble. + */ + public void expandStackAndSelectBubble(Intent intent) { + if (!Flags.enableBubbleAnything()) return; + Bubble b = mBubbleData.getOrCreateBubble(intent); // Removes from overflow + ProtoLog.v(WM_SHELL_BUBBLES, "expandStackAndSelectBubble - intent=%s", intent); + if (b.isInflated()) { + mBubbleData.setSelectedBubbleAndExpandStack(b); + } else { + b.enable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE); + inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false); + } + } + + /** * Expands and selects a bubble based on the provided {@link BubbleEntry}. If no bubble * exists for this entry, and it is able to bubble, a new bubble will be created. * @@ -2323,6 +2357,7 @@ public class BubbleController implements ConfigurationChangeListener, * @param entry the entry to bubble. */ static boolean canLaunchInTaskView(Context context, BubbleEntry entry) { + if (Flags.enableBubbleAnything()) return true; PendingIntent intent = entry.getBubbleMetadata() != null ? entry.getBubbleMetadata().getIntent() : null; @@ -2439,6 +2474,16 @@ public class BubbleController implements ConfigurationChangeListener, } @Override + public void showShortcutBubble(ShortcutInfo info) { + mMainExecutor.execute(() -> mController.expandStackAndSelectBubble(info)); + } + + @Override + public void showAppBubble(Intent intent) { + mMainExecutor.execute(() -> mController.expandStackAndSelectBubble(intent)); + } + + @Override public void showBubble(String key, int topOnScreen) { mMainExecutor.execute( () -> mController.expandStackAndSelectBubbleFromLauncher(key, topOnScreen)); @@ -2634,6 +2679,13 @@ public class BubbleController implements ConfigurationChangeListener, } @Override + public void expandStackAndSelectBubble(ShortcutInfo info) { + mMainExecutor.execute(() -> { + BubbleController.this.expandStackAndSelectBubble(info); + }); + } + + @Override public void expandStackAndSelectBubble(Bubble bubble) { mMainExecutor.execute(() -> { BubbleController.this.expandStackAndSelectBubble(bubble); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java index b6da761b0f9c..3c6c6fa0d8d5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java @@ -23,8 +23,10 @@ import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES; import android.annotation.NonNull; import android.app.PendingIntent; import android.content.Context; +import android.content.Intent; import android.content.LocusId; import android.content.pm.ShortcutInfo; +import android.os.UserHandle; import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; @@ -421,23 +423,19 @@ public class BubbleData { Bubble bubbleToReturn = getBubbleInStackWithKey(key); if (bubbleToReturn == null) { - bubbleToReturn = getOverflowBubbleWithKey(key); - if (bubbleToReturn != null) { - // Promoting from overflow - mOverflowBubbles.remove(bubbleToReturn); - if (mOverflowBubbles.isEmpty()) { - mStateChange.showOverflowChanged = true; + // Check if it's in the overflow + bubbleToReturn = findAndRemoveBubbleFromOverflow(key); + if (bubbleToReturn == null) { + if (entry != null) { + // Not in the overflow, have an entry, so it's a new bubble + bubbleToReturn = new Bubble(entry, + mBubbleMetadataFlagListener, + mCancelledListener, + mMainExecutor); + } else { + // If there's no entry it must be a persisted bubble + bubbleToReturn = persistedBubble; } - } else if (mPendingBubbles.containsKey(key)) { - // Update while it was pending - bubbleToReturn = mPendingBubbles.get(key); - } else if (entry != null) { - // New bubble - bubbleToReturn = new Bubble(entry, mBubbleMetadataFlagListener, mCancelledListener, - mMainExecutor); - } else { - // Persisted bubble being promoted - bubbleToReturn = persistedBubble; } } @@ -448,6 +446,46 @@ public class BubbleData { return bubbleToReturn; } + Bubble getOrCreateBubble(ShortcutInfo info) { + String bubbleKey = Bubble.getBubbleKeyForShortcut(info); + Bubble bubbleToReturn = findAndRemoveBubbleFromOverflow(bubbleKey); + if (bubbleToReturn == null) { + bubbleToReturn = Bubble.createShortcutBubble(info, mMainExecutor); + } + return bubbleToReturn; + } + + Bubble getOrCreateBubble(Intent intent) { + UserHandle user = UserHandle.of(mCurrentUserId); + String bubbleKey = Bubble.getAppBubbleKeyForApp(intent.getPackage(), + user); + Bubble bubbleToReturn = findAndRemoveBubbleFromOverflow(bubbleKey); + if (bubbleToReturn == null) { + bubbleToReturn = Bubble.createAppBubble(intent, user, null, mMainExecutor); + } + return bubbleToReturn; + } + + @Nullable + private Bubble findAndRemoveBubbleFromOverflow(String key) { + Bubble bubbleToReturn = getBubbleInStackWithKey(key); + if (bubbleToReturn != null) { + return bubbleToReturn; + } + bubbleToReturn = getOverflowBubbleWithKey(key); + if (bubbleToReturn != null) { + mOverflowBubbles.remove(bubbleToReturn); + // Promoting from overflow + mOverflowBubbles.remove(bubbleToReturn); + if (mOverflowBubbles.isEmpty()) { + mStateChange.showOverflowChanged = true; + } + } else if (mPendingBubbles.containsKey(key)) { + bubbleToReturn = mPendingBubbles.get(key); + } + return bubbleToReturn; + } + /** * When this method is called it is expected that all info in the bubble has completed loading. * @see Bubble#inflate(BubbleViewInfoTask.Callback, Context, BubbleExpandedViewManager, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java index fdb45239fa63..a0c0a25d97a2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java @@ -232,6 +232,9 @@ public class BubbleExpandedView extends LinearLayout { fillInIntent.addFlags(FLAG_ACTIVITY_NEW_DOCUMENT); fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); + final boolean isShortcutBubble = (mBubble.hasMetadataShortcutId() + || (mBubble.getShortcutInfo() != null && Flags.enableBubbleAnything())); + if (mBubble.isAppBubble()) { Context context = mContext.createContextAsUser( @@ -246,7 +249,8 @@ public class BubbleExpandedView extends LinearLayout { /* options= */ null); mTaskView.startActivity(pi, /* fillInIntent= */ null, options, launchBounds); - } else if (!mIsOverflow && mBubble.hasMetadataShortcutId()) { + } else if (!mIsOverflow && isShortcutBubble) { + ProtoLog.v(WM_SHELL_BUBBLES, "startingShortcutBubble=%s", getBubbleKey()); options.setApplyActivityFlagsForBubbles(true); mTaskView.startShortcutActivity(mBubble.getShortcutInfo(), options, launchBounds); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt index 3d9bf032c1b0..4e80e903b522 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt @@ -16,6 +16,8 @@ package com.android.wm.shell.bubbles +import com.android.wm.shell.common.bubbles.BubbleBarLocation + /** Manager interface for bubble expanded views. */ interface BubbleExpandedViewManager { @@ -30,6 +32,7 @@ interface BubbleExpandedViewManager { fun isStackExpanded(): Boolean fun isShowingAsBubbleBar(): Boolean fun hideCurrentInputMethod() + fun updateBubbleBarLocation(location: BubbleBarLocation) companion object { /** @@ -78,6 +81,10 @@ interface BubbleExpandedViewManager { override fun hideCurrentInputMethod() { controller.hideCurrentInputMethod() } + + override fun updateBubbleBarLocation(location: BubbleBarLocation) { + controller.bubbleBarLocation = location + } } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java index efa1031bf814..f002d8904626 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java @@ -1601,6 +1601,11 @@ public class BubbleStackView extends FrameLayout getResources().getColor(android.R.color.system_neutral1_1000))); mManageMenuScrim.setBackgroundDrawable(new ColorDrawable( getResources().getColor(android.R.color.system_neutral1_1000))); + if (mShowingManage) { + // the manage menu location depends on the manage button location which may need a + // layout pass, so post this to the looper + post(() -> showManageMenu(true)); + } } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java index 5e2141aa639e..5f8f0fd0c54c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java @@ -36,6 +36,7 @@ import android.view.ViewGroup; import androidx.annotation.Nullable; import com.android.internal.protolog.ProtoLog; +import com.android.wm.shell.Flags; import com.android.wm.shell.taskview.TaskView; /** @@ -110,6 +111,8 @@ public class BubbleTaskViewHelper { fillInIntent.addFlags(FLAG_ACTIVITY_NEW_DOCUMENT); fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); + final boolean isShortcutBubble = (mBubble.hasMetadataShortcutId() + || (mBubble.getShortcutInfo() != null && Flags.enableBubbleAnything())); if (mBubble.isAppBubble()) { Context context = mContext.createContextAsUser( @@ -124,7 +127,7 @@ public class BubbleTaskViewHelper { /* options= */ null); mTaskView.startActivity(pi, /* fillInIntent= */ null, options, launchBounds); - } else if (mBubble.hasMetadataShortcutId()) { + } else if (isShortcutBubble) { options.setApplyActivityFlagsForBubbles(true); mTaskView.startShortcutActivity(mBubble.getShortcutInfo(), options, launchBounds); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java index 589dfd24624e..9a27fb65ac2c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java @@ -23,6 +23,7 @@ import static java.lang.annotation.RetentionPolicy.SOURCE; import android.app.NotificationChannel; import android.content.Intent; +import android.content.pm.ShortcutInfo; import android.content.pm.UserInfo; import android.graphics.drawable.Icon; import android.hardware.HardwareBuffer; @@ -118,6 +119,14 @@ public interface Bubbles { /** * Request the stack expand if needed, then select the specified Bubble as current. + * If no bubble exists for this entry, one is created. + * + * @param info the shortcut info to use to create the bubble. + */ + void expandStackAndSelectBubble(ShortcutInfo info); + + /** + * Request the stack expand if needed, then select the specified Bubble as current. * * @param bubble the bubble to be selected */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl index 0907ddd1de83..5c789749412c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl @@ -18,6 +18,7 @@ package com.android.wm.shell.bubbles; import android.content.Intent; import android.graphics.Rect; +import android.content.pm.ShortcutInfo; import com.android.wm.shell.bubbles.IBubblesListener; import com.android.wm.shell.common.bubbles.BubbleBarLocation; @@ -48,4 +49,8 @@ interface IBubbles { oneway void updateBubbleBarTopOnScreen(in int topOnScreen) = 10; oneway void stopBubbleDrag(in BubbleBarLocation location, in int topOnScreen) = 11; + + oneway void showShortcutBubble(in ShortcutInfo info) = 12; + + oneway void showAppBubble(in Intent intent) = 13; }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java index 24c568c23bf2..6d868d215482 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java @@ -26,14 +26,18 @@ import android.graphics.Color; import android.graphics.Insets; import android.graphics.Outline; import android.graphics.Rect; +import android.os.Bundle; import android.util.AttributeSet; import android.util.FloatProperty; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewOutlineProvider; +import android.view.accessibility.AccessibilityNodeInfo; import android.widget.FrameLayout; +import androidx.annotation.NonNull; + import com.android.wm.shell.R; import com.android.wm.shell.bubbles.Bubble; import com.android.wm.shell.bubbles.BubbleExpandedViewManager; @@ -42,6 +46,7 @@ import com.android.wm.shell.bubbles.BubblePositioner; import com.android.wm.shell.bubbles.BubbleTaskView; import com.android.wm.shell.bubbles.BubbleTaskViewHelper; import com.android.wm.shell.bubbles.Bubbles; +import com.android.wm.shell.common.bubbles.BubbleBarLocation; import com.android.wm.shell.taskview.TaskView; import java.util.function.Supplier; @@ -81,6 +86,7 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView private static final String TAG = BubbleBarExpandedView.class.getSimpleName(); private static final int INVALID_TASK_ID = -1; + private Bubble mBubble; private BubbleExpandedViewManager mManager; private BubblePositioner mPositioner; private boolean mIsOverflow; @@ -188,12 +194,21 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView // Handle view needs to draw on top of task view. bringChildToFront(mHandleView); + + mHandleView.setAccessibilityDelegate(new HandleViewAccessibilityDelegate()); } mMenuViewController = new BubbleBarMenuViewController(mContext, this); mMenuViewController.setListener(new BubbleBarMenuViewController.Listener() { @Override public void onMenuVisibilityChanged(boolean visible) { setObscured(visible); + if (visible) { + mHandleView.setFocusable(false); + mHandleView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); + } else { + mHandleView.setFocusable(true); + mHandleView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_AUTO); + } } @Override @@ -319,6 +334,7 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView /** Updates the bubble shown in the expanded view. */ public void update(Bubble bubble) { + mBubble = bubble; mBubbleTaskViewHelper.update(bubble); mMenuViewController.updateMenu(bubble); } @@ -457,4 +473,51 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView invalidateOutline(); } } + + private class HandleViewAccessibilityDelegate extends AccessibilityDelegate { + @Override + public void onInitializeAccessibilityNodeInfo(@NonNull View host, + @NonNull AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(host, info); + info.addAction(new AccessibilityNodeInfo.AccessibilityAction( + AccessibilityNodeInfo.ACTION_CLICK, getResources().getString( + R.string.bubble_accessibility_action_expand_menu))); + info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_COLLAPSE); + info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_DISMISS); + if (mPositioner.isBubbleBarOnLeft()) { + info.addAction(new AccessibilityNodeInfo.AccessibilityAction( + R.id.action_move_bubble_bar_right, getResources().getString( + R.string.bubble_accessibility_action_move_bar_right))); + } else { + info.addAction(new AccessibilityNodeInfo.AccessibilityAction( + R.id.action_move_bubble_bar_left, getResources().getString( + R.string.bubble_accessibility_action_move_bar_left))); + } + } + + @Override + public boolean performAccessibilityAction(@NonNull View host, int action, + @Nullable Bundle args) { + if (super.performAccessibilityAction(host, action, args)) { + return true; + } + if (action == AccessibilityNodeInfo.ACTION_COLLAPSE) { + mManager.collapseStack(); + return true; + } + if (action == AccessibilityNodeInfo.ACTION_DISMISS) { + mManager.dismissBubble(mBubble, Bubbles.DISMISS_USER_GESTURE); + return true; + } + if (action == R.id.action_move_bubble_bar_left) { + mManager.updateBubbleBarLocation(BubbleBarLocation.LEFT); + return true; + } + if (action == R.id.action_move_bubble_bar_right) { + mManager.updateBubbleBarLocation(BubbleBarLocation.RIGHT); + return true; + } + return false; + } + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuItemView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuItemView.java index 00b977721bea..1c71ef415eae 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuItemView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuItemView.java @@ -64,7 +64,7 @@ public class BubbleBarMenuItemView extends LinearLayout { void update(Icon icon, String title, @ColorInt int tint) { if (tint == Color.TRANSPARENT) { final TypedArray typedArray = getContext().obtainStyledAttributes( - new int[]{android.R.attr.textColorPrimary}); + new int[]{com.android.internal.R.attr.materialColorOnSurface}); mTextView.setTextColor(typedArray.getColor(0, Color.BLACK)); } else { icon.setTint(tint); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuView.java index d5f492450ca8..0300869cbbe1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuView.java @@ -17,16 +17,21 @@ package com.android.wm.shell.bubbles.bar; import android.annotation.ColorInt; import android.content.Context; +import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.graphics.Color; import android.graphics.drawable.Icon; import android.util.AttributeSet; import android.view.LayoutInflater; +import android.view.View; import android.view.ViewGroup; +import android.view.accessibility.AccessibilityNodeInfo; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; +import androidx.core.widget.ImageViewCompat; + import com.android.wm.shell.R; import com.android.wm.shell.bubbles.Bubble; @@ -39,6 +44,7 @@ public class BubbleBarMenuView extends LinearLayout { private ViewGroup mBubbleSectionView; private ViewGroup mActionsSectionView; private ImageView mBubbleIconView; + private ImageView mBubbleDismissIconView; private TextView mBubbleTitleView; public BubbleBarMenuView(Context context) { @@ -65,13 +71,28 @@ public class BubbleBarMenuView extends LinearLayout { mActionsSectionView = findViewById(R.id.bubble_bar_manage_menu_actions_section); mBubbleIconView = findViewById(R.id.bubble_bar_manage_menu_bubble_icon); mBubbleTitleView = findViewById(R.id.bubble_bar_manage_menu_bubble_title); - updateActionsBackgroundColor(); + mBubbleDismissIconView = findViewById(R.id.bubble_bar_manage_menu_dismiss_icon); + updateThemeColors(); + + mBubbleSectionView.setAccessibilityDelegate(new AccessibilityDelegate() { + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(host, info); + info.addAction(new AccessibilityNodeInfo.AccessibilityAction( + AccessibilityNodeInfo.ACTION_CLICK, getResources().getString( + R.string.bubble_accessibility_action_collapse_menu))); + } + }); } - private void updateActionsBackgroundColor() { + private void updateThemeColors() { try (TypedArray ta = mContext.obtainStyledAttributes(new int[]{ - com.android.internal.R.attr.materialColorSurfaceBright})) { + com.android.internal.R.attr.materialColorSurfaceBright, + com.android.internal.R.attr.materialColorOnSurface + })) { mActionsSectionView.getBackground().setTint(ta.getColor(0, Color.WHITE)); + ImageViewCompat.setImageTintList(mBubbleDismissIconView, + ColorStateList.valueOf(ta.getColor(1, Color.BLACK))); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java index 02918db124e3..0d72998eb2e8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java @@ -19,12 +19,13 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Color; import android.graphics.drawable.Icon; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import androidx.core.content.ContextCompat; import androidx.dynamicanimation.animation.DynamicAnimation; import androidx.dynamicanimation.animation.SpringForce; @@ -172,12 +173,17 @@ class BubbleBarMenuViewController { private ArrayList<BubbleBarMenuView.MenuAction> createMenuActions(Bubble bubble) { ArrayList<BubbleBarMenuView.MenuAction> menuActions = new ArrayList<>(); Resources resources = mContext.getResources(); - + int tintColor; + try (TypedArray ta = mContext.obtainStyledAttributes(new int[]{ + com.android.internal.R.attr.materialColorOnSurface})) { + tintColor = ta.getColor(0, Color.TRANSPARENT); + } if (bubble.isConversation()) { // Don't bubble conversation action menuActions.add(new BubbleBarMenuView.MenuAction( Icon.createWithResource(mContext, R.drawable.bubble_ic_stop_bubble), resources.getString(R.string.bubbles_dont_bubble_conversation), + tintColor, view -> { hideMenu(true /* animated */); if (mListener != null) { @@ -204,7 +210,7 @@ class BubbleBarMenuViewController { menuActions.add(new BubbleBarMenuView.MenuAction( Icon.createWithResource(resources, R.drawable.ic_remove_no_shadow), resources.getString(R.string.bubble_dismiss_text), - ContextCompat.getColor(mContext, R.color.bubble_bar_expanded_view_menu_close), + tintColor, view -> { hideMenu(true /* animated */); if (mListener != null) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java index 1fb0e1745e3e..c4c177cbcc28 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java @@ -47,6 +47,8 @@ public class DisplayInsetsController implements DisplayController.OnDisplaysChan private final SparseArray<PerDisplay> mInsetsPerDisplay = new SparseArray<>(); private final SparseArray<CopyOnWriteArrayList<OnInsetsChangedListener>> mListeners = new SparseArray<>(); + private final CopyOnWriteArrayList<OnInsetsChangedListener> mGlobalListeners = + new CopyOnWriteArrayList<>(); public DisplayInsetsController(IWindowManager wmService, ShellInit shellInit, @@ -81,6 +83,16 @@ public class DisplayInsetsController implements DisplayController.OnDisplaysChan } /** + * Adds a callback to listen for insets changes for any display. Note that the + * listener will not be updated with the existing state of the insets on any display. + */ + public void addGlobalInsetsChangedListener(OnInsetsChangedListener listener) { + if (!mGlobalListeners.contains(listener)) { + mGlobalListeners.add(listener); + } + } + + /** * Removes a callback listening for insets changes from a particular display. */ public void removeInsetsChangedListener(int displayId, OnInsetsChangedListener listener) { @@ -91,6 +103,13 @@ public class DisplayInsetsController implements DisplayController.OnDisplaysChan listeners.remove(listener); } + /** + * Removes a callback listening for insets changes from any display. + */ + public void removeGlobalInsetsChangedListener(OnInsetsChangedListener listener) { + mGlobalListeners.remove(listener); + } + @Override public void onDisplayAdded(int displayId) { PerDisplay pd = new PerDisplay(displayId); @@ -138,12 +157,17 @@ public class DisplayInsetsController implements DisplayController.OnDisplaysChan private void insetsChanged(InsetsState insetsState) { CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(mDisplayId); - if (listeners == null) { + if (listeners == null && mGlobalListeners.isEmpty()) { return; } mDisplayController.updateDisplayInsets(mDisplayId, insetsState); - for (OnInsetsChangedListener listener : listeners) { - listener.insetsChanged(insetsState); + for (OnInsetsChangedListener listener : mGlobalListeners) { + listener.insetsChanged(mDisplayId, insetsState); + } + if (listeners != null) { + for (OnInsetsChangedListener listener : listeners) { + listener.insetsChanged(mDisplayId, insetsState); + } } } @@ -285,6 +309,13 @@ public class DisplayInsetsController implements DisplayController.OnDisplaysChan default void insetsChanged(InsetsState insetsState) {} /** + * Called when the window insets configuration has changed for the given display. + */ + default void insetsChanged(int displayId, InsetsState insetsState) { + insetsChanged(insetsState); + } + + /** * Called when this window retrieved control over a specified set of insets sources. */ default void insetsControlChanged(InsetsState insetsState, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java index 19a109e9a28c..e2988bc6f2aa 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java @@ -23,7 +23,10 @@ import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMA import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; +import static com.android.wm.shell.common.split.SplitLayout.BEHIND_APP_VEIL_LAYER; +import static com.android.wm.shell.common.split.SplitLayout.FRONT_APP_VEIL_LAYER; import static com.android.wm.shell.common.split.SplitScreenConstants.FADE_DURATION; +import static com.android.wm.shell.common.split.SplitScreenConstants.VEIL_DELAY_DURATION; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -74,7 +77,7 @@ public class SplitDecorManager extends WindowlessWindowManager { private final SurfaceSession mSurfaceSession; private Drawable mIcon; - private ImageView mResizingIconView; + private ImageView mVeilIconView; private SurfaceControlViewHost mViewHost; private SurfaceControl mHostLeash; private SurfaceControl mIconLeash; @@ -83,13 +86,14 @@ public class SplitDecorManager extends WindowlessWindowManager { private SurfaceControl mScreenshot; private boolean mShown; - private boolean mIsResizing; + /** True if the task is going through some kind of transition (moving or changing size). */ + private boolean mIsCurrentlyChanging; /** The original bounds of the main task, captured at the beginning of a resize transition. */ private final Rect mOldMainBounds = new Rect(); /** The original bounds of the side task, captured at the beginning of a resize transition. */ private final Rect mOldSideBounds = new Rect(); /** The current bounds of the main task, mid-resize. */ - private final Rect mResizingBounds = new Rect(); + private final Rect mInstantaneousBounds = new Rect(); private final Rect mTempRect = new Rect(); private ValueAnimator mFadeAnimator; private ValueAnimator mScreenshotAnimator; @@ -134,7 +138,7 @@ public class SplitDecorManager extends WindowlessWindowManager { mIconSize = context.getResources().getDimensionPixelSize(R.dimen.split_icon_size); final FrameLayout rootLayout = (FrameLayout) LayoutInflater.from(context) .inflate(R.layout.split_decor, null); - mResizingIconView = rootLayout.findViewById(R.id.split_resizing_icon); + mVeilIconView = rootLayout.findViewById(R.id.split_resizing_icon); final WindowManager.LayoutParams lp = new WindowManager.LayoutParams( 0 /* width */, 0 /* height */, TYPE_APPLICATION_OVERLAY, @@ -191,28 +195,28 @@ public class SplitDecorManager extends WindowlessWindowManager { } mHostLeash = null; mIcon = null; - mResizingIconView = null; - mIsResizing = false; + mVeilIconView = null; + mIsCurrentlyChanging = false; mShown = false; mOldMainBounds.setEmpty(); mOldSideBounds.setEmpty(); - mResizingBounds.setEmpty(); + mInstantaneousBounds.setEmpty(); } /** Showing resizing hint. */ public void onResizing(ActivityManager.RunningTaskInfo resizingTask, Rect newBounds, Rect sideBounds, SurfaceControl.Transaction t, int offsetX, int offsetY, boolean immediately) { - if (mResizingIconView == null) { + if (mVeilIconView == null) { return; } - if (!mIsResizing) { - mIsResizing = true; + if (!mIsCurrentlyChanging) { + mIsCurrentlyChanging = true; mOldMainBounds.set(newBounds); mOldSideBounds.set(sideBounds); } - mResizingBounds.set(newBounds); + mInstantaneousBounds.set(newBounds); mOffsetX = offsetX; mOffsetY = offsetY; @@ -254,8 +258,8 @@ public class SplitDecorManager extends WindowlessWindowManager { if (mIcon == null && resizingTask.topActivityInfo != null) { mIcon = mIconProvider.getIcon(resizingTask.topActivityInfo); - mResizingIconView.setImageDrawable(mIcon); - mResizingIconView.setVisibility(View.VISIBLE); + mVeilIconView.setImageDrawable(mIcon); + mVeilIconView.setVisibility(View.VISIBLE); WindowManager.LayoutParams lp = (WindowManager.LayoutParams) mViewHost.getView().getLayoutParams(); @@ -275,7 +279,12 @@ public class SplitDecorManager extends WindowlessWindowManager { t.setAlpha(mIconLeash, showVeil ? 1f : 0f); t.setVisibility(mIconLeash, showVeil); } else { - startFadeAnimation(showVeil, false, null); + startFadeAnimation( + showVeil, + false /* releaseSurface */, + null /* finishedCallback */, + false /* addDelay */ + ); } mShown = showVeil; } @@ -320,19 +329,19 @@ public class SplitDecorManager extends WindowlessWindowManager { mScreenshotAnimator.start(); } - if (mResizingIconView == null) { + if (mVeilIconView == null) { if (mRunningAnimationCount == 0 && animFinishedCallback != null) { animFinishedCallback.accept(false); } return; } - mIsResizing = false; + mIsCurrentlyChanging = false; mOffsetX = 0; mOffsetY = 0; mOldMainBounds.setEmpty(); mOldSideBounds.setEmpty(); - mResizingBounds.setEmpty(); + mInstantaneousBounds.setEmpty(); if (mFadeAnimator != null && mFadeAnimator.isRunning()) { if (!mShown) { // If fade-out animation is running, just add release callback to it. @@ -356,7 +365,7 @@ public class SplitDecorManager extends WindowlessWindowManager { if (mRunningAnimationCount == 0 && animFinishedCallback != null) { animFinishedCallback.accept(true); } - }); + }, false /* addDelay */); } else { // Decor surface is hidden so release it directly. releaseDecor(t); @@ -366,9 +375,94 @@ public class SplitDecorManager extends WindowlessWindowManager { } } + /** + * Called (on every frame) when two split apps are swapping, and a veil is needed. + */ + public void drawNextVeilFrameForSwapAnimation(ActivityManager.RunningTaskInfo resizingTask, + Rect newBounds, SurfaceControl.Transaction t, boolean isGoingBehind, + SurfaceControl leash, float iconOffsetX, float iconOffsetY) { + if (mVeilIconView == null) { + return; + } + + if (!mIsCurrentlyChanging) { + mIsCurrentlyChanging = true; + } + + mInstantaneousBounds.set(newBounds); + mOffsetX = (int) iconOffsetX; + mOffsetY = (int) iconOffsetY; + + t.setLayer(leash, isGoingBehind ? BEHIND_APP_VEIL_LAYER : FRONT_APP_VEIL_LAYER); + + if (!mShown) { + if (mFadeAnimator != null && mFadeAnimator.isRunning()) { + // Cancel mFadeAnimator if it is running + mFadeAnimator.cancel(); + } + } + + if (mBackgroundLeash == null) { + // Initialize background + mBackgroundLeash = SurfaceUtils.makeColorLayer(mHostLeash, + RESIZING_BACKGROUND_SURFACE_NAME, mSurfaceSession); + t.setColor(mBackgroundLeash, getResizingBackgroundColor(resizingTask)) + .setLayer(mBackgroundLeash, Integer.MAX_VALUE - 1); + } + + if (mIcon == null && resizingTask.topActivityInfo != null) { + // Initialize icon + mIcon = mIconProvider.getIcon(resizingTask.topActivityInfo); + mVeilIconView.setImageDrawable(mIcon); + mVeilIconView.setVisibility(View.VISIBLE); + + WindowManager.LayoutParams lp = + (WindowManager.LayoutParams) mViewHost.getView().getLayoutParams(); + lp.width = mIconSize; + lp.height = mIconSize; + mViewHost.relayout(lp); + + t.setLayer(mIconLeash, Integer.MAX_VALUE); + } + + t.setPosition(mIconLeash, + newBounds.width() / 2 - mIconSize / 2 - mOffsetX, + newBounds.height() / 2 - mIconSize / 2 - mOffsetY); + + // If this is the first frame, we need to trigger the veil's fade-in animation. + if (!mShown) { + startFadeAnimation( + true /* show */, + false /* releaseSurface */, + null /* finishedCallball */, + false /* addDelay */ + ); + mShown = true; + } + } + + /** Called at the end of the swap animation. */ + public void fadeOutVeilAndCleanUp(SurfaceControl.Transaction t) { + if (mVeilIconView == null) { + return; + } + + // Recenter icon + t.setPosition(mIconLeash, + mInstantaneousBounds.width() / 2f - mIconSize / 2f, + mInstantaneousBounds.height() / 2f - mIconSize / 2f); + + mIsCurrentlyChanging = false; + mOffsetX = 0; + mOffsetY = 0; + mInstantaneousBounds.setEmpty(); + + fadeOutDecor(() -> {}, true /* addDelay */); + } + /** Screenshot host leash and attach on it if meet some conditions */ public void screenshotIfNeeded(SurfaceControl.Transaction t) { - if (!mShown && mIsResizing && !mOldMainBounds.equals(mResizingBounds)) { + if (!mShown && mIsCurrentlyChanging && !mOldMainBounds.equals(mInstantaneousBounds)) { if (mScreenshotAnimator != null && mScreenshotAnimator.isRunning()) { mScreenshotAnimator.cancel(); } else if (mScreenshot != null) { @@ -386,7 +480,7 @@ public class SplitDecorManager extends WindowlessWindowManager { public void setScreenshotIfNeeded(SurfaceControl screenshot, SurfaceControl.Transaction t) { if (screenshot == null || !screenshot.isValid()) return; - if (!mShown && mIsResizing && !mOldMainBounds.equals(mResizingBounds)) { + if (!mShown && mIsCurrentlyChanging && !mOldMainBounds.equals(mInstantaneousBounds)) { if (mScreenshotAnimator != null && mScreenshotAnimator.isRunning()) { mScreenshotAnimator.cancel(); } else if (mScreenshot != null) { @@ -401,24 +495,35 @@ public class SplitDecorManager extends WindowlessWindowManager { /** Fade-out decor surface with animation end callback, if decor is hidden, run the callback * directly. */ - public void fadeOutDecor(Runnable finishedCallback) { + public void fadeOutDecor(Runnable finishedCallback, boolean addDelay) { if (mShown) { // If previous animation is running, just cancel it. if (mFadeAnimator != null && mFadeAnimator.isRunning()) { mFadeAnimator.cancel(); } - startFadeAnimation(false /* show */, true, finishedCallback); + startFadeAnimation( + false /* show */, true /* releaseSurface */, finishedCallback, addDelay); mShown = false; } else { if (finishedCallback != null) finishedCallback.run(); } } + /** + * Fades the veil in or out. Called at the first frame of a movement or resize when a veil is + * needed (with show = true), and called again at the end (with show = false). + * @param addDelay If true, adds a short delay before fading out to get the app behind the veil + * time to redraw. + */ private void startFadeAnimation(boolean show, boolean releaseSurface, - Runnable finishedCallback) { + Runnable finishedCallback, boolean addDelay) { final SurfaceControl.Transaction animT = new SurfaceControl.Transaction(); + mFadeAnimator = ValueAnimator.ofFloat(0f, 1f); + if (addDelay) { + mFadeAnimator.setStartDelay(VEIL_DELAY_DURATION); + } mFadeAnimator.setDuration(FADE_DURATION); mFadeAnimator.addUpdateListener(valueAnimator-> { final float progress = (float) valueAnimator.getAnimatedValue(); @@ -481,8 +586,8 @@ public class SplitDecorManager extends WindowlessWindowManager { } if (mIcon != null) { - mResizingIconView.setVisibility(View.GONE); - mResizingIconView.setImageDrawable(null); + mVeilIconView.setVisibility(View.GONE); + mVeilIconView.setImageDrawable(null); t.hide(mIconLeash); mIcon = null; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java index 51f9de8305f8..0e050694c733 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java @@ -53,6 +53,8 @@ import android.view.RoundedCorner; import android.view.SurfaceControl; import android.view.WindowInsets; import android.view.WindowManager; +import android.view.animation.Interpolator; +import android.view.animation.PathInterpolator; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; @@ -68,10 +70,12 @@ import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayImeController; import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.common.split.SplitScreenConstants.PersistentSnapPosition; import com.android.wm.shell.common.split.SplitScreenConstants.SnapPosition; import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition; import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.splitscreen.StageTaskListener; import java.io.PrintWriter; import java.util.function.Consumer; @@ -87,10 +91,29 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange public static final int PARALLAX_ALIGN_CENTER = 2; public static final int FLING_RESIZE_DURATION = 250; - private static final int FLING_SWITCH_DURATION = 350; private static final int FLING_ENTER_DURATION = 450; private static final int FLING_EXIT_DURATION = 450; + // Here are some (arbitrarily decided) layer definitions used during animations to make sure the + // layers stay in order. Note: This does not affect any other layer numbering systems because + // the layer system in WindowManager is local within sibling groups. So, for example, each + // "veil layer" defined here actually has two sub-layers; and *their* layer values, which we set + // in SplitDecorManager, are only important relative to each other. + public static final int DIVIDER_LAYER = 0; + public static final int FRONT_APP_VEIL_LAYER = DIVIDER_LAYER + 20; + public static final int FRONT_APP_LAYER = DIVIDER_LAYER + 10; + public static final int BEHIND_APP_VEIL_LAYER = DIVIDER_LAYER - 10; + public static final int BEHIND_APP_LAYER = DIVIDER_LAYER - 20; + + // Animation specs for the swap animation + private static final int SWAP_ANIMATION_TOTAL_DURATION = 500; + private static final float SWAP_ANIMATION_SHRINK_DURATION = 83; + private static final float SWAP_ANIMATION_SHRINK_MARGIN_DP = 14; + private static final Interpolator SHRINK_INTERPOLATOR = + new PathInterpolator(0.2f, 0f, 0f, 1f); + private static final Interpolator GROW_INTERPOLATOR = + new PathInterpolator(0.45f, 0f, 0.5f, 1f); + private int mDividerWindowWidth; private int mDividerInsets; private int mDividerSize; @@ -134,6 +157,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange private final InteractionJankMonitor mInteractionJankMonitor; private boolean mIsLeftRightSplit; private ValueAnimator mDividerFlingAnimator; + private AnimatorSet mSwapAnimator; public SplitLayout(String windowName, Context context, Configuration configuration, SplitLayoutHandler splitLayoutHandler, @@ -579,6 +603,10 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange } void onDoubleTappedDivider() { + if (isCurrentlySwapping()) { + return; + } + mSplitLayoutHandler.onDoubleTappedDivider(); } @@ -685,36 +713,43 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange } /** Switch both surface position with animation. */ - public void splitSwitching(SurfaceControl.Transaction t, SurfaceControl leash1, - SurfaceControl leash2, Consumer<Rect> finishCallback) { + public void playSwapAnimation(SurfaceControl.Transaction t, StageTaskListener topLeftStage, + StageTaskListener bottomRightStage, Consumer<Rect> finishCallback) { final Rect insets = getDisplayStableInsets(mContext); + // If we have insets in the direction of the swap, the animation won't look correct because + // window contents will shift and redraw again at the end. So we show a veil to hide that. insets.set(mIsLeftRightSplit ? insets.left : 0, mIsLeftRightSplit ? 0 : insets.top, mIsLeftRightSplit ? insets.right : 0, mIsLeftRightSplit ? 0 : insets.bottom); + final boolean shouldVeil = + insets.left != 0 || insets.top != 0 || insets.right != 0 || insets.bottom != 0; final int dividerPos = mDividerSnapAlgorithm.calculateNonDismissingSnapTarget( mIsLeftRightSplit ? mBounds2.width() : mBounds2.height()).position; - final Rect distBounds1 = new Rect(); - final Rect distBounds2 = new Rect(); - final Rect distDividerBounds = new Rect(); - // Compute dist bounds. - updateBounds(dividerPos, distBounds2, distBounds1, distDividerBounds, + final Rect endBounds1 = new Rect(); + final Rect endBounds2 = new Rect(); + final Rect endDividerBounds = new Rect(); + // Compute destination bounds. + updateBounds(dividerPos, endBounds2, endBounds1, endDividerBounds, false /* setEffectBounds */); // Offset to real position under root container. - distBounds1.offset(-mRootBounds.left, -mRootBounds.top); - distBounds2.offset(-mRootBounds.left, -mRootBounds.top); - distDividerBounds.offset(-mRootBounds.left, -mRootBounds.top); - - ValueAnimator animator1 = moveSurface(t, leash1, getRefBounds1(), distBounds1, - -insets.left, -insets.top); - ValueAnimator animator2 = moveSurface(t, leash2, getRefBounds2(), distBounds2, - insets.left, insets.top); - ValueAnimator animator3 = moveSurface(t, getDividerLeash(), getRefDividerBounds(), - distDividerBounds, 0 /* offsetX */, 0 /* offsetY */); - - AnimatorSet set = new AnimatorSet(); - set.playTogether(animator1, animator2, animator3); - set.setDuration(FLING_SWITCH_DURATION); - set.addListener(new AnimatorListenerAdapter() { + endBounds1.offset(-mRootBounds.left, -mRootBounds.top); + endBounds2.offset(-mRootBounds.left, -mRootBounds.top); + endDividerBounds.offset(-mRootBounds.left, -mRootBounds.top); + + ValueAnimator animator1 = moveSurface(t, topLeftStage, getRefBounds1(), endBounds1, + -insets.left, -insets.top, true /* roundCorners */, true /* isGoingBehind */, + shouldVeil); + ValueAnimator animator2 = moveSurface(t, bottomRightStage, getRefBounds2(), endBounds2, + insets.left, insets.top, true /* roundCorners */, false /* isGoingBehind */, + shouldVeil); + ValueAnimator animator3 = moveSurface(t, null /* stage */, getRefDividerBounds(), + endDividerBounds, 0 /* offsetX */, 0 /* offsetY */, false /* roundCorners */, + false /* isGoingBehind */, false /* addVeil */); + + mSwapAnimator = new AnimatorSet(); + mSwapAnimator.playTogether(animator1, animator2, animator3); + mSwapAnimator.setDuration(SWAP_ANIMATION_TOTAL_DURATION); + mSwapAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { mInteractionJankMonitor.begin(getDividerLeash(), @@ -734,36 +769,144 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange mInteractionJankMonitor.cancel(CUJ_SPLIT_SCREEN_DOUBLE_TAP_DIVIDER); } }); - set.start(); + mSwapAnimator.start(); + } + + /** Returns true if a swap animation is currently playing. */ + public boolean isCurrentlySwapping() { + return mSwapAnimator != null && mSwapAnimator.isRunning(); } - private ValueAnimator moveSurface(SurfaceControl.Transaction t, SurfaceControl leash, - Rect start, Rect end, float offsetX, float offsetY) { + /** + * Animates a task leash across the screen. Currently used only for the swap animation. + * + * @param stage The stage holding the task being animated. If null, it is the divider. + * @param roundCorners Whether we should round the corners of the task while animating. + * @param isGoingBehind Whether we should a shrink-and-grow effect to the task while it is + * moving. (Simulates moving behind the divider.) + */ + private ValueAnimator moveSurface(SurfaceControl.Transaction t, StageTaskListener stage, + Rect start, Rect end, float offsetX, float offsetY, boolean roundCorners, + boolean isGoingBehind, boolean addVeil) { + final boolean isApp = stage != null; // check if this is an app or a divider + final SurfaceControl leash = isApp ? stage.getRootLeash() : getDividerLeash(); + final ActivityManager.RunningTaskInfo taskInfo = isApp ? stage.getRunningTaskInfo() : null; + final SplitDecorManager decorManager = isApp ? stage.getDecorManager() : null; + Rect tempStart = new Rect(start); Rect tempEnd = new Rect(end); final float diffX = tempEnd.left - tempStart.left; final float diffY = tempEnd.top - tempStart.top; final float diffWidth = tempEnd.width() - tempStart.width(); final float diffHeight = tempEnd.height() - tempStart.height(); + + // Get display measurements (for possible shrink animation). + final RoundedCorner roundedCorner = mSplitWindowManager.getDividerView().getDisplay() + .getRoundedCorner(0 /* position */); + float cornerRadius = roundedCorner == null ? 0 : roundedCorner.getRadius(); + float shrinkMarginPx = PipUtils.dpToPx( + SWAP_ANIMATION_SHRINK_MARGIN_DP, mContext.getResources().getDisplayMetrics()); + float shrinkAmountPx = shrinkMarginPx * 2; + + // Timing calculations + float shrinkPortion = SWAP_ANIMATION_SHRINK_DURATION / SWAP_ANIMATION_TOTAL_DURATION; + float growPortion = 1 - shrinkPortion; + ValueAnimator animator = ValueAnimator.ofFloat(0, 1); + animator.setInterpolator(Interpolators.EMPHASIZED); animator.addUpdateListener(animation -> { if (leash == null) return; + if (roundCorners) { + // Add rounded corners to the task leash while it is animating. + t.setCornerRadius(leash, cornerRadius); + } + + final float progress = (float) animation.getAnimatedValue(); + float instantaneousX = tempStart.left + progress * diffX; + float instantaneousY = tempStart.top + progress * diffY; + int width = (int) (tempStart.width() + progress * diffWidth); + int height = (int) (tempStart.height() + progress * diffHeight); + + if (isGoingBehind) { + float shrinkDiffX; // the position adjustments needed for this frame + float shrinkDiffY; + float shrinkScaleX; // the scale adjustments needed for this frame + float shrinkScaleY; + + // Find the max amount we will be shrinking this leash, as a proportion (e.g. 0.1f). + float maxShrinkX = shrinkAmountPx / height; + float maxShrinkY = shrinkAmountPx / width; + + // Find if we are in the shrinking part of the animation, or the growing part. + boolean shrinking = progress <= shrinkPortion; + + if (shrinking) { + // Find how far into the shrink portion we are (e.g. 0.5f). + float shrinkProgress = progress / shrinkPortion; + // Find how much we should have progressed in shrinking the leash (e.g. 0.8f). + float interpolatedShrinkProgress = + SHRINK_INTERPOLATOR.getInterpolation(shrinkProgress); + // Find how much width proportion we should be taking off (e.g. 0.1f) + float widthProportionLost = maxShrinkX * interpolatedShrinkProgress; + shrinkScaleX = 1 - widthProportionLost; + // Find how much height proportion we should be taking off (e.g. 0.1f) + float heightProportionLost = maxShrinkY * interpolatedShrinkProgress; + shrinkScaleY = 1 - heightProportionLost; + // Add a small amount to the leash's position to keep the task centered. + shrinkDiffX = (width * widthProportionLost) / 2; + shrinkDiffY = (height * heightProportionLost) / 2; + } else { + // Find how far into the grow portion we are (e.g. 0.5f). + float growProgress = (progress - shrinkPortion) / growPortion; + // Find how much we should have progressed in growing the leash (e.g. 0.8f). + float interpolatedGrowProgress = + GROW_INTERPOLATOR.getInterpolation(growProgress); + // Find how much width proportion we should be taking off (e.g. 0.1f) + float widthProportionLost = maxShrinkX * (1 - interpolatedGrowProgress); + shrinkScaleX = 1 - widthProportionLost; + // Find how much height proportion we should be taking off (e.g. 0.1f) + float heightProportionLost = maxShrinkY * (1 - interpolatedGrowProgress); + shrinkScaleY = 1 - heightProportionLost; + // Add a small amount to the leash's position to keep the task centered. + shrinkDiffX = (width * widthProportionLost) / 2; + shrinkDiffY = (height * heightProportionLost) / 2; + } + + instantaneousX += shrinkDiffX; + instantaneousY += shrinkDiffY; + width *= shrinkScaleX; + height *= shrinkScaleY; + // Set scale on the leash's contents. + t.setScale(leash, shrinkScaleX, shrinkScaleY); + } + + // Set layers + if (taskInfo != null) { + t.setLayer(leash, isGoingBehind ? BEHIND_APP_LAYER : FRONT_APP_LAYER); + } else { + t.setLayer(leash, DIVIDER_LAYER); + } - final float scale = (float) animation.getAnimatedValue(); - final float distX = tempStart.left + scale * diffX; - final float distY = tempStart.top + scale * diffY; - final int width = (int) (tempStart.width() + scale * diffWidth); - final int height = (int) (tempStart.height() + scale * diffHeight); if (offsetX == 0 && offsetY == 0) { - t.setPosition(leash, distX, distY); + t.setPosition(leash, instantaneousX, instantaneousY); + mTempRect.set((int) instantaneousX, (int) instantaneousY, + (int) (instantaneousX + width), (int) (instantaneousY + height)); t.setWindowCrop(leash, width, height); + if (addVeil) { + decorManager.drawNextVeilFrameForSwapAnimation( + taskInfo, mTempRect, t, isGoingBehind, leash, 0, 0); + } } else { - final int diffOffsetX = (int) (scale * offsetX); - final int diffOffsetY = (int) (scale * offsetY); - t.setPosition(leash, distX + diffOffsetX, distY + diffOffsetY); + final int diffOffsetX = (int) (progress * offsetX); + final int diffOffsetY = (int) (progress * offsetY); + t.setPosition(leash, instantaneousX + diffOffsetX, instantaneousY + diffOffsetY); mTempRect.set(0, 0, width, height); mTempRect.offsetTo(-diffOffsetX, -diffOffsetY); t.setCrop(leash, mTempRect); + if (addVeil) { + decorManager.drawNextVeilFrameForSwapAnimation( + taskInfo, mTempRect, t, isGoingBehind, leash, diffOffsetX, diffOffsetY); + } } t.apply(); }); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java index e8c809e5db4a..8c06de79ba76 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java @@ -29,6 +29,8 @@ import com.android.wm.shell.shared.TransitionUtil; public class SplitScreenConstants { /** Duration used for every split fade-in or fade-out. */ public static final int FADE_DURATION = 133; + /** Duration where we keep an app veiled to allow it to redraw itself behind the scenes. */ + public static final int VEIL_DELAY_DURATION = 400; /** Key for passing in widget intents when invoking split from launcher workspace. */ public static final String KEY_EXTRA_WIDGET_INTENT = "key_extra_widget_intent"; 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/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index a18bbadbde69..e787a3d94000 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -32,6 +32,7 @@ import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.UiEventLogger; import com.android.internal.statusbar.IStatusBarService; import com.android.launcher3.icons.IconProvider; +import com.android.window.flags.Flags; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.WindowManagerShellWrapper; @@ -58,6 +59,7 @@ import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.dagger.back.ShellBackAnimationModule; import com.android.wm.shell.dagger.pip.PipModule; +import com.android.wm.shell.desktopmode.DefaultDragToDesktopTransitionHandler; import com.android.wm.shell.desktopmode.DesktopModeEventLogger; import com.android.wm.shell.desktopmode.DesktopModeLoggerTransitionObserver; import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; @@ -68,7 +70,9 @@ import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler; import com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler; import com.android.wm.shell.desktopmode.ExitDesktopTaskTransitionHandler; import com.android.wm.shell.desktopmode.ReturnToDragStartAnimator; +import com.android.wm.shell.desktopmode.SpringDragToDesktopTransitionHandler; import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler; +import com.android.wm.shell.desktopmode.education.data.AppHandleEducationDatastoreRepository; import com.android.wm.shell.draganddrop.DragAndDropController; import com.android.wm.shell.draganddrop.GlobalDragListener; import com.android.wm.shell.freeform.FreeformComponents; @@ -603,10 +607,12 @@ public abstract class WMShellModule { Context context, Transitions transitions, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, - Optional<DesktopTasksLimiter> desktopTasksLimiter, InteractionJankMonitor interactionJankMonitor) { - return new DragToDesktopTransitionHandler(context, transitions, - rootTaskDisplayAreaOrganizer, interactionJankMonitor); + return Flags.enableDesktopWindowingTransitions() + ? new SpringDragToDesktopTransitionHandler(context, transitions, + rootTaskDisplayAreaOrganizer, interactionJankMonitor) + : new DefaultDragToDesktopTransitionHandler(context, transitions, + rootTaskDisplayAreaOrganizer, interactionJankMonitor); } @WMSingleton @@ -674,6 +680,13 @@ public abstract class WMShellModule { return new DesktopModeEventLogger(); } + @WMSingleton + @Provides + static AppHandleEducationDatastoreRepository provideAppHandleEducationDatastoreRepository( + Context context) { + return new AppHandleEducationDatastoreRepository(context); + } + // // Drag and drop // diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt index 400882a8da9a..05c9d02a0de7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt @@ -73,25 +73,9 @@ class DesktopModeEventLogger { sessionId, taskUpdate.instanceId ) - FrameworkStatsLog.write( - DESKTOP_MODE_TASK_UPDATE_ATOM_ID, - /* task_event */ + logTaskUpdate( FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_ADDED, - /* instance_id */ - taskUpdate.instanceId, - /* uid */ - taskUpdate.uid, - /* task_height */ - taskUpdate.taskHeight, - /* task_width */ - taskUpdate.taskWidth, - /* task_x */ - taskUpdate.taskX, - /* task_y */ - taskUpdate.taskY, - /* session_id */ - sessionId - ) + sessionId, taskUpdate) } /** @@ -105,25 +89,9 @@ class DesktopModeEventLogger { sessionId, taskUpdate.instanceId ) - FrameworkStatsLog.write( - DESKTOP_MODE_TASK_UPDATE_ATOM_ID, - /* task_event */ + logTaskUpdate( FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_REMOVED, - /* instance_id */ - taskUpdate.instanceId, - /* uid */ - taskUpdate.uid, - /* task_height */ - taskUpdate.taskHeight, - /* task_width */ - taskUpdate.taskWidth, - /* task_x */ - taskUpdate.taskX, - /* task_y */ - taskUpdate.taskY, - /* session_id */ - sessionId - ) + sessionId, taskUpdate) } /** @@ -137,10 +105,16 @@ class DesktopModeEventLogger { sessionId, taskUpdate.instanceId ) + logTaskUpdate( + FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED, + sessionId, taskUpdate) + } + + private fun logTaskUpdate(taskEvent: Int, sessionId: Int, taskUpdate: TaskUpdate) { FrameworkStatsLog.write( DESKTOP_MODE_TASK_UPDATE_ATOM_ID, /* task_event */ - FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED, + taskEvent, /* instance_id */ taskUpdate.instanceId, /* uid */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt index 73aa7ceea68d..a6ed3b8cb50c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt @@ -22,6 +22,7 @@ import android.app.TaskInfo import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.content.Context import android.os.IBinder +import android.os.Trace import android.util.SparseArray import android.view.SurfaceControl import android.view.WindowManager @@ -51,6 +52,8 @@ import com.android.wm.shell.shared.TransitionUtil import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.Transitions +const val VISIBLE_TASKS_COUNTER_NAME = "DESKTOP_MODE_VISIBLE_TASKS" + /** * A [Transitions.TransitionObserver] that observes transitions and the proposed changes to log * appropriate desktop mode session log events. This observes transitions related to desktop mode @@ -292,8 +295,14 @@ class DesktopModeLoggerTransitionObserver( val previousTaskInfo = preTransitionVisibleFreeformTasks[taskId] when { // new tasks added - previousTaskInfo == null -> + previousTaskInfo == null -> { desktopModeEventLogger.logTaskAdded(sessionId, currentTaskUpdate) + Trace.setCounter( + Trace.TRACE_TAG_WINDOW_MANAGER, + VISIBLE_TASKS_COUNTER_NAME, + postTransitionVisibleFreeformTasks.size().toLong() + ) + } // old tasks that were resized or repositioned // TODO(b/347935387): Log changes only once they are stable. buildTaskUpdateForTask(previousTaskInfo) != currentTaskUpdate -> @@ -305,6 +314,11 @@ class DesktopModeLoggerTransitionObserver( preTransitionVisibleFreeformTasks.forEach { taskId, taskInfo -> if (!postTransitionVisibleFreeformTasks.containsKey(taskId)) { desktopModeEventLogger.logTaskRemoved(sessionId, buildTaskUpdateForTask(taskInfo)) + Trace.setCounter( + Trace.TRACE_TAG_WINDOW_MANAGER, + VISIBLE_TASKS_COUNTER_NAME, + postTransitionVisibleFreeformTasks.size().toLong() + ) } } } 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/DesktopModeVisualIndicator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java index 6011db7fc752..09f9139cb1d5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java @@ -27,6 +27,7 @@ import android.animation.AnimatorListenerAdapter; import android.animation.RectEvaluator; import android.animation.ValueAnimator; import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.ActivityManager; import android.app.WindowConfiguration; import android.content.Context; @@ -262,12 +263,22 @@ public class DesktopModeVisualIndicator { /** * Fade out indicator without fully releasing it. Animator fades it out while shrinking bounds. + * + * @param finishCallback called when animation ends or gets cancelled */ - private void fadeOutIndicator() { + void fadeOutIndicator(@Nullable Runnable finishCallback) { final VisualIndicatorAnimator animator = VisualIndicatorAnimator .fadeBoundsOut(mView, mCurrentType, mDisplayController.getDisplayLayout(mTaskInfo.displayId)); animator.start(); + if (finishCallback != null) { + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + finishCallback.run(); + } + }); + } mCurrentType = IndicatorType.NO_INDICATOR; } @@ -282,7 +293,7 @@ public class DesktopModeVisualIndicator { if (mCurrentType == IndicatorType.NO_INDICATOR) { fadeInIndicator(newType); } else if (newType == IndicatorType.NO_INDICATOR) { - fadeOutIndicator(); + fadeOutIndicator(null /* finishCallback */); } else { final VisualIndicatorAnimator animator = VisualIndicatorAnimator.animateIndicatorType( mView, mDisplayController.getDisplayLayout(mTaskInfo.displayId), mCurrentType, 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 e154da58028a..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 @@ -163,8 +163,10 @@ class DesktopTasksController( } private fun removeVisualIndicator(tx: SurfaceControl.Transaction) { - visualIndicator?.releaseVisualIndicator(tx) - visualIndicator = null + visualIndicator?.fadeOutIndicator { + visualIndicator?.releaseVisualIndicator(tx) + visualIndicator = null + } } } @@ -193,7 +195,7 @@ class DesktopTasksController( ) transitions.addHandler(this) taskRepository.addVisibleTasksListener(taskVisibilityListener, mainExecutor) - dragToDesktopTransitionHandler.setDragToDesktopStateListener(dragToDesktopStateListener) + dragToDesktopTransitionHandler.dragToDesktopStateListener = dragToDesktopStateListener recentsTransitionHandler.addTransitionStateListener( object : RecentsTransitionStateListener { override fun onAnimationStateChanged(running: Boolean) { @@ -213,7 +215,7 @@ class DesktopTasksController( fun setOnTaskResizeAnimationListener(listener: OnTaskResizeAnimationListener) { toggleResizeDesktopTaskTransitionHandler.setOnTaskResizeAnimationListener(listener) enterDesktopTaskTransitionHandler.setOnTaskResizeAnimationListener(listener) - dragToDesktopTransitionHandler.setOnTaskResizeAnimatorListener(listener) + dragToDesktopTransitionHandler.onTaskResizeAnimationListener = listener } fun setOnTaskRepositionAnimationListener(listener: OnTaskRepositionAnimationListener) { @@ -1068,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/desktopmode/DesktopTasksLimiter.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt index 38675129ce57..597637d3fbfc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt @@ -70,11 +70,11 @@ class DesktopTasksLimiter ( // TODO(b/333018485): replace this observer when implementing the minimize-animation private inner class MinimizeTransitionObserver : TransitionObserver { - private val mPendingTransitionTokensAndTasks = mutableMapOf<IBinder, TaskDetails>() - private val mActiveTransitionTokensAndTasks = mutableMapOf<IBinder, TaskDetails>() + private val pendingTransitionTokensAndTasks = mutableMapOf<IBinder, TaskDetails>() + private val activeTransitionTokensAndTasks = mutableMapOf<IBinder, TaskDetails>() fun addPendingTransitionToken(transition: IBinder, taskDetails: TaskDetails) { - mPendingTransitionTokensAndTasks[transition] = taskDetails + pendingTransitionTokensAndTasks[transition] = taskDetails } override fun onTransitionReady( @@ -83,9 +83,7 @@ class DesktopTasksLimiter ( startTransaction: SurfaceControl.Transaction, finishTransaction: SurfaceControl.Transaction ) { - val taskToMinimize = mPendingTransitionTokensAndTasks.remove(transition) ?: return - taskToMinimize.transitionInfo = info - mActiveTransitionTokensAndTasks[transition] = taskToMinimize + val taskToMinimize = pendingTransitionTokensAndTasks.remove(transition) ?: return if (!taskRepository.isActiveTask(taskToMinimize.taskId)) return @@ -97,6 +95,8 @@ class DesktopTasksLimiter ( return } + taskToMinimize.transitionInfo = info + activeTransitionTokensAndTasks[transition] = taskToMinimize this@DesktopTasksLimiter.markTaskMinimized( taskToMinimize.displayId, taskToMinimize.taskId) } @@ -121,7 +121,7 @@ class DesktopTasksLimiter ( } override fun onTransitionStarting(transition: IBinder) { - val mActiveTaskDetails = mActiveTransitionTokensAndTasks[transition] + val mActiveTaskDetails = activeTransitionTokensAndTasks[transition] if (mActiveTaskDetails != null && mActiveTaskDetails.transitionInfo != null) { // Begin minimize window CUJ instrumentation. interactionJankMonitor.begin( @@ -132,11 +132,11 @@ class DesktopTasksLimiter ( } override fun onTransitionMerged(merged: IBinder, playing: IBinder) { - if (mActiveTransitionTokensAndTasks.remove(merged) != null) { + if (activeTransitionTokensAndTasks.remove(merged) != null) { interactionJankMonitor.end(CUJ_DESKTOP_MODE_MINIMIZE_WINDOW) } - mPendingTransitionTokensAndTasks.remove(merged)?.let { taskToTransfer -> - mPendingTransitionTokensAndTasks[playing] = taskToTransfer + pendingTransitionTokensAndTasks.remove(merged)?.let { taskToTransfer -> + pendingTransitionTokensAndTasks[playing] = taskToTransfer } } @@ -144,14 +144,14 @@ class DesktopTasksLimiter ( ProtoLog.v( ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, "DesktopTasksLimiter: transition %s finished", transition) - if (mActiveTransitionTokensAndTasks.remove(transition) != null) { + if (activeTransitionTokensAndTasks.remove(transition) != null) { if (aborted) { interactionJankMonitor.cancel(CUJ_DESKTOP_MODE_MINIMIZE_WINDOW) } else { interactionJankMonitor.end(CUJ_DESKTOP_MODE_MINIMIZE_WINDOW) } } - mPendingTransitionTokensAndTasks.remove(transition) + pendingTransitionTokensAndTasks.remove(transition) } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt index 5221a4592d39..9874f4c269a4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt @@ -27,19 +27,21 @@ import android.view.WindowManager.TRANSIT_CLOSE import android.window.TransitionInfo import android.window.TransitionInfo.Change import android.window.TransitionRequestInfo -import android.window.WindowContainerToken import android.window.WindowContainerTransaction +import androidx.dynamicanimation.animation.SpringForce import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE import com.android.internal.jank.InteractionJankMonitor import com.android.internal.protolog.ProtoLog import com.android.wm.shell.RootTaskDisplayAreaOrganizer +import com.android.wm.shell.animation.FloatProperties import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition import com.android.wm.shell.protolog.ShellProtoLogGroup import com.android.wm.shell.shared.TransitionUtil +import com.android.wm.shell.shared.animation.PhysicsAnimator import com.android.wm.shell.splitscreen.SplitScreenController import com.android.wm.shell.transition.Transitions import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP @@ -50,40 +52,31 @@ import com.android.wm.shell.windowdecor.MoveToDesktopAnimator import com.android.wm.shell.windowdecor.MoveToDesktopAnimator.Companion.DRAG_FREEFORM_SCALE import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener import java.util.function.Supplier +import kotlin.math.max /** * Handles the transition to enter desktop from fullscreen by dragging on the handle bar. It also * handles the cancellation case where the task is dragged back to the status bar area in the same * gesture. + * + * It's a base sealed class that delegates flag dependant logic to its subclasses: + * [DefaultDragToDesktopTransitionHandler] and [SpringDragToDesktopTransitionHandler] + * + * TODO(b/356764679): Clean up after the full flag rollout */ -class DragToDesktopTransitionHandler( +sealed class DragToDesktopTransitionHandler( private val context: Context, private val transitions: Transitions, private val taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, - private val interactionJankMonitor: InteractionJankMonitor, - private val transactionSupplier: Supplier<SurfaceControl.Transaction>, + protected val interactionJankMonitor: InteractionJankMonitor, + protected val transactionSupplier: Supplier<SurfaceControl.Transaction>, ) : TransitionHandler { - constructor( - context: Context, - transitions: Transitions, - rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, - interactionJankMonitor: InteractionJankMonitor - ) : this( - context, - transitions, - rootTaskDisplayAreaOrganizer, - interactionJankMonitor, - Supplier { SurfaceControl.Transaction() } - ) - - private val rectEvaluator = RectEvaluator(Rect()) + protected val rectEvaluator = RectEvaluator(Rect()) private val launchHomeIntent = Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME) - private var dragToDesktopStateListener: DragToDesktopStateListener? = null private lateinit var splitScreenController: SplitScreenController private var transitionState: TransitionState? = null - private lateinit var onTaskResizeAnimationListener: OnTaskResizeAnimationListener /** Whether a drag-to-desktop transition is in progress. */ val inProgress: Boolean @@ -92,20 +85,18 @@ class DragToDesktopTransitionHandler( /** The task id of the task currently being dragged from fullscreen/split. */ val draggingTaskId: Int get() = transitionState?.draggedTaskId ?: INVALID_TASK_ID - /** Sets a listener to receive callback about events during the transition animation. */ - fun setDragToDesktopStateListener(listener: DragToDesktopStateListener) { - dragToDesktopStateListener = listener - } + + /** Listener to receive callback about events during the transition animation. */ + var dragToDesktopStateListener: DragToDesktopStateListener? = null + + /** Task listener for animation start, task bounds resize, and the animation finish */ + lateinit var onTaskResizeAnimationListener: OnTaskResizeAnimationListener /** Setter needed to avoid cyclic dependency. */ fun setSplitScreenController(controller: SplitScreenController) { splitScreenController = controller } - fun setOnTaskResizeAnimatorListener(listener: OnTaskResizeAnimationListener) { - onTaskResizeAnimationListener = listener - } - /** * Starts a transition that performs a transient launch of Home so that Home is brought to the * front while still keeping the currently focused task that is being dragged resumed. This @@ -307,24 +298,18 @@ class DragToDesktopTransitionHandler( return false } - // Layering: non-wallpaper, non-home tasks excluding the dragged task go at the bottom, - // then Home on top of that, wallpaper on top of that and finally the dragged task on top - // of everything. - val appLayers = info.changes.size - val homeLayers = info.changes.size * 2 - val wallpaperLayers = info.changes.size * 3 - val dragLayer = wallpaperLayers + val layers = calculateStartDragToDesktopLayers(info) val leafTaskFilter = TransitionUtil.LeafTaskFilter() info.changes.withIndex().forEach { (i, change) -> if (TransitionUtil.isWallpaper(change)) { - val layer = wallpaperLayers - i + val layer = layers.wallpaperLayers - i startTransaction.apply { setLayer(change.leash, layer) show(change.leash) } } else if (isHomeChange(change)) { - state.homeToken = change.container - val layer = homeLayers - i + state.homeChange = change + val layer = layers.homeLayers - i startTransaction.apply { setLayer(change.leash, layer) show(change.leash) @@ -338,11 +323,11 @@ class DragToDesktopTransitionHandler( if (state.cancelState == CancelState.NO_CANCEL) { // Normal case, split root goes to the bottom behind everything // else. - appLayers - i + layers.appLayers - i } else { // Cancel-early case, pretend nothing happened so split root stays // top. - dragLayer + layers.dragLayer } startTransaction.apply { setLayer(change.leash, layer) @@ -357,7 +342,7 @@ class DragToDesktopTransitionHandler( state.draggedTaskChange = change val bounds = change.endAbsBounds startTransaction.apply { - setLayer(change.leash, dragLayer) + setLayer(change.leash, layers.dragLayer) setWindowCrop(change.leash, bounds.width(), bounds.height()) show(change.leash) } @@ -370,7 +355,7 @@ class DragToDesktopTransitionHandler( state.otherRootChanges.add(change) val bounds = change.endAbsBounds startTransaction.apply { - setLayer(change.leash, appLayers - i) + setLayer(change.leash, layers.appLayers - i) setWindowCrop(change.leash, bounds.width(), bounds.height()) show(change.leash) } @@ -404,7 +389,7 @@ class DragToDesktopTransitionHandler( ) val bounds = change.endAbsBounds startTransaction.apply { - setLayer(change.leash, dragLayer) + setLayer(change.leash, layers.dragLayer) setWindowCrop(change.leash, bounds.width(), bounds.height()) show(change.leash) } @@ -452,6 +437,15 @@ class DragToDesktopTransitionHandler( return true } + /** + * Calculates start drag to desktop layers for transition [info]. The leash layer is calculated + * based on its change position in the transition, e.g. `appLayer = appLayers - i`, where i is + * the change index. + */ + protected abstract fun calculateStartDragToDesktopLayers( + info: TransitionInfo + ): DragToDesktopLayers + override fun mergeAnimation( transition: IBinder, info: TransitionInfo, @@ -483,114 +477,140 @@ class DragToDesktopTransitionHandler( state.startTransitionFinishCb ?: error("Start transition expected to be waiting for merge but wasn't") if (isEndTransition) { - info.changes.withIndex().forEach { (i, change) -> - // If we're exiting split, hide the remaining split task. - if ( - state is TransitionState.FromSplit && - change.taskInfo?.taskId == state.otherSplitTask - ) { - t.hide(change.leash) - startTransactionFinishT.hide(change.leash) + setupEndDragToDesktop( + info, + startTransaction = t, + finishTransaction = startTransactionFinishT + ) + // Call finishCallback to merge animation before startTransitionFinishCb is called + finishCallback.onTransitionFinished(null /* wct */) + animateEndDragToDesktop(startTransaction = t, startTransitionFinishCb) + } else if (isCancelTransition) { + info.changes.forEach { change -> + t.show(change.leash) + startTransactionFinishT.show(change.leash) + } + t.apply() + finishCallback.onTransitionFinished(null /* wct */) + startTransitionFinishCb.onTransitionFinished(null /* wct */) + clearState() + } + } + + protected open fun setupEndDragToDesktop( + info: TransitionInfo, + startTransaction: SurfaceControl.Transaction, + finishTransaction: SurfaceControl.Transaction + ) { + val state = requireTransitionState() + val freeformTaskChanges = mutableListOf<Change>() + info.changes.forEachIndexed { i, change -> + when { + state is TransitionState.FromSplit && + change.taskInfo?.taskId == state.otherSplitTask -> { + // If we're exiting split, hide the remaining split task. + startTransaction.hide(change.leash) + finishTransaction.hide(change.leash) + } + change.mode == TRANSIT_CLOSE -> { + startTransaction.hide(change.leash) + finishTransaction.hide(change.leash) } - if (change.mode == TRANSIT_CLOSE) { - t.hide(change.leash) - startTransactionFinishT.hide(change.leash) - } else if (change.taskInfo?.taskId == state.draggedTaskId) { - t.show(change.leash) - startTransactionFinishT.show(change.leash) + change.taskInfo?.taskId == state.draggedTaskId -> { + startTransaction.show(change.leash) + finishTransaction.show(change.leash) state.draggedTaskChange = change - } else if (change.taskInfo?.windowingMode == WINDOWING_MODE_FREEFORM) { + } + change.taskInfo?.windowingMode == WINDOWING_MODE_FREEFORM -> { // Other freeform tasks that are being restored go behind the dragged task. val draggedTaskLeash = state.draggedTaskChange?.leash ?: error("Expected dragged leash to be non-null") - t.setRelativeLayer(change.leash, draggedTaskLeash, -i) - startTransactionFinishT.setRelativeLayer(change.leash, draggedTaskLeash, -i) + startTransaction.setRelativeLayer(change.leash, draggedTaskLeash, -i) + finishTransaction.setRelativeLayer(change.leash, draggedTaskLeash, -i) + freeformTaskChanges.add(change) } } + } - val draggedTaskChange = - state.draggedTaskChange - ?: throw IllegalStateException("Expected non-null change of dragged task") - val draggedTaskLeash = draggedTaskChange.leash - val startBounds = draggedTaskChange.startAbsBounds - val endBounds = draggedTaskChange.endAbsBounds - - // Pause any animation that may be currently playing; we will use the relevant - // details of that animation here. - state.dragAnimator.cancelAnimator() - // We still apply scale to task bounds; as we animate the bounds to their - // end value, animate scale to 1. - val startScale = state.dragAnimator.scale - val startPosition = state.dragAnimator.position - val unscaledStartWidth = startBounds.width() - val unscaledStartHeight = startBounds.height() - val unscaledStartBounds = - Rect( - startPosition.x.toInt(), - startPosition.y.toInt(), - startPosition.x.toInt() + unscaledStartWidth, - startPosition.y.toInt() + unscaledStartHeight - ) + state.freeformTaskChanges = freeformTaskChanges + } + + protected open fun animateEndDragToDesktop( + startTransaction: SurfaceControl.Transaction, + startTransitionFinishCb: Transitions.TransitionFinishCallback + ) { + val state = requireTransitionState() + val draggedTaskChange = + state.draggedTaskChange ?: error("Expected non-null change of dragged task") + val draggedTaskLeash = draggedTaskChange.leash + val startBounds = draggedTaskChange.startAbsBounds + val endBounds = draggedTaskChange.endAbsBounds - dragToDesktopStateListener?.onCommitToDesktopAnimationStart(t) - // Accept the merge by applying the merging transaction (applied by #showResizeVeil) - // and finish callback. Show the veil and position the task at the first frame before - // starting the final animation. - onTaskResizeAnimationListener.onAnimationStart( - state.draggedTaskId, - t, - unscaledStartBounds + // Cancel any animation that may be currently playing; we will use the relevant + // details of that animation here. + state.dragAnimator.cancelAnimator() + // We still apply scale to task bounds; as we animate the bounds to their + // end value, animate scale to 1. + val startScale = state.dragAnimator.scale + val startPosition = state.dragAnimator.position + val unscaledStartWidth = startBounds.width() + val unscaledStartHeight = startBounds.height() + val unscaledStartBounds = + Rect( + startPosition.x.toInt(), + startPosition.y.toInt(), + startPosition.x.toInt() + unscaledStartWidth, + startPosition.y.toInt() + unscaledStartHeight ) - finishCallback.onTransitionFinished(null /* wct */) - val tx: SurfaceControl.Transaction = transactionSupplier.get() - ValueAnimator.ofObject(rectEvaluator, unscaledStartBounds, endBounds) - .setDuration(DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS) - .apply { - addUpdateListener { animator -> - val animBounds = animator.animatedValue as Rect - val animFraction = animator.animatedFraction - // Progress scale from starting value to 1 as animation plays. - val animScale = startScale + animFraction * (1 - startScale) - tx.apply { - setScale(draggedTaskLeash, animScale, animScale) - setPosition( - draggedTaskLeash, - animBounds.left.toFloat(), - animBounds.top.toFloat() - ) - setWindowCrop(draggedTaskLeash, animBounds.width(), animBounds.height()) - } - onTaskResizeAnimationListener.onBoundsChange( - state.draggedTaskId, - tx, - animBounds + + dragToDesktopStateListener?.onCommitToDesktopAnimationStart(startTransaction) + // Accept the merge by applying the merging transaction (applied by #showResizeVeil) + // and finish callback. Show the veil and position the task at the first frame before + // starting the final animation. + onTaskResizeAnimationListener.onAnimationStart( + state.draggedTaskId, + startTransaction, + unscaledStartBounds + ) + val tx: SurfaceControl.Transaction = transactionSupplier.get() + ValueAnimator.ofObject(rectEvaluator, unscaledStartBounds, endBounds) + .setDuration(DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS) + .apply { + addUpdateListener { animator -> + val animBounds = animator.animatedValue as Rect + val animFraction = animator.animatedFraction + // Progress scale from starting value to 1 as animation plays. + val animScale = startScale + animFraction * (1 - startScale) + tx.apply { + setScale(draggedTaskLeash, animScale, animScale) + setPosition( + draggedTaskLeash, + animBounds.left.toFloat(), + animBounds.top.toFloat() ) + setWindowCrop(draggedTaskLeash, animBounds.width(), animBounds.height()) } - addListener( - object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - onTaskResizeAnimationListener.onAnimationEnd(state.draggedTaskId) - startTransitionFinishCb.onTransitionFinished(null /* null */) - clearState() - interactionJankMonitor.end( - CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE - ) - } - } + onTaskResizeAnimationListener.onBoundsChange( + state.draggedTaskId, + tx, + animBounds ) - start() } - } else if (isCancelTransition) { - info.changes.forEach { change -> - t.show(change.leash) - startTransactionFinishT.show(change.leash) + addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + onTaskResizeAnimationListener.onAnimationEnd(state.draggedTaskId) + startTransitionFinishCb.onTransitionFinished(/* wct = */ null) + clearState() + interactionJankMonitor.end( + CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE + ) + } + } + ) + start() } - t.apply() - finishCallback.onTransitionFinished(null /* wct */) - startTransitionFinishCb.onTransitionFinished(null /* wct */) - clearState() - } } override fun handleRequest( @@ -707,11 +727,12 @@ class DragToDesktopTransitionHandler( wct.reorder(wc, true /* toTop */) } } - val homeWc = state.homeToken ?: error("Home task should be non-null before cancelling") + val homeWc = + state.homeChange?.container ?: error("Home task should be non-null before cancelling") wct.restoreTransientOrder(homeWc) } - private fun clearState() { + protected fun clearState() { transitionState = null } @@ -731,10 +752,21 @@ class DragToDesktopTransitionHandler( return splitScreenController.getTaskInfo(otherTaskPos)?.taskId } - private fun requireTransitionState(): TransitionState { + protected fun requireTransitionState(): TransitionState { return transitionState ?: error("Expected non-null transition state") } + /** + * Represents the layering (Z order) that will be given to any window based on its type during + * the "start" transition of the drag-to-desktop transition + */ + protected data class DragToDesktopLayers( + val appLayers: Int, + val homeLayers: Int, + val wallpaperLayers: Int, + val dragLayer: Int, + ) + interface DragToDesktopStateListener { fun onCommitToDesktopAnimationStart(tx: SurfaceControl.Transaction) @@ -748,8 +780,9 @@ class DragToDesktopTransitionHandler( abstract var startTransitionFinishCb: Transitions.TransitionFinishCallback? abstract var startTransitionFinishTransaction: SurfaceControl.Transaction? abstract var cancelTransitionToken: IBinder? - abstract var homeToken: WindowContainerToken? + abstract var homeChange: Change? abstract var draggedTaskChange: Change? + abstract var freeformTaskChanges: List<Change> abstract var cancelState: CancelState abstract var startAborted: Boolean @@ -760,8 +793,9 @@ class DragToDesktopTransitionHandler( override var startTransitionFinishCb: Transitions.TransitionFinishCallback? = null, override var startTransitionFinishTransaction: SurfaceControl.Transaction? = null, override var cancelTransitionToken: IBinder? = null, - override var homeToken: WindowContainerToken? = null, + override var homeChange: Change? = null, override var draggedTaskChange: Change? = null, + override var freeformTaskChanges: List<Change> = emptyList(), override var cancelState: CancelState = CancelState.NO_CANCEL, override var startAborted: Boolean = false, var otherRootChanges: MutableList<Change> = mutableListOf() @@ -774,8 +808,9 @@ class DragToDesktopTransitionHandler( override var startTransitionFinishCb: Transitions.TransitionFinishCallback? = null, override var startTransitionFinishTransaction: SurfaceControl.Transaction? = null, override var cancelTransitionToken: IBinder? = null, - override var homeToken: WindowContainerToken? = null, + override var homeChange: Change? = null, override var draggedTaskChange: Change? = null, + override var freeformTaskChanges: List<Change> = emptyList(), override var cancelState: CancelState = CancelState.NO_CANCEL, override var startAborted: Boolean = false, var splitRootChange: Change? = null, @@ -797,6 +832,210 @@ class DragToDesktopTransitionHandler( companion object { /** The duration of the animation to commit or cancel the drag-to-desktop gesture. */ - private const val DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS = 336L + internal const val DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS = 336L + } +} + +/** Enables flagged rollout of the [SpringDragToDesktopTransitionHandler] */ +class DefaultDragToDesktopTransitionHandler +@JvmOverloads +constructor( + context: Context, + transitions: Transitions, + taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, + interactionJankMonitor: InteractionJankMonitor, + transactionSupplier: Supplier<SurfaceControl.Transaction> = Supplier { + SurfaceControl.Transaction() + }, +) : + DragToDesktopTransitionHandler( + context, + transitions, + taskDisplayAreaOrganizer, + interactionJankMonitor, + transactionSupplier + ) { + + /** + * @return layers in order: + * - appLayers - non-wallpaper, non-home tasks excluding the dragged task go at the bottom + * - homeLayers - home task on top of apps + * - wallpaperLayers - wallpaper on top of home + * - dragLayer - the dragged task on top of everything, there's only 1 dragged task + */ + override fun calculateStartDragToDesktopLayers(info: TransitionInfo): DragToDesktopLayers = + DragToDesktopLayers( + appLayers = info.changes.size, + homeLayers = info.changes.size * 2, + wallpaperLayers = info.changes.size * 3, + dragLayer = info.changes.size * 3 + ) +} + +/** Desktop transition handler with spring based animation for the end drag to desktop transition */ +class SpringDragToDesktopTransitionHandler +@JvmOverloads +constructor( + context: Context, + transitions: Transitions, + taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, + interactionJankMonitor: InteractionJankMonitor, + transactionSupplier: Supplier<SurfaceControl.Transaction> = Supplier { + SurfaceControl.Transaction() + }, +) : + DragToDesktopTransitionHandler( + context, + transitions, + taskDisplayAreaOrganizer, + interactionJankMonitor, + transactionSupplier + ) { + + private val positionSpringConfig = + PhysicsAnimator.SpringConfig( + SpringForce.STIFFNESS_LOW, + SpringForce.DAMPING_RATIO_LOW_BOUNCY + ) + + private val sizeSpringConfig = + PhysicsAnimator.SpringConfig(SpringForce.STIFFNESS_LOW, SpringForce.DAMPING_RATIO_NO_BOUNCY) + + /** + * @return layers in order: + * - appLayers - below everything z < 0, effectively hides the leash + * - homeLayers - home task on top of apps, z in 0..<size + * - wallpaperLayers - wallpaper on top of home, z in size..<size*2 + * - dragLayer - the dragged task on top of everything, z == size*2 + */ + override fun calculateStartDragToDesktopLayers(info: TransitionInfo): DragToDesktopLayers = + DragToDesktopLayers( + appLayers = -1, + homeLayers = info.changes.size - 1, + wallpaperLayers = info.changes.size * 2 - 1, + dragLayer = info.changes.size * 2 + ) + + override fun setupEndDragToDesktop( + info: TransitionInfo, + startTransaction: SurfaceControl.Transaction, + finishTransaction: SurfaceControl.Transaction + ) { + super.setupEndDragToDesktop(info, startTransaction, finishTransaction) + + val state = requireTransitionState() + val homeLeash = state.homeChange?.leash ?: error("Expects home leash to be non-null") + // Hide home on finish to prevent flickering when wallpaper activity flag is enabled + finishTransaction.hide(homeLeash) + // Setup freeform tasks before animation + state.freeformTaskChanges.forEach { change -> + val startScale = DRAG_TO_DESKTOP_FREEFORM_TASK_INITIAL_SCALE + val startX = + change.endAbsBounds.left + change.endAbsBounds.width() * (1 - startScale) / 2 + val startY = + change.endAbsBounds.top + change.endAbsBounds.height() * (1 - startScale) / 2 + startTransaction.setPosition(change.leash, startX, startY) + startTransaction.setScale(change.leash, startScale, startScale) + startTransaction.setAlpha(change.leash, 0f) + } + } + + override fun animateEndDragToDesktop( + startTransaction: SurfaceControl.Transaction, + startTransitionFinishCb: Transitions.TransitionFinishCallback + ) { + val state = requireTransitionState() + val draggedTaskChange = + state.draggedTaskChange ?: error("Expected non-null change of dragged task") + val draggedTaskLeash = draggedTaskChange.leash + val freeformTaskChanges = state.freeformTaskChanges + val startBounds = draggedTaskChange.startAbsBounds + val endBounds = draggedTaskChange.endAbsBounds + val currentVelocity = state.dragAnimator.computeCurrentVelocity() + + // Cancel any animation that may be currently playing; we will use the relevant + // details of that animation here. + state.dragAnimator.cancelAnimator() + // We still apply scale to task bounds; as we animate the bounds to their + // end value, animate scale to 1. + val startScale = state.dragAnimator.scale + val startPosition = state.dragAnimator.position + val startBoundsWithOffset = + Rect(startBounds).apply { offset(startPosition.x.toInt(), startPosition.y.toInt()) } + + dragToDesktopStateListener?.onCommitToDesktopAnimationStart(startTransaction) + // Accept the merge by applying the merging transaction (applied by #showResizeVeil) + // and finish callback. Show the veil and position the task at the first frame before + // starting the final animation. + onTaskResizeAnimationListener.onAnimationStart( + state.draggedTaskId, + startTransaction, + startBoundsWithOffset + ) + + val tx: SurfaceControl.Transaction = transactionSupplier.get() + PhysicsAnimator.getInstance(startBoundsWithOffset) + .spring( + FloatProperties.RECT_X, + endBounds.left.toFloat(), + currentVelocity.x, + positionSpringConfig + ) + .spring( + FloatProperties.RECT_Y, + endBounds.top.toFloat(), + currentVelocity.y, + positionSpringConfig + ) + .spring(FloatProperties.RECT_WIDTH, endBounds.width().toFloat(), sizeSpringConfig) + .spring(FloatProperties.RECT_HEIGHT, endBounds.height().toFloat(), sizeSpringConfig) + .addUpdateListener { animBounds, _ -> + val animFraction = + (animBounds.width() - startBounds.width()).toFloat() / + (endBounds.width() - startBounds.width()) + val animScale = startScale + animFraction * (1 - startScale) + // Freeform animation starts 50% in the animation + val freeformAnimFraction = max(animFraction - 0.5f, 0f) * 2f + val freeformStartScale = DRAG_TO_DESKTOP_FREEFORM_TASK_INITIAL_SCALE + val freeformAnimScale = + freeformStartScale + freeformAnimFraction * (1 - freeformStartScale) + tx.apply { + // Update dragged task + setScale(draggedTaskLeash, animScale, animScale) + setPosition( + draggedTaskLeash, + animBounds.left.toFloat(), + animBounds.top.toFloat() + ) + // Update freeform tasks + freeformTaskChanges.forEach { + val startX = + it.endAbsBounds.left + + it.endAbsBounds.width() * (1 - freeformAnimScale) / 2 + val startY = + it.endAbsBounds.top + + it.endAbsBounds.height() * (1 - freeformAnimScale) / 2 + setPosition(it.leash, startX, startY) + setScale(it.leash, freeformAnimScale, freeformAnimScale) + setAlpha(it.leash, freeformAnimFraction) + } + } + onTaskResizeAnimationListener.onBoundsChange(state.draggedTaskId, tx, animBounds) + } + .withEndActions({ + onTaskResizeAnimationListener.onAnimationEnd(state.draggedTaskId) + startTransitionFinishCb.onTransitionFinished(/* wct = */ null) + clearState() + interactionJankMonitor.end(CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE) + }) + .start() + } + + companion object { + /** + * The initial scale of the freeform tasks in the animation to commit the drag-to-desktop + * gesture. + */ + private const val DRAG_TO_DESKTOP_FREEFORM_TASK_INITIAL_SCALE = 0.9f } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt new file mode 100644 index 000000000000..bf4a2abf9edc --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt @@ -0,0 +1,81 @@ +/* + * 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.desktopmode.education.data + +import android.content.Context +import android.util.Log +import androidx.datastore.core.CorruptionException +import androidx.datastore.core.DataStore +import androidx.datastore.core.DataStoreFactory +import androidx.datastore.core.Serializer +import androidx.datastore.dataStore +import androidx.datastore.dataStoreFile +import com.android.framework.protobuf.InvalidProtocolBufferException +import com.android.internal.annotations.VisibleForTesting +import java.io.InputStream +import java.io.OutputStream +import kotlinx.coroutines.flow.first + +/** + * Manages interactions with the App Handle education datastore. + * + * This class provides a layer of abstraction between the UI/business logic and the underlying + * DataStore. + */ +class AppHandleEducationDatastoreRepository +@VisibleForTesting +constructor(private val dataStore: DataStore<WindowingEducationProto>) { + constructor( + context: Context + ) : this( + DataStoreFactory.create( + serializer = WindowingEducationProtoSerializer, + produceFile = { context.dataStoreFile(APP_HANDLE_EDUCATION_DATASTORE_FILEPATH) })) + + /** + * Reads and returns the [WindowingEducationProto] Proto object from the DataStore. If the + * DataStore is empty or there's an error reading, it returns the default value of Proto. + */ + suspend fun windowingEducationProto(): WindowingEducationProto = + try { + dataStore.data.first() + } catch (e: Exception) { + Log.e(TAG, "Unable to read from datastore") + WindowingEducationProto.getDefaultInstance() + } + + companion object { + private const val TAG = "AppHandleEducationDatastoreRepository" + private const val APP_HANDLE_EDUCATION_DATASTORE_FILEPATH = "app_handle_education.pb" + + object WindowingEducationProtoSerializer : Serializer<WindowingEducationProto> { + + override val defaultValue: WindowingEducationProto = + WindowingEducationProto.getDefaultInstance() + + override suspend fun readFrom(input: InputStream): WindowingEducationProto = + try { + WindowingEducationProto.parseFrom(input) + } catch (exception: InvalidProtocolBufferException) { + throw CorruptionException("Cannot read proto.", exception) + } + + override suspend fun writeTo(windowingProto: WindowingEducationProto, output: OutputStream) = + windowingProto.writeTo(output) + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/proto/windowing_education.proto b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/proto/windowing_education.proto new file mode 100644 index 000000000000..d29ec53d9c61 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/proto/windowing_education.proto @@ -0,0 +1,39 @@ +/* + * 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. + */ + +syntax = "proto2"; + +option java_package = "com.android.wm.shell.desktopmode.education.data"; +option java_multiple_files = true; + +// Desktop Windowing education data +message WindowingEducationProto { + // Timestamp in milliseconds of when the education was last viewed. + optional int64 education_viewed_timestamp_millis = 1; + // Timestamp in milliseconds of when the feature was last used. + optional int64 feature_used_timestamp_millis = 2; + oneof education_data { + // Fields specific to app handle education + AppHandleEducation app_handle_education = 3; + } + + message AppHandleEducation { + // Map that stores app launch count for corresponding package + map<string, int32> app_usage_stats = 1; + // Timestamp of when app_usage_stats was last cached + optional int64 app_usage_stats_last_update_timestamp_millis = 2; + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java index 723a53128bd0..428cc9118900 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java @@ -693,16 +693,6 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, return; } - if (mSplitScreenOptional.isPresent()) { - // If pip activity will reparent to origin task case and if the origin task still - // under split root, apply exit split transaction to make it expand to fullscreen. - SplitScreenController split = mSplitScreenOptional.get(); - if (split.isTaskInSplitScreen(mTaskInfo.lastParentTaskIdBeforePip)) { - split.prepareExitSplitScreen(wct, split.getStageOfTask( - mTaskInfo.lastParentTaskIdBeforePip), - SplitScreenController.EXIT_REASON_APP_FINISHED); - } - } mPipTransitionController.startExitTransition(TRANSIT_EXIT_PIP, wct, destinationBounds); return; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java index 284620e7d0c4..da6221efdaee 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java @@ -632,6 +632,12 @@ public class PipController implements PipTransitionController.PipTransitionCallb public void insetsChanged(InsetsState insetsState) { DisplayLayout pendingLayout = mDisplayController .getDisplayLayout(mPipDisplayLayoutState.getDisplayId()); + if (pendingLayout == null) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "insetsChanged: no display layout for displayId=%d", + mPipDisplayLayoutState.getDisplayId()); + return; + } if (mIsInFixedRotation || mIsKeyguardShowingOrAnimating || pendingLayout.rotation() 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/pip2/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java index 77743844f3c3..dc21f82c326c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java @@ -31,6 +31,8 @@ import android.graphics.Rect; import android.os.Bundle; import android.view.InsetsState; import android.view.SurfaceControl; +import android.window.DisplayAreaInfo; +import android.window.WindowContainerTransaction; import androidx.annotation.BinderThread; import androidx.annotation.Nullable; @@ -40,6 +42,7 @@ import com.android.internal.protolog.ProtoLog; import com.android.internal.util.Preconditions; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.DisplayChangeController; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.DisplayLayout; @@ -71,7 +74,8 @@ import java.util.function.Consumer; */ public class PipController implements ConfigurationChangeListener, PipTransitionState.PipTransitionStateChangedListener, - DisplayController.OnDisplaysChangedListener, RemoteCallable<PipController> { + DisplayController.OnDisplaysChangedListener, + DisplayChangeController.OnDisplayChangingListener, RemoteCallable<PipController> { private static final String TAG = PipController.class.getSimpleName(); private static final String SWIPE_TO_PIP_APP_BOUNDS = "pip_app_bounds"; private static final String SWIPE_TO_PIP_OVERLAY = "swipe_to_pip_overlay"; @@ -197,11 +201,12 @@ public class PipController implements ConfigurationChangeListener, mPipDisplayLayoutState.setDisplayLayout(layout); mDisplayController.addDisplayWindowListener(this); + mDisplayController.addDisplayChangingController(this); mDisplayInsetsController.addInsetsChangedListener(mPipDisplayLayoutState.getDisplayId(), new DisplayInsetsController.OnInsetsChangedListener() { @Override public void insetsChanged(InsetsState insetsState) { - onDisplayChanged(mDisplayController + setDisplayLayout(mDisplayController .getDisplayLayout(mPipDisplayLayoutState.getDisplayId())); } }); @@ -264,11 +269,12 @@ public class PipController implements ConfigurationChangeListener, @Override public void onThemeChanged() { - onDisplayChanged(new DisplayLayout(mContext, mContext.getDisplay())); + setDisplayLayout(new DisplayLayout(mContext, mContext.getDisplay())); } // - // DisplayController.OnDisplaysChangedListener implementations + // DisplayController.OnDisplaysChangedListener and + // DisplayChangeController.OnDisplayChangingListener implementations // @Override @@ -276,7 +282,7 @@ public class PipController implements ConfigurationChangeListener, if (displayId != mPipDisplayLayoutState.getDisplayId()) { return; } - onDisplayChanged(mDisplayController.getDisplayLayout(displayId)); + setDisplayLayout(mDisplayController.getDisplayLayout(displayId)); } @Override @@ -284,10 +290,35 @@ public class PipController implements ConfigurationChangeListener, if (displayId != mPipDisplayLayoutState.getDisplayId()) { return; } - onDisplayChanged(mDisplayController.getDisplayLayout(displayId)); + setDisplayLayout(mDisplayController.getDisplayLayout(displayId)); } - private void onDisplayChanged(DisplayLayout layout) { + /** + * A callback for any observed transition that contains a display change in its + * {@link android.window.TransitionRequestInfo} with a non-zero rotation delta. + */ + @Override + public void onDisplayChange(int displayId, int fromRotation, int toRotation, + @Nullable DisplayAreaInfo newDisplayAreaInfo, WindowContainerTransaction t) { + if (!mPipTransitionState.isInPip()) { + return; + } + + // Calculate the snap fraction pre-rotation. + float snapFraction = mPipBoundsAlgorithm.getSnapFraction(mPipBoundsState.getBounds()); + + // Update the caches to reflect the new display layout and movement bounds. + mPipDisplayLayoutState.rotateTo(toRotation); + mPipTouchHandler.updateMovementBounds(); + + // The policy is to keep PiP width, height and snap fraction invariant. + Rect toBounds = mPipBoundsState.getBounds(); + mPipBoundsAlgorithm.applySnapFraction(toBounds, snapFraction); + mPipBoundsState.setBounds(toBounds); + t.setBounds(mPipTransitionState.mPipTaskToken, toBounds); + } + + private void setDisplayLayout(DisplayLayout layout) { mPipDisplayLayoutState.setDisplayLayout(layout); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java index d7c225b9e6e1..d75fa00b1fdd 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java @@ -1081,7 +1081,7 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha * Updates the current movement bounds based on whether the menu is currently visible and * resized. */ - private void updateMovementBounds() { + void updateMovementBounds() { Rect insetBounds = new Rect(); mPipBoundsAlgorithm.getInsetBounds(insetBounds); mPipBoundsAlgorithm.getMovementBounds(mPipBoundsState.getBounds(), 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/SplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java index 48d17ec6963f..c11a112cde60 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java @@ -439,9 +439,9 @@ class SplitScreenTransitions { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "setResizeTransition: hasPendingResize=%b", mPendingResize != null); if (mPendingResize != null) { + mPendingResize.cancel(null); mainDecor.cancelRunningAnimations(); sideDecor.cancelRunningAnimations(); - mPendingResize.cancel(null); mAnimations.clear(); onFinish(null /* wct */); } 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 a7551bddc42d..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 @@ -123,10 +123,10 @@ import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayImeController; import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.LaunchAdjacentController; -import com.android.wm.shell.common.ScreenshotUtils; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.common.split.SplitDecorManager; import com.android.wm.shell.common.split.SplitLayout; import com.android.wm.shell.common.split.SplitScreenConstants.PersistentSnapPosition; import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition; @@ -1010,40 +1010,41 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mTempRect1.setEmpty(); final StageTaskListener topLeftStage = mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage; - final SurfaceControl topLeftScreenshot = ScreenshotUtils.takeScreenshot(t, - topLeftStage.mRootLeash, mTempRect1, Integer.MAX_VALUE - 1); final StageTaskListener bottomRightStage = mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mMainStage : mSideStage; - final SurfaceControl bottomRightScreenshot = ScreenshotUtils.takeScreenshot(t, - bottomRightStage.mRootLeash, mTempRect1, Integer.MAX_VALUE - 1); - mSplitLayout.splitSwitching(t, topLeftStage.mRootLeash, bottomRightStage.mRootLeash, + + // Don't allow windows or divider to be focused during animation (mRootTaskInfo is the + // parent of all 3 leaves). We don't want the user to be able to tap and focus a window + // while it is moving across the screen, because granting focus also recalculates the + // layering order, which is in delicate balance during this animation. + WindowContainerTransaction noFocus = new WindowContainerTransaction(); + noFocus.setFocusable(mRootTaskInfo.token, false); + mSyncQueue.queue(noFocus); + + mSplitLayout.playSwapAnimation(t, topLeftStage, bottomRightStage, insets -> { + // Runs at the end of the swap animation + SplitDecorManager decorManager1 = topLeftStage.getDecorManager(); + SplitDecorManager decorManager2 = bottomRightStage.getDecorManager(); + WindowContainerTransaction wct = new WindowContainerTransaction(); + + // Restore focus-ability to the windows and divider + wct.setFocusable(mRootTaskInfo.token, true); + setSideStagePosition(reverseSplitPosition(mSideStagePosition), wct); mSyncQueue.queue(wct); mSyncQueue.runInSync(st -> { updateSurfaceBounds(mSplitLayout, st, false /* applyResizingOffset */); - st.setPosition(topLeftScreenshot, -insets.left, -insets.top); - st.setPosition(bottomRightScreenshot, insets.left, insets.top); - - final ValueAnimator va = ValueAnimator.ofFloat(1, 0); - va.addUpdateListener(valueAnimator-> { - final float progress = (float) valueAnimator.getAnimatedValue(); - t.setAlpha(topLeftScreenshot, progress); - t.setAlpha(bottomRightScreenshot, progress); - t.apply(); - }); - va.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd( - @androidx.annotation.NonNull Animator animation) { - t.remove(topLeftScreenshot); - t.remove(bottomRightScreenshot); - t.apply(); - mTransactionPool.release(t); - } - }); - va.start(); + + // updateSurfaceBounds(), above, officially puts the two apps in their new + // stages. Starting on the next frame, all calculations are made using the + // new layouts/insets. So any follow-up animations on the same leashes below + // should contain some cleanup/repositioning to prevent jank. + + // Play follow-up animations if needed + decorManager1.fadeOutVeilAndCleanUp(st); + decorManager2.fadeOutVeilAndCleanUp(st); }); }); @@ -2242,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. @@ -2257,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 @@ -2330,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. @@ -2347,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/splitscreen/StageTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java index d1ab3e96d4c2..f19eb3f8291e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java @@ -69,7 +69,7 @@ import java.util.function.Predicate; * * @see StageCoordinator */ -class StageTaskListener implements ShellTaskOrganizer.TaskListener { +public class StageTaskListener implements ShellTaskOrganizer.TaskListener { private static final String TAG = StageTaskListener.class.getSimpleName(); /** Callback interface for listening to changes in a split-screen stage. */ @@ -162,6 +162,18 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { return getChildTaskInfo(predicate) != null; } + public SurfaceControl getRootLeash() { + return mRootLeash; + } + + public ActivityManager.RunningTaskInfo getRunningTaskInfo() { + return mRootTaskInfo; + } + + public SplitDecorManager getDecorManager() { + return mSplitDecorManager; + } + @Nullable private ActivityManager.RunningTaskInfo getChildTaskInfo( Predicate<ActivityManager.RunningTaskInfo> predicate) { @@ -335,7 +347,7 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { void fadeOutDecor(Runnable finishedCallback) { if (mSplitDecorManager != null) { - mSplitDecorManager.fadeOutDecor(finishedCallback); + mSplitDecorManager.fadeOutDecor(finishedCallback, false /* addDelay */); } else { finishedCallback.run(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSplashWindowCreator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSplashWindowCreator.java index f3725579bf48..1a38449fa447 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSplashWindowCreator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSplashWindowCreator.java @@ -16,7 +16,6 @@ package com.android.wm.shell.startingsurface; -import static android.graphics.Color.WHITE; import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SPLASH_SCREEN; import android.app.ActivityManager; @@ -69,8 +68,9 @@ class WindowlessSplashWindowCreator extends AbsSplashWindowCreator { // Can't show splash screen on requested display, so skip showing at all. return; } + final int theme = getSplashScreenTheme(0 /* splashScreenThemeResId */, activityInfo); final Context myContext = SplashscreenContentDrawer.createContext(mContext, windowInfo, - 0 /* theme */, STARTING_WINDOW_TYPE_SPLASH_SCREEN, mDisplayManager); + theme, STARTING_WINDOW_TYPE_SPLASH_SCREEN, mDisplayManager); if (myContext == null) { return; } @@ -86,19 +86,11 @@ class WindowlessSplashWindowCreator extends AbsSplashWindowCreator { final Rect windowBounds = taskInfo.configuration.windowConfiguration.getBounds(); lp.width = windowBounds.width(); lp.height = windowBounds.height(); - final ActivityManager.TaskDescription taskDescription; - if (taskInfo.taskDescription != null) { - taskDescription = taskInfo.taskDescription; - } else { - taskDescription = new ActivityManager.TaskDescription(); - taskDescription.setBackgroundColor(WHITE); - } final FrameLayout rootLayout = new FrameLayout( mSplashscreenContentDrawer.createViewContextWrapper(myContext)); viewHost.setView(rootLayout, lp); - - final int bgColor = taskDescription.getBackgroundColor(); + final int bgColor = mSplashscreenContentDrawer.estimateTaskBackgroundColor(myContext); final SplashScreenView splashScreenView = mSplashscreenContentDrawer .makeSimpleSplashScreenContentView(myContext, windowInfo, bgColor); rootLayout.addView(splashScreenView); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java index 778478405dda..de6887a2173b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java @@ -502,15 +502,19 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { backgroundColorForTransition = getTransitionBackgroundColorIfSet(info, change, a, backgroundColorForTransition); - if (!isTask && a.hasExtension()) { - if (!TransitionUtil.isOpeningType(mode)) { - // Can screenshot now (before startTransaction is applied) - edgeExtendWindow(change, a, startTransaction, finishTransaction); + if (!isTask && a.getExtensionEdges() != 0x0) { + if (com.android.graphics.libgui.flags.Flags.edgeExtensionShader()) { + finishTransaction.setEdgeExtensionEffect(change.getLeash(), /* edge */ 0); } else { - // Need to screenshot after startTransaction is applied otherwise activity - // may not be visible or ready yet. - postStartTransactionCallbacks - .add(t -> edgeExtendWindow(change, a, t, finishTransaction)); + if (!TransitionUtil.isOpeningType(mode)) { + // Can screenshot now (before startTransaction is applied) + edgeExtendWindow(change, a, startTransaction, finishTransaction); + } else { + // Need to screenshot after startTransaction is applied otherwise + // activity may not be visible or ready yet. + postStartTransactionCallbacks + .add(t -> edgeExtendWindow(change, a, t, finishTransaction)); + } } } @@ -558,7 +562,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { buildSurfaceAnimation(animations, a, change.getLeash(), onAnimFinish, mTransactionPool, mMainExecutor, animRelOffset, cornerRadius, - clipRect); + clipRect, change.getActivityComponent() != null); final TransitionInfo.AnimationOptions options; if (Flags.moveAnimationOptionsToChange()) { @@ -823,7 +827,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { @NonNull Animation anim, @NonNull SurfaceControl leash, @NonNull Runnable finishCallback, @NonNull TransactionPool pool, @NonNull ShellExecutor mainExecutor, @Nullable Point position, float cornerRadius, - @Nullable Rect clipRect) { + @Nullable Rect clipRect, boolean isActivity) { final SurfaceControl.Transaction transaction = pool.acquire(); final ValueAnimator va = ValueAnimator.ofFloat(0f, 1f); final Transformation transformation = new Transformation(); @@ -835,13 +839,13 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { final long currentPlayTime = Math.min(va.getDuration(), va.getCurrentPlayTime()); applyTransformation(currentPlayTime, transaction, leash, anim, transformation, matrix, - position, cornerRadius, clipRect); + position, cornerRadius, clipRect, isActivity); }; va.addUpdateListener(updateListener); final Runnable finisher = () -> { applyTransformation(va.getDuration(), transaction, leash, anim, transformation, matrix, - position, cornerRadius, clipRect); + position, cornerRadius, clipRect, isActivity); pool.release(transaction); mainExecutor.execute(() -> { @@ -931,7 +935,8 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { a.restrictDuration(MAX_ANIMATION_DURATION); a.scaleCurrentDuration(mTransitionAnimationScaleSetting); buildSurfaceAnimation(animations, a, wt.getSurface(), finisher, mTransactionPool, - mMainExecutor, change.getEndRelOffset(), cornerRadius, change.getEndAbsBounds()); + mMainExecutor, change.getEndRelOffset(), cornerRadius, change.getEndAbsBounds(), + change.getActivityComponent() != null); } private void attachThumbnailAnimation(@NonNull ArrayList<Animator> animations, @@ -955,7 +960,8 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { a.restrictDuration(MAX_ANIMATION_DURATION); a.scaleCurrentDuration(mTransitionAnimationScaleSetting); buildSurfaceAnimation(animations, a, wt.getSurface(), finisher, mTransactionPool, - mMainExecutor, change.getEndRelOffset(), cornerRadius, change.getEndAbsBounds()); + mMainExecutor, change.getEndRelOffset(), cornerRadius, change.getEndAbsBounds(), + change.getActivityComponent() != null); } private static int getWallpaperTransitType(TransitionInfo info) { @@ -1005,9 +1011,14 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { private static void applyTransformation(long time, SurfaceControl.Transaction t, SurfaceControl leash, Animation anim, Transformation tmpTransformation, float[] matrix, - Point position, float cornerRadius, @Nullable Rect immutableClipRect) { + Point position, float cornerRadius, @Nullable Rect immutableClipRect, + boolean isActivity) { tmpTransformation.clear(); anim.getTransformation(time, tmpTransformation); + if (com.android.graphics.libgui.flags.Flags.edgeExtensionShader() + && anim.getExtensionEdges() != 0x0 && isActivity) { + t.setEdgeExtensionEffect(leash, anim.getExtensionEdges()); + } if (position != null) { tmpTransformation.getMatrix().postTranslate(position.x, position.y); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java index e196254628d0..195882553602 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java @@ -325,21 +325,21 @@ class ScreenRotationAnimation { @NonNull Runnable finishCallback, @NonNull ShellExecutor mainExecutor) { buildSurfaceAnimation(animations, mRotateEnterAnimation, mSurfaceControl, finishCallback, mTransactionPool, mainExecutor, null /* position */, 0 /* cornerRadius */, - null /* clipRect */); + null /* clipRect */, false /* isActivity */); } private void startScreenshotRotationAnimation(@NonNull ArrayList<Animator> animations, @NonNull Runnable finishCallback, @NonNull ShellExecutor mainExecutor) { buildSurfaceAnimation(animations, mRotateExitAnimation, mAnimLeash, finishCallback, mTransactionPool, mainExecutor, null /* position */, 0 /* cornerRadius */, - null /* clipRect */); + null /* clipRect */, false /* isActivity */); } private void buildScreenshotAlphaAnimation(@NonNull ArrayList<Animator> animations, @NonNull Runnable finishCallback, @NonNull ShellExecutor mainExecutor) { buildSurfaceAnimation(animations, mRotateAlphaAnimation, mAnimLeash, finishCallback, mTransactionPool, mainExecutor, null /* position */, 0 /* cornerRadius */, - null /* clipRect */); + null /* clipRect */, false /* isActivity */); } private void startColorAnimation(float animationScale, @NonNull ShellExecutor animExecutor) { 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/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index a242b8a4fdd3..8c8f205ca353 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -69,7 +69,6 @@ import android.view.InputChannel; import android.view.InputEvent; import android.view.InputEventReceiver; import android.view.InputMonitor; -import android.view.InsetsSource; import android.view.InsetsState; import android.view.MotionEvent; import android.view.SurfaceControl; @@ -115,6 +114,7 @@ import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration.ExclusionRegionListener; +import com.android.wm.shell.windowdecor.extension.InsetsStateKt; import com.android.wm.shell.windowdecor.extension.TaskInfoKt; import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder; @@ -321,7 +321,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { private void onInit() { mShellController.addKeyguardChangeListener(mDesktopModeKeyguardChangeListener); mShellCommandHandler.addDumpCallback(this::dump, this); - mDisplayInsetsController.addInsetsChangedListener(mContext.getDisplayId(), + mDisplayInsetsController.addGlobalInsetsChangedListener( new DesktopModeOnInsetsChangedListener()); mDesktopTasksController.setOnTaskResizeAnimationListener( new DesktopModeOnTaskResizeAnimationListener()); @@ -1196,10 +1196,6 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { && mSplitScreenController.isTaskRootOrStageRoot(taskInfo.taskId)) { return false; } - if (mDesktopModeKeyguardChangeListener.isKeyguardVisibleAndOccluded() - && taskInfo.isFocused) { - return false; - } if (DesktopModeFlags.MODALS_POLICY.isEnabled(mContext) && isTopActivityExemptFromDesktopWindowing(mContext, taskInfo)) { return false; @@ -1397,19 +1393,17 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { } } - static class DesktopModeKeyguardChangeListener implements KeyguardChangeListener { - private boolean mIsKeyguardVisible; - private boolean mIsKeyguardOccluded; - + class DesktopModeKeyguardChangeListener implements KeyguardChangeListener { @Override public void onKeyguardVisibilityChanged(boolean visible, boolean occluded, boolean animatingDismiss) { - mIsKeyguardVisible = visible; - mIsKeyguardOccluded = occluded; - } - - public boolean isKeyguardVisibleAndOccluded() { - return mIsKeyguardVisible && mIsKeyguardOccluded; + final int size = mWindowDecorByTaskId.size(); + for (int i = size - 1; i >= 0; i--) { + final DesktopModeWindowDecoration decor = mWindowDecorByTaskId.valueAt(i); + if (decor != null) { + decor.onKeyguardStateChanged(visible, occluded); + } + } } } @@ -1417,28 +1411,26 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { class DesktopModeOnInsetsChangedListener implements DisplayInsetsController.OnInsetsChangedListener { @Override - public void insetsChanged(InsetsState insetsState) { - for (int i = 0; i < insetsState.sourceSize(); i++) { - final InsetsSource source = insetsState.sourceAt(i); - if (source.getType() != statusBars()) { + public void insetsChanged(int displayId, @NonNull InsetsState insetsState) { + final int size = mWindowDecorByTaskId.size(); + for (int i = size - 1; i >= 0; i--) { + final DesktopModeWindowDecoration decor = mWindowDecorByTaskId.valueAt(i); + if (decor == null) { continue; } - - final DesktopModeWindowDecoration decor = getFocusedDecor(); - if (decor == null) { - return; + if (decor.mTaskInfo.displayId == displayId + && Flags.enableDesktopWindowingImmersiveHandleHiding()) { + decor.onInsetsStateChanged(insetsState); } - // If status bar inset is visible, top task is not in immersive mode - final boolean inImmersiveMode = !source.isVisible(); - // Calls WindowDecoration#relayout if decoration visibility needs to be updated - if (inImmersiveMode != mInImmersiveMode) { - if (Flags.enableDesktopWindowingImmersiveHandleHiding()) { - decor.relayout(decor.mTaskInfo); - } - mInImmersiveMode = inImmersiveMode; + if (!Flags.enableAdditionalWindowsAboveStatusBar()) { + // If status bar inset is visible, top task is not in immersive mode. + // This value is only needed when the App Handle input is being handled + // through the global input monitor (hence the flag check) to ignore gestures + // when the app is in immersive mode. When disabled, the view itself handles + // input, and since it's removed when in immersive there's no need to track + // this here. + mInImmersiveMode = !InsetsStateKt.isVisible(insetsState, statusBars()); } - - return; } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt index 54b33e931830..095d33736595 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt @@ -60,6 +60,7 @@ import androidx.core.animation.addListener import com.android.wm.shell.R import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.animation.Interpolators.EMPHASIZED_DECELERATE +import com.android.wm.shell.animation.Interpolators.FAST_OUT_LINEAR_IN import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.SyncTransactionQueue import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewHostViewContainer @@ -120,8 +121,9 @@ class MaximizeMenu( /** Closes the maximize window and releases its view. */ fun close() { - maximizeMenuView?.cancelAnimation() - maximizeMenu?.releaseView() + maximizeMenuView?.animateCloseMenu { + maximizeMenu?.releaseView() + } maximizeMenu = null maximizeMenuView = null } @@ -255,7 +257,7 @@ class MaximizeMenu( .getDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_buttons_fill_radius) private val hoverTempRect = Rect() - private val openMenuAnimatorSet = AnimatorSet() + private var menuAnimatorSet: AnimatorSet? = null private lateinit var taskInfo: RunningTaskInfo private lateinit var style: MenuStyle @@ -346,15 +348,16 @@ class MaximizeMenu( fun animateOpenMenu() { maximizeButton.setLayerType(View.LAYER_TYPE_HARDWARE, null) maximizeText.setLayerType(View.LAYER_TYPE_HARDWARE, null) - openMenuAnimatorSet.playTogether( + menuAnimatorSet = AnimatorSet() + menuAnimatorSet?.playTogether( ObjectAnimator.ofFloat(rootView, SCALE_Y, STARTING_MENU_HEIGHT_SCALE, 1f) .apply { - duration = MENU_HEIGHT_ANIMATION_DURATION_MS + duration = OPEN_MENU_HEIGHT_ANIMATION_DURATION_MS interpolator = EMPHASIZED_DECELERATE }, ValueAnimator.ofFloat(STARTING_MENU_HEIGHT_SCALE, 1f) .apply { - duration = MENU_HEIGHT_ANIMATION_DURATION_MS + duration = OPEN_MENU_HEIGHT_ANIMATION_DURATION_MS interpolator = EMPHASIZED_DECELERATE addUpdateListener { // Animate padding so that controls stay pinned to the bottom of @@ -367,7 +370,7 @@ class MaximizeMenu( } }, ValueAnimator.ofFloat(1 / STARTING_MENU_HEIGHT_SCALE, 1f).apply { - duration = MENU_HEIGHT_ANIMATION_DURATION_MS + duration = OPEN_MENU_HEIGHT_ANIMATION_DURATION_MS interpolator = EMPHASIZED_DECELERATE addUpdateListener { // Scale up the children of the maximize menu so that the menu @@ -381,7 +384,7 @@ class MaximizeMenu( }, ObjectAnimator.ofFloat(rootView, TRANSLATION_Y, (STARTING_MENU_HEIGHT_SCALE - 1) * menuHeight, 0f).apply { - duration = MENU_HEIGHT_ANIMATION_DURATION_MS + duration = OPEN_MENU_HEIGHT_ANIMATION_DURATION_MS interpolator = EMPHASIZED_DECELERATE }, ObjectAnimator.ofInt(rootView.background, "alpha", @@ -391,7 +394,7 @@ class MaximizeMenu( ValueAnimator.ofFloat(0f, 1f) .apply { duration = ALPHA_ANIMATION_DURATION_MS - startDelay = CONTROLS_ALPHA_ANIMATION_DELAY_MS + startDelay = CONTROLS_ALPHA_OPEN_MENU_ANIMATION_DELAY_MS addUpdateListener { val value = animatedValue as Float maximizeButton.alpha = value @@ -403,21 +406,96 @@ class MaximizeMenu( ObjectAnimator.ofFloat(rootView, TRANSLATION_Z, MENU_Z_TRANSLATION) .apply { duration = ELEVATION_ANIMATION_DURATION_MS - startDelay = CONTROLS_ALPHA_ANIMATION_DELAY_MS + startDelay = CONTROLS_ALPHA_OPEN_MENU_ANIMATION_DELAY_MS } ) - openMenuAnimatorSet.addListener( + menuAnimatorSet?.addListener( onEnd = { maximizeButton.setLayerType(View.LAYER_TYPE_SOFTWARE, null) maximizeText.setLayerType(View.LAYER_TYPE_SOFTWARE, null) } ) - openMenuAnimatorSet.start() + menuAnimatorSet?.start() + } + + /** Animate the closing of the menu */ + fun animateCloseMenu(onEnd: (() -> Unit)) { + maximizeButton.setLayerType(View.LAYER_TYPE_HARDWARE, null) + maximizeText.setLayerType(View.LAYER_TYPE_HARDWARE, null) + cancelAnimation() + menuAnimatorSet = AnimatorSet() + menuAnimatorSet?.playTogether( + ObjectAnimator.ofFloat(rootView, SCALE_Y, 1f, STARTING_MENU_HEIGHT_SCALE) + .apply { + duration = CLOSE_MENU_HEIGHT_ANIMATION_DURATION_MS + interpolator = FAST_OUT_LINEAR_IN + }, + ValueAnimator.ofFloat(1f, STARTING_MENU_HEIGHT_SCALE) + .apply { + duration = CLOSE_MENU_HEIGHT_ANIMATION_DURATION_MS + interpolator = FAST_OUT_LINEAR_IN + addUpdateListener { + // Animate padding so that controls stay pinned to the bottom of + // the menu. + val value = animatedValue as Float + val topPadding = menuPadding - + ((1 - value) * menuHeight).toInt() + container.setPadding(menuPadding, topPadding, + menuPadding, menuPadding) + } + }, + ValueAnimator.ofFloat(1f, 1 / STARTING_MENU_HEIGHT_SCALE).apply { + duration = CLOSE_MENU_HEIGHT_ANIMATION_DURATION_MS + interpolator = FAST_OUT_LINEAR_IN + addUpdateListener { + // Scale up the children of the maximize menu so that the menu + // scale is cancelled out and only the background is scaled. + val value = animatedValue as Float + maximizeButton.scaleY = value + snapButtonsLayout.scaleY = value + maximizeText.scaleY = value + snapWindowText.scaleY = value + } + }, + ObjectAnimator.ofFloat(rootView, TRANSLATION_Y, + 0f, (STARTING_MENU_HEIGHT_SCALE - 1) * menuHeight).apply { + duration = CLOSE_MENU_HEIGHT_ANIMATION_DURATION_MS + interpolator = FAST_OUT_LINEAR_IN + }, + ObjectAnimator.ofInt(rootView.background, "alpha", + MAX_DRAWABLE_ALPHA_VALUE, 0).apply { + startDelay = CONTAINER_ALPHA_CLOSE_MENU_ANIMATION_DELAY_MS + duration = ALPHA_ANIMATION_DURATION_MS + }, + ValueAnimator.ofFloat(1f, 0f) + .apply { + duration = ALPHA_ANIMATION_DURATION_MS + addUpdateListener { + val value = animatedValue as Float + maximizeButton.alpha = value + snapButtonsLayout.alpha = value + maximizeText.alpha = value + snapWindowText.alpha = value + } + }, + ObjectAnimator.ofFloat(rootView, TRANSLATION_Z, MENU_Z_TRANSLATION, 0f) + .apply { + duration = ELEVATION_ANIMATION_DURATION_MS + } + ) + menuAnimatorSet?.addListener( + onEnd = { + maximizeButton.setLayerType(View.LAYER_TYPE_SOFTWARE, null) + maximizeText.setLayerType(View.LAYER_TYPE_SOFTWARE, null) + onEnd?.invoke() + } + ) + menuAnimatorSet?.start() } - /** Cancel the open menu animation. */ - fun cancelAnimation() { - openMenuAnimatorSet.cancel() + /** Cancel the menu animation. */ + private fun cancelAnimation() { + menuAnimatorSet?.cancel() } /** Update the view state to a new snap to half selection. */ @@ -645,9 +723,11 @@ class MaximizeMenu( private const val ALPHA_ANIMATION_DURATION_MS = 50L private const val MAX_DRAWABLE_ALPHA_VALUE = 255 private const val STARTING_MENU_HEIGHT_SCALE = 0.8f - private const val MENU_HEIGHT_ANIMATION_DURATION_MS = 300L + private const val OPEN_MENU_HEIGHT_ANIMATION_DURATION_MS = 300L + private const val CLOSE_MENU_HEIGHT_ANIMATION_DURATION_MS = 200L private const val ELEVATION_ANIMATION_DURATION_MS = 50L - private const val CONTROLS_ALPHA_ANIMATION_DELAY_MS = 33L + private const val CONTROLS_ALPHA_OPEN_MENU_ANIMATION_DELAY_MS = 33L + private const val CONTAINER_ALPHA_CLOSE_MENU_ANIMATION_DELAY_MS = 33L private const val MENU_Z_TRANSLATION = 1f } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MoveToDesktopAnimator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MoveToDesktopAnimator.kt index 974166700203..70c0b54462e3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MoveToDesktopAnimator.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MoveToDesktopAnimator.kt @@ -7,6 +7,7 @@ import android.graphics.PointF import android.graphics.Rect import android.view.MotionEvent import android.view.SurfaceControl +import android.view.VelocityTracker import com.android.wm.shell.R /** @@ -34,6 +35,7 @@ class MoveToDesktopAnimator @JvmOverloads constructor( val scale: Float get() = dragToDesktopAnimator.animatedValue as Float private val mostRecentInput = PointF() + private val velocityTracker = VelocityTracker.obtain() private val dragToDesktopAnimator: ValueAnimator = ValueAnimator.ofFloat(1f, DRAG_FREEFORM_SCALE) .setDuration(ANIMATION_DURATION.toLong()) @@ -90,6 +92,7 @@ class MoveToDesktopAnimator @JvmOverloads constructor( if (!allowSurfaceChangesOnMove || dragToDesktopAnimator.isRunning) { return } + velocityTracker.addMovement(ev) setTaskPosition(ev.rawX, ev.rawY) val t = transactionFactory() t.setPosition(taskSurface, position.x, position.y) @@ -109,6 +112,15 @@ class MoveToDesktopAnimator @JvmOverloads constructor( * Cancels the animation, intended to be used when another animator will take over. */ fun cancelAnimator() { + velocityTracker.clear() dragToDesktopAnimator.cancel() } + + /** + * Computes the current velocity per second based on the points that have been collected. + */ + fun computeCurrentVelocity(): PointF { + velocityTracker.computeCurrentVelocity(/* units = */ 1000) + return PointF(velocityTracker.xVelocity, velocityTracker.yVelocity) + } }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java index 0c5898710983..4af5b2c95cd5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java @@ -61,6 +61,7 @@ import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; import com.android.wm.shell.windowdecor.WindowDecoration.RelayoutParams.OccludingCaptionElement; import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewHostViewContainer; +import com.android.wm.shell.windowdecor.extension.InsetsStateKt; import java.util.ArrayList; import java.util.Arrays; @@ -143,6 +144,9 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> TaskDragResizer mTaskDragResizer; boolean mIsCaptionVisible; + private boolean mIsStatusBarVisible; + private boolean mIsKeyguardVisibleAndOccluded; + /** The most recent set of insets applied to this window decoration. */ private WindowDecorationInsets mWindowDecorationInsets; private final Binder mOwner = new Binder(); @@ -184,6 +188,9 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> mWindowContainerTransactionSupplier = windowContainerTransactionSupplier; mSurfaceControlViewHostFactory = surfaceControlViewHostFactory; mDisplay = mDisplayController.getDisplay(mTaskInfo.displayId); + final InsetsState insetsState = mDisplayController.getInsetsState(mTaskInfo.displayId); + mIsStatusBarVisible = insetsState != null + && InsetsStateKt.isVisible(insetsState, statusBars()); } /** @@ -234,7 +241,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> } rootView = null; // Clear it just in case we use it accidentally - updateCaptionVisibility(outResult.mRootView, mTaskInfo.displayId); + updateCaptionVisibility(outResult.mRootView); final Rect taskBounds = mTaskInfo.getConfiguration().windowConfiguration.getBounds(); outResult.mWidth = taskBounds.width(); @@ -284,17 +291,20 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> mDecorWindowContext = mContext.createConfigurationContext(mWindowDecorConfig); mDecorWindowContext.setTheme(mContext.getThemeResId()); if (params.mLayoutResId != 0) { - outResult.mRootView = (T) LayoutInflater.from(mDecorWindowContext) - .inflate(params.mLayoutResId, null); + outResult.mRootView = inflateLayout(mDecorWindowContext, params.mLayoutResId); } } if (outResult.mRootView == null) { - outResult.mRootView = (T) LayoutInflater.from(mDecorWindowContext) - .inflate(params.mLayoutResId, null); + outResult.mRootView = inflateLayout(mDecorWindowContext, params.mLayoutResId); } } + @VisibleForTesting + T inflateLayout(Context context, int layoutResId) { + return (T) LayoutInflater.from(context).inflate(layoutResId, null); + } + private void updateDecorationContainerSurface( SurfaceControl.Transaction startT, RelayoutResult<T> outResult) { if (mDecorationContainerSurface == null) { @@ -497,24 +507,33 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> throw new IllegalArgumentException("Unexpected alignment " + element.mAlignment); } - /** - * Checks if task has entered/exited immersive mode and requires a change in caption visibility. - */ - private void updateCaptionVisibility(View rootView, int displayId) { - final InsetsState insetsState = mDisplayController.getInsetsState(displayId); - for (int i = 0; i < insetsState.sourceSize(); i++) { - final InsetsSource source = insetsState.sourceAt(i); - if (source.getType() != statusBars()) { - continue; - } + void onKeyguardStateChanged(boolean visible, boolean occluded) { + final boolean prevVisAndOccluded = mIsKeyguardVisibleAndOccluded; + mIsKeyguardVisibleAndOccluded = visible && occluded; + final boolean changed = prevVisAndOccluded != mIsKeyguardVisibleAndOccluded; + if (changed) { + relayout(mTaskInfo); + } + } - mIsCaptionVisible = source.isVisible(); - setCaptionVisibility(rootView, mIsCaptionVisible); + void onInsetsStateChanged(@NonNull InsetsState insetsState) { + final boolean prevStatusBarVisibility = mIsStatusBarVisible; + mIsStatusBarVisible = InsetsStateKt.isVisible(insetsState, statusBars()); + final boolean changed = prevStatusBarVisibility != mIsStatusBarVisible; - return; + if (changed) { + relayout(mTaskInfo); } } + /** + * Checks if task has entered/exited immersive mode and requires a change in caption visibility. + */ + private void updateCaptionVisibility(View rootView) { + mIsCaptionVisible = mIsStatusBarVisible && !mIsKeyguardVisibleAndOccluded; + setCaptionVisibility(rootView, mIsCaptionVisible); + } + void setTaskDragResizer(TaskDragResizer taskDragResizer) { mTaskDragResizer = taskDragResizer; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/extension/InsetsState.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/extension/InsetsState.kt new file mode 100644 index 000000000000..be01a20f9307 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/extension/InsetsState.kt @@ -0,0 +1,33 @@ +/* + * 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.windowdecor.extension + +import android.view.InsetsState +import android.view.WindowInsets + +/** + * Whether the source of the given [type] is visible or false if there is no source of that type. + */ +fun InsetsState.isVisible(@WindowInsets.Type.InsetsType type: Int): Boolean { + for (i in 0 until sourceSize()) { + val source = sourceAt(i) + if (source.type != type) { + continue + } + return source.isVisible + } + return false +} diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ClosePipBySwipingDownTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ClosePipBySwipingDownTest.kt index a5e0550d9c79..3ffc9d7b87f6 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ClosePipBySwipingDownTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ClosePipBySwipingDownTest.kt @@ -21,6 +21,7 @@ import android.tools.flicker.junit.FlickerParametersRunnerFactory import android.tools.flicker.legacy.FlickerBuilder import android.tools.flicker.legacy.LegacyFlickerTest import android.tools.traces.component.ComponentNameMatcher +import com.android.wm.shell.Flags import com.android.wm.shell.flicker.pip.common.ClosePipTransition import org.junit.FixMethodOrder import org.junit.Test @@ -60,7 +61,7 @@ class ClosePipBySwipingDownTest(flicker: LegacyFlickerTest) : ClosePipTransition val pipCenterY = pipRegion.centerY() val displayCenterX = device.displayWidth / 2 val barComponent = - if (flicker.scenario.isTablet) { + if (flicker.scenario.isTablet || Flags.enableTaskbarOnPhones()) { ComponentNameMatcher.TASK_BAR } else { ComponentNameMatcher.NAV_BAR diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/ICommonAssertions.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/ICommonAssertions.kt index 4465a16a8e0f..acaf021981ed 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/ICommonAssertions.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/ICommonAssertions.kt @@ -28,12 +28,16 @@ import com.android.server.wm.flicker.statusBarLayerPositionAtStartAndEnd import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible import com.android.server.wm.flicker.taskBarLayerIsVisibleAtStartAndEnd import com.android.server.wm.flicker.taskBarWindowIsAlwaysVisible +import com.android.wm.shell.Flags import org.junit.Assume import org.junit.Test interface ICommonAssertions { val flicker: LegacyFlickerTest + val usesTaskbar: Boolean + get() = flicker.scenario.isTablet || Flags.enableTaskbarOnPhones() + /** Checks that all parts of the screen are covered during the transition */ @Presubmit @Test fun entireScreenCovered() = flicker.entireScreenCovered() @@ -43,7 +47,7 @@ interface ICommonAssertions { @Presubmit @Test fun navBarLayerIsVisibleAtStartAndEnd() { - Assume.assumeFalse(flicker.scenario.isTablet) + Assume.assumeFalse(usesTaskbar) flicker.navBarLayerIsVisibleAtStartAndEnd() } @@ -54,7 +58,7 @@ interface ICommonAssertions { @Presubmit @Test fun navBarLayerPositionAtStartAndEnd() { - Assume.assumeFalse(flicker.scenario.isTablet) + Assume.assumeFalse(usesTaskbar) flicker.navBarLayerPositionAtStartAndEnd() } @@ -66,7 +70,7 @@ interface ICommonAssertions { @Presubmit @Test fun navBarWindowIsAlwaysVisible() { - Assume.assumeFalse(flicker.scenario.isTablet) + Assume.assumeFalse(usesTaskbar) flicker.navBarWindowIsAlwaysVisible() } @@ -76,7 +80,7 @@ interface ICommonAssertions { @Presubmit @Test fun taskBarLayerIsVisibleAtStartAndEnd() { - Assume.assumeTrue(flicker.scenario.isTablet) + Assume.assumeTrue(usesTaskbar) flicker.taskBarLayerIsVisibleAtStartAndEnd() } @@ -88,7 +92,7 @@ interface ICommonAssertions { @Presubmit @Test fun taskBarWindowIsAlwaysVisible() { - Assume.assumeTrue(flicker.scenario.isTablet) + Assume.assumeTrue(usesTaskbar) flicker.taskBarWindowIsAlwaysVisible() } diff --git a/libs/WindowManager/Shell/tests/unittest/Android.bp b/libs/WindowManager/Shell/tests/unittest/Android.bp index a0408652a29b..4d761e18b990 100644 --- a/libs/WindowManager/Shell/tests/unittest/Android.bp +++ b/libs/WindowManager/Shell/tests/unittest/Android.bp @@ -44,6 +44,8 @@ android_test { "androidx.test.runner", "androidx.test.rules", "androidx.test.ext.junit", + "androidx.datastore_datastore", + "kotlinx_coroutines_test", "androidx.dynamicanimation_dynamicanimation", "dagger2", "frameworks-base-testutils", diff --git a/libs/WindowManager/Shell/tests/unittest/res/layout/caption_layout.xml b/libs/WindowManager/Shell/tests/unittest/res/layout/caption_layout.xml new file mode 100644 index 000000000000..079ee13ba4da --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/res/layout/caption_layout.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> +<com.android.wm.shell.windowdecor.WindowDecorLinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/caption" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="end" + android:background="@drawable/caption_decor_title"/>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java index 669e433ba386..9df9956fa0e1 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java @@ -18,6 +18,7 @@ package com.android.wm.shell.common; import static android.view.Display.DEFAULT_DISPLAY; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -160,6 +161,19 @@ public class DisplayInsetsControllerTest extends ShellTestCase { assertTrue(secondListener.hideInsetsCount == 1); } + @Test + public void testGlobalListenerCallback() throws RemoteException { + TrackedListener globalListener = new TrackedListener(); + addDisplay(SECOND_DISPLAY); + mController.addGlobalInsetsChangedListener(globalListener); + + mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).insetsChanged(null); + mInsetsControllersByDisplayId.get(SECOND_DISPLAY).insetsChanged(null); + mExecutor.flushAll(); + + assertEquals(2, globalListener.insetsChangedCount); + } + private void addDisplay(int displayId) throws RemoteException { mController.onDisplayAdded(displayId); verify(mWm, times(mInsetsControllersByDisplayId.size() + 1)) 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/desktopmode/DragToDesktopTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt index e4e2bd216c94..c97bcfb1a4cb 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt @@ -11,6 +11,7 @@ import android.os.IBinder import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper import android.view.SurfaceControl +import android.view.WindowManager.TRANSIT_OPEN import android.window.TransitionInfo import android.window.TransitionInfo.FLAG_IS_WALLPAPER import android.window.WindowContainerTransaction @@ -27,6 +28,7 @@ import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_CANCEL_D import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP import com.android.wm.shell.windowdecor.MoveToDesktopAnimator +import java.util.function.Supplier import junit.framework.Assert.assertFalse import org.junit.Before import org.junit.Test @@ -40,7 +42,6 @@ import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.verifyZeroInteractions import org.mockito.kotlin.whenever -import java.util.function.Supplier /** Tests of [DragToDesktopTransitionHandler]. */ @SmallTest @@ -52,17 +53,26 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { @Mock private lateinit var taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer @Mock private lateinit var splitScreenController: SplitScreenController @Mock private lateinit var dragAnimator: MoveToDesktopAnimator - @Mock - private lateinit var mockInteractionJankMonitor: InteractionJankMonitor + @Mock private lateinit var mockInteractionJankMonitor: InteractionJankMonitor + @Mock private lateinit var draggedTaskLeash: SurfaceControl + @Mock private lateinit var homeTaskLeash: SurfaceControl private val transactionSupplier = Supplier { mock<SurfaceControl.Transaction>() } - private lateinit var handler: DragToDesktopTransitionHandler + private lateinit var defaultHandler: DragToDesktopTransitionHandler + private lateinit var springHandler: SpringDragToDesktopTransitionHandler @Before fun setUp() { - handler = - DragToDesktopTransitionHandler( + defaultHandler = DefaultDragToDesktopTransitionHandler( + context, + transitions, + taskDisplayAreaOrganizer, + mockInteractionJankMonitor, + transactionSupplier, + ) + .apply { setSplitScreenController(splitScreenController) } + springHandler = SpringDragToDesktopTransitionHandler( context, transitions, taskDisplayAreaOrganizer, @@ -76,10 +86,10 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { fun startDragToDesktop_animateDragWhenReady() { val task = createTask() // Simulate transition is started. - val transition = startDragToDesktopTransition(task, dragAnimator) + val transition = startDragToDesktopTransition(defaultHandler, task, dragAnimator) // Now it's ready to animate. - handler.startAnimation( + defaultHandler.startAnimation( transition = transition, info = createTransitionInfo( @@ -96,65 +106,70 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { @Test fun startDragToDesktop_cancelledBeforeReady_startCancelTransition() { - performEarlyCancel(DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL) + performEarlyCancel( + defaultHandler, + DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL + ) verify(transitions) - .startTransition(eq(TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP), any(), eq(handler)) + .startTransition( + eq(TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP), + any(), + eq(defaultHandler) + ) } @Test fun startDragToDesktop_cancelledBeforeReady_verifySplitLeftCancel() { - performEarlyCancel(DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_LEFT) - verify(splitScreenController).requestEnterSplitSelect( - any(), - any(), - eq(SPLIT_POSITION_TOP_OR_LEFT), - any() + performEarlyCancel( + defaultHandler, + DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_LEFT ) + verify(splitScreenController) + .requestEnterSplitSelect(any(), any(), eq(SPLIT_POSITION_TOP_OR_LEFT), any()) } @Test fun startDragToDesktop_cancelledBeforeReady_verifySplitRightCancel() { - performEarlyCancel(DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_RIGHT) - verify(splitScreenController).requestEnterSplitSelect( - any(), - any(), - eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), - any() + performEarlyCancel( + defaultHandler, + DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_RIGHT ) + verify(splitScreenController) + .requestEnterSplitSelect(any(), any(), eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), any()) } @Test fun startDragToDesktop_aborted_finishDropped() { val task = createTask() // Simulate transition is started. - val transition = startDragToDesktopTransition(task, dragAnimator) + val transition = startDragToDesktopTransition(defaultHandler, task, dragAnimator) // But the transition was aborted. - handler.onTransitionConsumed(transition, aborted = true, mock()) + defaultHandler.onTransitionConsumed(transition, aborted = true, mock()) // Attempt to finish the failed drag start. - handler.finishDragToDesktopTransition(WindowContainerTransaction()) + defaultHandler.finishDragToDesktopTransition(WindowContainerTransaction()) // Should not be attempted and state should be reset. verify(transitions, never()) - .startTransition(eq(TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP), any(), any()) - assertFalse(handler.inProgress) + .startTransition(eq(TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP), any(), any()) + assertFalse(defaultHandler.inProgress) } @Test fun startDragToDesktop_aborted_cancelDropped() { val task = createTask() // Simulate transition is started. - val transition = startDragToDesktopTransition(task, dragAnimator) + val transition = startDragToDesktopTransition(defaultHandler, task, dragAnimator) // But the transition was aborted. - handler.onTransitionConsumed(transition, aborted = true, mock()) + defaultHandler.onTransitionConsumed(transition, aborted = true, mock()) // Attempt to finish the failed drag start. - handler.cancelDragToDesktopTransition( + defaultHandler.cancelDragToDesktopTransition( DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL ) // Should not be attempted and state should be reset. - assertFalse(handler.inProgress) + assertFalse(defaultHandler.inProgress) } @Test @@ -162,23 +177,24 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { val task = createTask() // Simulate attempt to start two drag to desktop transitions. - startDragToDesktopTransition(task, dragAnimator) - startDragToDesktopTransition(task, dragAnimator) + startDragToDesktopTransition(defaultHandler, task, dragAnimator) + startDragToDesktopTransition(defaultHandler, task, dragAnimator) // Verify transition only started once. - verify(transitions, times(1)).startTransition( + verify(transitions, times(1)) + .startTransition( eq(TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP), any(), - eq(handler) - ) + eq(defaultHandler) + ) } @Test fun cancelDragToDesktop_startWasReady_cancel() { - startDrag() + startDrag(defaultHandler) // Then user cancelled after it had already started. - handler.cancelDragToDesktopTransition( + defaultHandler.cancelDragToDesktopTransition( DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL ) @@ -188,48 +204,40 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { @Test fun cancelDragToDesktop_splitLeftCancelType_splitRequested() { - startDrag() + startDrag(defaultHandler) // Then user cancelled it, requesting split. - handler.cancelDragToDesktopTransition( + defaultHandler.cancelDragToDesktopTransition( DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_LEFT ) // Verify the request went through split controller. - verify(splitScreenController).requestEnterSplitSelect( - any(), - any(), - eq(SPLIT_POSITION_TOP_OR_LEFT), - any() - ) + verify(splitScreenController) + .requestEnterSplitSelect(any(), any(), eq(SPLIT_POSITION_TOP_OR_LEFT), any()) } @Test fun cancelDragToDesktop_splitRightCancelType_splitRequested() { - startDrag() + startDrag(defaultHandler) // Then user cancelled it, requesting split. - handler.cancelDragToDesktopTransition( + defaultHandler.cancelDragToDesktopTransition( DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_RIGHT ) // Verify the request went through split controller. - verify(splitScreenController).requestEnterSplitSelect( - any(), - any(), - eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), - any() - ) + verify(splitScreenController) + .requestEnterSplitSelect(any(), any(), eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), any()) } @Test fun cancelDragToDesktop_startWasNotReady_animateCancel() { val task = createTask() // Simulate transition is started and is ready to animate. - startDragToDesktopTransition(task, dragAnimator) + startDragToDesktopTransition(defaultHandler, task, dragAnimator) // Then user cancelled before the transition was ready and animated. - handler.cancelDragToDesktopTransition( + defaultHandler.cancelDragToDesktopTransition( DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL ) @@ -240,50 +248,139 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { @Test fun cancelDragToDesktop_transitionNotInProgress_dropCancel() { // Then cancel is called before the transition was started. - handler.cancelDragToDesktopTransition( + defaultHandler.cancelDragToDesktopTransition( DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL ) // Verify cancel is dropped. - verify(transitions, never()).startTransition( + verify(transitions, never()) + .startTransition( eq(TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP), any(), - eq(handler) - ) + eq(defaultHandler) + ) } @Test fun finishDragToDesktop_transitionNotInProgress_dropFinish() { // Then finish is called before the transition was started. - handler.finishDragToDesktopTransition(WindowContainerTransaction()) + defaultHandler.finishDragToDesktopTransition(WindowContainerTransaction()) // Verify finish is dropped. - verify(transitions, never()).startTransition( + verify(transitions, never()) + .startTransition( eq(TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP), any(), - eq(handler) + eq(defaultHandler) + ) + } + + @Test + fun mergeAnimation_otherTransition_doesNotMerge() { + val transaction = mock<SurfaceControl.Transaction>() + val finishCallback = mock<Transitions.TransitionFinishCallback>() + val task = createTask() + + startDrag(defaultHandler, task) + defaultHandler.mergeAnimation( + transition = mock(), + info = createTransitionInfo(type = TRANSIT_OPEN, draggedTask = task), + t = transaction, + mergeTarget = mock(), + finishCallback = finishCallback ) + + // Should NOT have any transaction changes + verifyZeroInteractions(transaction) + // Should NOT merge animation + verify(finishCallback, never()).onTransitionFinished(any()) } - private fun startDrag() { + @Test + fun mergeAnimation_endTransition_mergesAnimation() { + val playingFinishTransaction = mock<SurfaceControl.Transaction>() + val mergedStartTransaction = mock<SurfaceControl.Transaction>() + val finishCallback = mock<Transitions.TransitionFinishCallback>() val task = createTask() + val startTransition = + startDrag(defaultHandler, task, finishTransaction = playingFinishTransaction) + defaultHandler.onTaskResizeAnimationListener = mock() + + defaultHandler.mergeAnimation( + transition = mock(), + info = + createTransitionInfo( + type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, + draggedTask = task + ), + t = mergedStartTransaction, + mergeTarget = startTransition, + finishCallback = finishCallback + ) + + // Should show dragged task layer in start and finish transaction + verify(mergedStartTransaction).show(draggedTaskLeash) + verify(playingFinishTransaction).show(draggedTaskLeash) + // Should merge animation + verify(finishCallback).onTransitionFinished(null) + } + + @Test + fun mergeAnimation_endTransition_springHandler_hidesHome() { + whenever(dragAnimator.computeCurrentVelocity()).thenReturn(PointF()) + val playingFinishTransaction = mock<SurfaceControl.Transaction>() + val mergedStartTransaction = mock<SurfaceControl.Transaction>() + val finishCallback = mock<Transitions.TransitionFinishCallback>() + val task = createTask() + val startTransition = + startDrag(springHandler, task, finishTransaction = playingFinishTransaction) + springHandler.onTaskResizeAnimationListener = mock() + + springHandler.mergeAnimation( + transition = mock(), + info = + createTransitionInfo( + type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, + draggedTask = task + ), + t = mergedStartTransaction, + mergeTarget = startTransition, + finishCallback = finishCallback + ) + + // Should show dragged task layer in start and finish transaction + verify(mergedStartTransaction).show(draggedTaskLeash) + verify(playingFinishTransaction).show(draggedTaskLeash) + // Should hide home task leash in finish transaction + verify(playingFinishTransaction).hide(homeTaskLeash) + // Should merge animation + verify(finishCallback).onTransitionFinished(null) + } + + private fun startDrag( + handler: DragToDesktopTransitionHandler, + task: RunningTaskInfo = createTask(), + finishTransaction: SurfaceControl.Transaction = mock() + ): IBinder { whenever(dragAnimator.position).thenReturn(PointF()) // Simulate transition is started and is ready to animate. - val transition = startDragToDesktopTransition(task, dragAnimator) + val transition = startDragToDesktopTransition(handler, task, dragAnimator) handler.startAnimation( transition = transition, info = - createTransitionInfo( - type = TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP, - draggedTask = task - ), + createTransitionInfo( + type = TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP, + draggedTask = task + ), startTransaction = mock(), - finishTransaction = mock(), + finishTransaction = finishTransaction, finishCallback = {} ) + return transition } private fun startDragToDesktopTransition( + handler: DragToDesktopTransitionHandler, task: RunningTaskInfo, dragAnimator: MoveToDesktopAnimator ): IBinder { @@ -300,20 +397,23 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { return token } - private fun performEarlyCancel(cancelState: DragToDesktopTransitionHandler.CancelState) { + private fun performEarlyCancel( + handler: DragToDesktopTransitionHandler, + cancelState: DragToDesktopTransitionHandler.CancelState + ) { val task = createTask() // Simulate transition is started and is ready to animate. - val transition = startDragToDesktopTransition(task, dragAnimator) + val transition = startDragToDesktopTransition(handler, task, dragAnimator) handler.cancelDragToDesktopTransition(cancelState) handler.startAnimation( transition = transition, info = - createTransitionInfo( - type = TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP, - draggedTask = task - ), + createTransitionInfo( + type = TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP, + draggedTask = task + ), startTransaction = mock(), finishTransaction = mock(), finishCallback = {} @@ -340,7 +440,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { private fun createTransitionInfo(type: Int, draggedTask: RunningTaskInfo): TransitionInfo { return TransitionInfo(type, 0 /* flags */).apply { addChange( // Home. - TransitionInfo.Change(mock(), mock()).apply { + TransitionInfo.Change(mock(), homeTaskLeash).apply { parent = null taskInfo = TestRunningTaskInfoBuilder().setActivityType(ACTIVITY_TYPE_HOME).build() @@ -348,7 +448,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { } ) addChange( // Dragged Task. - TransitionInfo.Change(mock(), mock()).apply { + TransitionInfo.Change(mock(), draggedTaskLeash).apply { parent = null taskInfo = draggedTask } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationDatastoreRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationDatastoreRepositoryTest.kt new file mode 100644 index 000000000000..4d407387d323 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationDatastoreRepositoryTest.kt @@ -0,0 +1,125 @@ +/* + * 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.desktopmode.education + +import android.content.Context +import android.testing.AndroidTestingRunner +import androidx.datastore.core.DataStore +import androidx.datastore.core.DataStoreFactory +import androidx.datastore.dataStoreFile +import androidx.test.core.app.ApplicationProvider +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry +import com.android.wm.shell.desktopmode.education.data.AppHandleEducationDatastoreRepository +import com.android.wm.shell.desktopmode.education.data.WindowingEducationProto +import com.google.common.truth.Truth.assertThat +import java.io.File +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@ExperimentalCoroutinesApi +class AppHandleEducationDatastoreRepositoryTest { + private val testContext: Context = InstrumentationRegistry.getInstrumentation().targetContext + private lateinit var testDatastore: DataStore<WindowingEducationProto> + private lateinit var datastoreRepository: AppHandleEducationDatastoreRepository + private lateinit var datastoreScope: CoroutineScope + + @Before + fun setUp() { + Dispatchers.setMain(StandardTestDispatcher()) + datastoreScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) + testDatastore = + DataStoreFactory.create( + serializer = + AppHandleEducationDatastoreRepository.Companion.WindowingEducationProtoSerializer, + scope = datastoreScope) { + testContext.dataStoreFile(APP_HANDLE_EDUCATION_DATASTORE_TEST_FILE) + } + datastoreRepository = AppHandleEducationDatastoreRepository(testDatastore) + } + + @After + fun tearDown() { + File(ApplicationProvider.getApplicationContext<Context>().filesDir, "datastore") + .deleteRecursively() + + datastoreScope.cancel() + } + + @Test + fun getWindowingEducationProto_returnsCorrectProto() = + runTest(StandardTestDispatcher()) { + val windowingEducationProto = + createWindowingEducationProto( + educationViewedTimestampMillis = 123L, + featureUsedTimestampMillis = 124L, + appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 2), + appUsageStatsLastUpdateTimestampMillis = 125L) + testDatastore.updateData { windowingEducationProto } + + val resultProto = datastoreRepository.windowingEducationProto() + + assertThat(resultProto).isEqualTo(windowingEducationProto) + } + + private fun createWindowingEducationProto( + educationViewedTimestampMillis: Long? = null, + featureUsedTimestampMillis: Long? = null, + appUsageStats: Map<String, Int>? = null, + appUsageStatsLastUpdateTimestampMillis: Long? = null + ): WindowingEducationProto = + WindowingEducationProto.newBuilder() + .apply { + if (educationViewedTimestampMillis != null) + setEducationViewedTimestampMillis(educationViewedTimestampMillis) + if (featureUsedTimestampMillis != null) + setFeatureUsedTimestampMillis(featureUsedTimestampMillis) + setAppHandleEducation( + createAppHandleEducationProto( + appUsageStats, appUsageStatsLastUpdateTimestampMillis)) + } + .build() + + private fun createAppHandleEducationProto( + appUsageStats: Map<String, Int>? = null, + appUsageStatsLastUpdateTimestampMillis: Long? = null + ): WindowingEducationProto.AppHandleEducation = + WindowingEducationProto.AppHandleEducation.newBuilder() + .apply { + if (appUsageStats != null) putAllAppUsageStats(appUsageStats) + if (appUsageStatsLastUpdateTimestampMillis != null) + setAppUsageStatsLastUpdateTimestampMillis(appUsageStatsLastUpdateTimestampMillis) + } + .build() + + companion object { + private const val GMAIL_PACKAGE_NAME = "com.google.android.gm" + private const val APP_HANDLE_EDUCATION_DATASTORE_TEST_FILE = "app_handle_education_test.pb" + } +} 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/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java index ee9f88663326..af6c077303c4 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java @@ -370,6 +370,6 @@ public class StartingSurfaceDrawerTests extends ShellTestCase { Surface.ROTATION_0, taskSize, contentInsets, new Rect() /* letterboxInsets */, false, true /* isRealSnapshot */, WINDOWING_MODE_FULLSCREEN, 0 /* systemUiVisibility */, false /* isTranslucent */, - hasImeSurface /* hasImeSurface */); + hasImeSurface /* hasImeSurface */, 0 /* uiMode */); } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt index 68975ec3556e..fa905e2e5c37 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt @@ -36,7 +36,6 @@ import android.net.Uri import android.os.Handler import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags -import android.platform.test.annotations.RequiresFlagsEnabled import android.platform.test.flag.junit.CheckFlagsRule import android.platform.test.flag.junit.DeviceFlagsValueProvider import android.platform.test.flag.junit.SetFlagsRule @@ -56,9 +55,9 @@ import android.view.Surface import android.view.SurfaceControl import android.view.SurfaceView import android.view.View -import android.view.WindowInsets.Type.navigationBars import android.view.WindowInsets.Type.statusBars import android.window.WindowContainerTransaction +import android.window.WindowContainerTransaction.HierarchyOp import androidx.test.filters.SmallTest import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession @@ -85,11 +84,11 @@ import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition import com.android.wm.shell.freeform.FreeformTaskTransitionStarter import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.splitscreen.SplitScreenController -import com.android.wm.shell.sysui.KeyguardChangeListener import com.android.wm.shell.sysui.ShellCommandHandler import com.android.wm.shell.sysui.ShellController import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.Transitions +import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel.DesktopModeKeyguardChangeListener import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel.DesktopModeOnInsetsChangedListener import java.util.Optional import java.util.function.Consumer @@ -172,6 +171,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { private lateinit var shellInit: ShellInit private lateinit var desktopModeOnInsetsChangedListener: DesktopModeOnInsetsChangedListener private lateinit var displayChangingListener: DisplayChangeController.OnDisplayChangingListener + private lateinit var desktopModeOnKeyguardChangedListener: DesktopModeKeyguardChangeListener private lateinit var desktopModeWindowDecorViewModel: DesktopModeWindowDecorViewModel @Before @@ -225,17 +225,20 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { shellInit.init() - val insetListenerCaptor = - argumentCaptor<DesktopModeWindowDecorViewModel.DesktopModeOnInsetsChangedListener>() - verify(displayInsetsController) - .addInsetsChangedListener(anyInt(), insetListenerCaptor.capture()) - desktopModeOnInsetsChangedListener = insetListenerCaptor.firstValue - val displayChangingListenerCaptor = argumentCaptor<DisplayChangeController.OnDisplayChangingListener>() verify(mockDisplayController) .addDisplayChangingController(displayChangingListenerCaptor.capture()) displayChangingListener = displayChangingListenerCaptor.firstValue + val insetsChangedCaptor = + argumentCaptor<DesktopModeWindowDecorViewModel.DesktopModeOnInsetsChangedListener>() + verify(displayInsetsController) + .addGlobalInsetsChangedListener(insetsChangedCaptor.capture()) + desktopModeOnInsetsChangedListener = insetsChangedCaptor.firstValue + val keyguardChangedCaptor = + argumentCaptor<DesktopModeKeyguardChangeListener>() + verify(mockShellController).addKeyguardChangeListener(keyguardChangedCaptor.capture()) + desktopModeOnKeyguardChangedListener = keyguardChangedCaptor.firstValue } @After @@ -354,23 +357,33 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { } @Test - fun testCaptionIsNotCreatedWhenKeyguardIsVisible() { - val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN, focused = true) - val keyguardListenerCaptor = argumentCaptor<KeyguardChangeListener>() - verify(mockShellController).addKeyguardChangeListener(keyguardListenerCaptor.capture()) + fun testCloseButtonInFreeform() { + val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM) + val windowDecor = setUpMockDecorationForTask(task) - keyguardListenerCaptor.firstValue.onKeyguardVisibilityChanged( - true /* visible */, - true /* occluded */, - false /* animatingDismiss */ - ) onTaskOpening(task) + val onClickListenerCaptor = argumentCaptor<View.OnClickListener>() + verify(windowDecor).setCaptionListeners( + onClickListenerCaptor.capture(), any(), any(), any()) - task.setWindowingMode(WINDOWING_MODE_UNDEFINED) - task.setWindowingMode(ACTIVITY_TYPE_UNDEFINED) - onTaskChanging(task) + val onClickListener = onClickListenerCaptor.firstValue + val view = mock(View::class.java) + whenever(view.id).thenReturn(R.id.close_window) - assertFalse(windowDecorByTaskIdSpy.contains(task.taskId)) + val freeformTaskTransitionStarter = mock(FreeformTaskTransitionStarter::class.java) + desktopModeWindowDecorViewModel + .setFreeformTaskTransitionStarter(freeformTaskTransitionStarter) + + onClickListener.onClick(view) + + val transactionCaptor = argumentCaptor<WindowContainerTransaction>() + verify(freeformTaskTransitionStarter).startRemoveTransition(transactionCaptor.capture()) + val wct = transactionCaptor.firstValue + + assertEquals(1, wct.getHierarchyOps().size) + assertEquals(HierarchyOp.HIERARCHY_OP_TYPE_REMOVE_TASK, + wct.getHierarchyOps().get(0).getType()) + assertEquals(task.token.asBinder(), wct.getHierarchyOps().get(0).getContainer()) } @Test @@ -418,67 +431,50 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { } @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_IMMERSIVE_HANDLE_HIDING) - fun testRelayoutRunsWhenStatusBarsInsetsSourceVisibilityChanges() { - val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM, focused = true) - val decoration = setUpMockDecorationForTask(task) - - onTaskOpening(task) + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_IMMERSIVE_HANDLE_HIDING) + fun testInsetsStateChanged_notifiesAllDecorsInDisplay() { + val task1 = createTask(windowingMode = WINDOWING_MODE_FREEFORM, displayId = 1) + val decoration1 = setUpMockDecorationForTask(task1) + onTaskOpening(task1) + val task2 = createTask(windowingMode = WINDOWING_MODE_FREEFORM, displayId = 2) + val decoration2 = setUpMockDecorationForTask(task2) + onTaskOpening(task2) + val task3 = createTask(windowingMode = WINDOWING_MODE_FREEFORM, displayId = 2) + val decoration3 = setUpMockDecorationForTask(task3) + onTaskOpening(task3) // Add status bar insets source - val insetsState = InsetsState() - val statusBarInsetsSourceId = 0 - val statusBarInsetsSource = InsetsSource(statusBarInsetsSourceId, statusBars()) - statusBarInsetsSource.isVisible = false - insetsState.addSource(statusBarInsetsSource) - - desktopModeOnInsetsChangedListener.insetsChanged(insetsState) - - // Verify relayout occurs when status bar inset visibility changes - verify(decoration, times(1)).relayout(task) - } - - @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_IMMERSIVE_HANDLE_HIDING) - fun testRelayoutDoesNotRunWhenNonStatusBarsInsetsSourceVisibilityChanges() { - val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM, focused = true) - val decoration = setUpMockDecorationForTask(task) - - onTaskOpening(task) - - // Add navigation bar insets source - val insetsState = InsetsState() - val navigationBarInsetsSourceId = 1 - val navigationBarInsetsSource = InsetsSource(navigationBarInsetsSourceId, navigationBars()) - navigationBarInsetsSource.isVisible = false - insetsState.addSource(navigationBarInsetsSource) - - desktopModeOnInsetsChangedListener.insetsChanged(insetsState) + val insetsState = InsetsState().apply { + addSource(InsetsSource(0 /* id */, statusBars()).apply { + isVisible = false + }) + } + desktopModeOnInsetsChangedListener.insetsChanged(2 /* displayId */, insetsState) - // Verify relayout does not occur when non-status bar inset changes visibility - verify(decoration, never()).relayout(task) + verify(decoration1, never()).onInsetsStateChanged(insetsState) + verify(decoration2).onInsetsStateChanged(insetsState) + verify(decoration3).onInsetsStateChanged(insetsState) } @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_IMMERSIVE_HANDLE_HIDING) - fun testRelayoutDoesNotRunWhenNonStatusBarsInsetSourceVisibilityDoesNotChange() { - val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM, focused = true) - val decoration = setUpMockDecorationForTask(task) - - onTaskOpening(task) - - // Add status bar insets source - val insetsState = InsetsState() - val statusBarInsetsSourceId = 0 - val statusBarInsetsSource = InsetsSource(statusBarInsetsSourceId, statusBars()) - statusBarInsetsSource.isVisible = false - insetsState.addSource(statusBarInsetsSource) - - desktopModeOnInsetsChangedListener.insetsChanged(insetsState) - desktopModeOnInsetsChangedListener.insetsChanged(insetsState) - - // Verify relayout runs only once when status bar inset visibility changes. - verify(decoration, times(1)).relayout(task) + fun testKeyguardState_notifiesAllDecors() { + val task1 = createTask(windowingMode = WINDOWING_MODE_FREEFORM) + val decoration1 = setUpMockDecorationForTask(task1) + onTaskOpening(task1) + val task2 = createTask(windowingMode = WINDOWING_MODE_FREEFORM) + val decoration2 = setUpMockDecorationForTask(task2) + onTaskOpening(task2) + val task3 = createTask(windowingMode = WINDOWING_MODE_FREEFORM) + val decoration3 = setUpMockDecorationForTask(task3) + onTaskOpening(task3) + + desktopModeOnKeyguardChangedListener + .onKeyguardVisibilityChanged(true /* visible */, true /* occluded */, + false /* animatingDismiss */) + + verify(decoration1).onKeyguardStateChanged(true /* visible */, true /* occluded */) + verify(decoration2).onKeyguardStateChanged(true /* visible */, true /* occluded */) + verify(decoration3).onKeyguardStateChanged(true /* visible */, true /* occluded */) } @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java index 2ec3ab52725e..6154391c5e97 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java @@ -31,6 +31,7 @@ import static com.android.wm.shell.MockSurfaceControlHelper.createMockSurfaceCon import static com.google.common.truth.Truth.assertThat; +import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; import static org.junit.Assert.assertEquals; @@ -63,6 +64,7 @@ import android.testing.AndroidTestingRunner; import android.util.DisplayMetrics; import android.view.AttachedSurfaceControl; import android.view.Display; +import android.view.InsetsSource; import android.view.InsetsState; import android.view.SurfaceControl; import android.view.SurfaceControlViewHost; @@ -158,6 +160,8 @@ public class WindowDecorationTests extends ShellTestCase { mRelayoutParams.mShadowRadiusId = R.dimen.test_window_decor_shadow_radius; mRelayoutParams.mCornerRadius = CORNER_RADIUS; + when(mMockDisplayController.getDisplay(Display.DEFAULT_DISPLAY)) + .thenReturn(mock(Display.class)); doReturn(mMockSurfaceControlViewHost).when(mMockSurfaceControlViewHostFactory) .create(any(), any(), any()); when(mMockSurfaceControlViewHost.getRootSurfaceControl()) @@ -629,15 +633,15 @@ public class WindowDecorationTests extends ShellTestCase { .setVisible(true) .setBounds(new Rect(0, 0, 1000, 1000)) .build(); + taskInfo.isFocused = true; + // Caption visible at first. + when(mMockDisplayController.getInsetsState(taskInfo.displayId)) + .thenReturn(createInsetsState(statusBars(), true /* visible */)); final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo); - - // Run it once so that insets are added. - mInsetsState.getOrCreateSource(STATUS_BAR_INSET_SOURCE_ID, captionBar()).setVisible(true); windowDecor.relayout(taskInfo); - // Run it again so that insets are removed. - mInsetsState.getOrCreateSource(STATUS_BAR_INSET_SOURCE_ID, captionBar()).setVisible(false); - windowDecor.relayout(taskInfo); + // Hide caption so insets are removed. + windowDecor.onInsetsStateChanged(createInsetsState(statusBars(), false /* visible */)); verify(mMockWindowContainerTransaction).removeInsetsSource(eq(taskInfo.token), any(), eq(0) /* index */, eq(captionBar())); @@ -656,10 +660,10 @@ public class WindowDecorationTests extends ShellTestCase { .setVisible(true) .setBounds(new Rect(0, 0, 1000, 1000)) .build(); - final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo); - // Hidden from the beginning, so no insets were ever added. - mInsetsState.getOrCreateSource(STATUS_BAR_INSET_SOURCE_ID, captionBar()).setVisible(false); + when(mMockDisplayController.getInsetsState(taskInfo.displayId)) + .thenReturn(createInsetsState(statusBars(), false /* visible */)); + final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo); windowDecor.relayout(taskInfo); // Never added. @@ -896,6 +900,78 @@ public class WindowDecorationTests extends ShellTestCase { windowDecor.updateViewHost(mRelayoutParams, null /* onDrawTransaction */, mRelayoutResult); } + @Test + public void onStatusBarVisibilityChange_shownToHidden_hidesCaption() { + final ActivityManager.RunningTaskInfo task = createTaskInfo(); + when(mMockDisplayController.getInsetsState(task.displayId)) + .thenReturn(createInsetsState(statusBars(), true /* visible */)); + final TestWindowDecoration decor = createWindowDecoration(task); + decor.relayout(task); + assertTrue(decor.mIsCaptionVisible); + + decor.onInsetsStateChanged(createInsetsState(statusBars(), false /* visible */)); + + assertFalse(decor.mIsCaptionVisible); + } + + @Test + public void onStatusBarVisibilityChange_hiddenToShown_showsCaption() { + final ActivityManager.RunningTaskInfo task = createTaskInfo(); + when(mMockDisplayController.getInsetsState(task.displayId)) + .thenReturn(createInsetsState(statusBars(), false /* visible */)); + final TestWindowDecoration decor = createWindowDecoration(task); + decor.relayout(task); + assertFalse(decor.mIsCaptionVisible); + + decor.onInsetsStateChanged(createInsetsState(statusBars(), true /* visible */)); + + assertTrue(decor.mIsCaptionVisible); + } + + @Test + public void onKeyguardStateChange_hiddenToShownAndOccluding_hidesCaption() { + final ActivityManager.RunningTaskInfo task = createTaskInfo(); + when(mMockDisplayController.getInsetsState(task.displayId)) + .thenReturn(createInsetsState(statusBars(), true /* visible */)); + final TestWindowDecoration decor = createWindowDecoration(task); + decor.relayout(task); + assertTrue(decor.mIsCaptionVisible); + + decor.onKeyguardStateChanged(true /* visible */, true /* occluding */); + + assertFalse(decor.mIsCaptionVisible); + } + + @Test + public void onKeyguardStateChange_showingAndOccludingToHidden_showsCaption() { + final ActivityManager.RunningTaskInfo task = createTaskInfo(); + when(mMockDisplayController.getInsetsState(task.displayId)) + .thenReturn(createInsetsState(statusBars(), true /* visible */)); + final TestWindowDecoration decor = createWindowDecoration(task); + decor.onKeyguardStateChanged(true /* visible */, true /* occluding */); + assertFalse(decor.mIsCaptionVisible); + + decor.onKeyguardStateChanged(false /* visible */, false /* occluding */); + + assertTrue(decor.mIsCaptionVisible); + } + + private ActivityManager.RunningTaskInfo createTaskInfo() { + final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder() + .setVisible(true) + .build(); + taskInfo.isFocused = true; + return taskInfo; + } + + private InsetsState createInsetsState(@WindowInsets.Type.InsetsType int type, boolean visible) { + final InsetsState state = new InsetsState(); + final InsetsSource source = new InsetsSource(0, type); + source.setVisible(visible); + state.addSource(source); + return state; + } + private TestWindowDecoration createWindowDecoration(ActivityManager.RunningTaskInfo taskInfo) { return new TestWindowDecoration(mContext, mContext, mMockDisplayController, mMockShellTaskOrganizer, taskInfo, mMockTaskSurface, @@ -961,10 +1037,24 @@ public class WindowDecorationTests extends ShellTestCase { return null; } + @Override + int getCaptionViewId() { + return R.id.caption; + } + + @Override + TestView inflateLayout(Context context, int layoutResId) { + if (layoutResId == R.layout.caption_layout) { + return mMockView; + } + return super.inflateLayout(context, layoutResId); + } + void relayout(ActivityManager.RunningTaskInfo taskInfo, boolean applyStartTransactionOnDraw) { mRelayoutParams.mRunningTaskInfo = taskInfo; mRelayoutParams.mApplyStartTransactionOnDraw = applyStartTransactionOnDraw; + mRelayoutParams.mLayoutResId = R.layout.caption_layout; relayout(mRelayoutParams, mMockSurfaceControlStartT, mMockSurfaceControlFinishT, mMockWindowContainerTransaction, mMockView, mRelayoutResult); } diff --git a/libs/androidfw/BigBuffer.cpp b/libs/androidfw/BigBuffer.cpp index bedfc49a1b0d..43b56c32fb79 100644 --- a/libs/androidfw/BigBuffer.cpp +++ b/libs/androidfw/BigBuffer.cpp @@ -17,8 +17,8 @@ #include <androidfw/BigBuffer.h> #include <algorithm> +#include <iterator> #include <memory> -#include <vector> #include "android-base/logging.h" @@ -78,10 +78,27 @@ void* BigBuffer::NextBlock(size_t* out_size) { std::string BigBuffer::to_string() const { std::string result; + result.reserve(size_); for (const Block& block : blocks_) { result.append(block.buffer.get(), block.buffer.get() + block.size); } return result; } +void BigBuffer::AppendBuffer(BigBuffer&& buffer) { + std::move(buffer.blocks_.begin(), buffer.blocks_.end(), std::back_inserter(blocks_)); + size_ += buffer.size_; + buffer.blocks_.clear(); + buffer.size_ = 0; +} + +void BigBuffer::BackUp(size_t count) { + Block& block = blocks_.back(); + block.size -= count; + size_ -= count; + // BigBuffer is supposed to always give zeroed memory, but backing up usually means + // something has been already written into the block. Erase it. + std::fill_n(block.buffer.get() + block.size, count, 0); +} + } // namespace android diff --git a/libs/androidfw/include/androidfw/BigBuffer.h b/libs/androidfw/include/androidfw/BigBuffer.h index b99a4edf9d88..c4cd7c576542 100644 --- a/libs/androidfw/include/androidfw/BigBuffer.h +++ b/libs/androidfw/include/androidfw/BigBuffer.h @@ -14,13 +14,12 @@ * limitations under the License. */ -#ifndef _ANDROID_BIG_BUFFER_H -#define _ANDROID_BIG_BUFFER_H +#pragma once -#include <cstring> #include <memory> #include <string> #include <type_traits> +#include <utility> #include <vector> #include "android-base/logging.h" @@ -150,24 +149,11 @@ inline size_t BigBuffer::block_size() const { template <typename T> inline T* BigBuffer::NextBlock(size_t count) { - static_assert(std::is_standard_layout<T>::value, "T must be standard_layout type"); + static_assert(std::is_standard_layout_v<T>, "T must be standard_layout type"); CHECK(count != 0); return reinterpret_cast<T*>(NextBlockImpl(sizeof(T) * count)); } -inline void BigBuffer::BackUp(size_t count) { - Block& block = blocks_.back(); - block.size -= count; - size_ -= count; -} - -inline void BigBuffer::AppendBuffer(BigBuffer&& buffer) { - std::move(buffer.blocks_.begin(), buffer.blocks_.end(), std::back_inserter(blocks_)); - size_ += buffer.size_; - buffer.blocks_.clear(); - buffer.size_ = 0; -} - inline void BigBuffer::Pad(size_t bytes) { NextBlock<char>(bytes); } @@ -188,5 +174,3 @@ inline BigBuffer::const_iterator BigBuffer::end() const { } } // namespace android - -#endif // _ANDROID_BIG_BUFFER_H diff --git a/libs/androidfw/tests/BigBuffer_test.cpp b/libs/androidfw/tests/BigBuffer_test.cpp index 382d21e20846..7e38f1758057 100644 --- a/libs/androidfw/tests/BigBuffer_test.cpp +++ b/libs/androidfw/tests/BigBuffer_test.cpp @@ -98,4 +98,20 @@ TEST(BigBufferTest, PadAndAlignProperly) { ASSERT_EQ(8u, buffer.size()); } +TEST(BigBufferTest, BackUpZeroed) { + BigBuffer buffer(16); + + auto block = buffer.NextBlock<char>(2); + ASSERT_TRUE(block != nullptr); + ASSERT_EQ(2u, buffer.size()); + block[0] = 0x01; + block[1] = 0x02; + buffer.BackUp(1); + ASSERT_EQ(1u, buffer.size()); + auto new_block = buffer.NextBlock<char>(1); + ASSERT_TRUE(new_block != nullptr); + ASSERT_EQ(2u, buffer.size()); + ASSERT_EQ(0, *new_block); +} + } // namespace android diff --git a/libs/hwui/Properties.cpp b/libs/hwui/Properties.cpp index d184f64b1c2c..1217b47664dd 100644 --- a/libs/hwui/Properties.cpp +++ b/libs/hwui/Properties.cpp @@ -42,6 +42,9 @@ constexpr bool hdr_10bit_plus() { constexpr bool initialize_gl_always() { return false; } +constexpr bool resample_gainmap_regions() { + return false; +} } // namespace hwui_flags #endif @@ -100,6 +103,7 @@ float Properties::maxHdrHeadroomOn8bit = 5.f; // TODO: Refine this number bool Properties::clipSurfaceViews = false; bool Properties::hdr10bitPlus = false; +bool Properties::resampleGainmapRegions = false; int Properties::timeoutMultiplier = 1; @@ -175,6 +179,8 @@ bool Properties::load() { clipSurfaceViews = base::GetBoolProperty("debug.hwui.clip_surfaceviews", hwui_flags::clip_surfaceviews()); hdr10bitPlus = hwui_flags::hdr_10bit_plus(); + resampleGainmapRegions = base::GetBoolProperty("debug.hwui.resample_gainmap_regions", + hwui_flags::resample_gainmap_regions()); timeoutMultiplier = android::base::GetIntProperty("ro.hw_timeout_multiplier", 1); diff --git a/libs/hwui/Properties.h b/libs/hwui/Properties.h index e2646422030e..73e80ce4afd0 100644 --- a/libs/hwui/Properties.h +++ b/libs/hwui/Properties.h @@ -342,6 +342,7 @@ public: static bool clipSurfaceViews; static bool hdr10bitPlus; + static bool resampleGainmapRegions; static int timeoutMultiplier; diff --git a/libs/hwui/aconfig/hwui_flags.aconfig b/libs/hwui/aconfig/hwui_flags.aconfig index cd3ae5342f4e..13c0b00daa21 100644 --- a/libs/hwui/aconfig/hwui_flags.aconfig +++ b/libs/hwui/aconfig/hwui_flags.aconfig @@ -97,3 +97,13 @@ flag { description: "Initialize GL even when HWUI is set to use Vulkan. This improves app startup time for apps using GL." bug: "335172671" } + +flag { + name: "resample_gainmap_regions" + namespace: "core_graphics" + description: "Resample gainmaps when decoding regions, to improve visual quality" + bug: "352847821" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/libs/hwui/jni/BitmapRegionDecoder.cpp b/libs/hwui/jni/BitmapRegionDecoder.cpp index ea5c14486ea4..6a65b8273194 100644 --- a/libs/hwui/jni/BitmapRegionDecoder.cpp +++ b/libs/hwui/jni/BitmapRegionDecoder.cpp @@ -87,8 +87,17 @@ public: requireUnpremul, prefColorSpace); } - bool decodeGainmapRegion(sp<uirenderer::Gainmap>* outGainmap, int outWidth, int outHeight, - const SkIRect& desiredSubset, int sampleSize, bool requireUnpremul) { + // Decodes the gainmap region. If decoding succeeded, returns true and + // populate outGainmap with the decoded gainmap. Otherwise, returns false. + // + // Note that the desiredSubset is the logical region within the source + // gainmap that we want to decode. This is used for scaling into the final + // bitmap, since we do not want to include portions of the gainmap outside + // of this region. desiredSubset is also _not_ guaranteed to be + // pixel-aligned, so it's not possible to simply resize the resulting + // bitmap to accomplish this. + bool decodeGainmapRegion(sp<uirenderer::Gainmap>* outGainmap, SkISize bitmapDimensions, + const SkRect& desiredSubset, int sampleSize, bool requireUnpremul) { SkColorType decodeColorType = mGainmapBRD->computeOutputColorType(kN32_SkColorType); sk_sp<SkColorSpace> decodeColorSpace = mGainmapBRD->computeOutputColorSpace(decodeColorType, nullptr); @@ -107,13 +116,30 @@ public: // allocation type. RecyclingClippingPixelAllocator will populate this with the // actual alpha type in either allocPixelRef() or copyIfNecessary() sk_sp<Bitmap> nativeBitmap = Bitmap::allocateHeapBitmap(SkImageInfo::Make( - outWidth, outHeight, decodeColorType, kPremul_SkAlphaType, decodeColorSpace)); + bitmapDimensions, decodeColorType, kPremul_SkAlphaType, decodeColorSpace)); if (!nativeBitmap) { ALOGE("OOM allocating Bitmap for Gainmap"); return false; } - RecyclingClippingPixelAllocator allocator(nativeBitmap.get(), false); - if (!mGainmapBRD->decodeRegion(&bm, &allocator, desiredSubset, sampleSize, decodeColorType, + + // Round out the subset so that we decode a slightly larger region, in + // case the subset has fractional components. + SkIRect roundedSubset = desiredSubset.roundOut(); + + // Map the desired subset to the space of the decoded gainmap. The + // subset is repositioned relative to the resulting bitmap, and then + // scaled to respect the sampleSize. + // This assumes that the subset will not be modified by the decoder, which is true + // for existing gainmap formats. + SkRect logicalSubset = desiredSubset.makeOffset(-std::floorf(desiredSubset.left()), + -std::floorf(desiredSubset.top())); + logicalSubset.fLeft /= sampleSize; + logicalSubset.fTop /= sampleSize; + logicalSubset.fRight /= sampleSize; + logicalSubset.fBottom /= sampleSize; + + RecyclingClippingPixelAllocator allocator(nativeBitmap.get(), false, logicalSubset); + if (!mGainmapBRD->decodeRegion(&bm, &allocator, roundedSubset, sampleSize, decodeColorType, requireUnpremul, decodeColorSpace)) { ALOGE("Error decoding Gainmap region"); return false; @@ -130,16 +156,31 @@ public: return true; } - SkIRect calculateGainmapRegion(const SkIRect& mainImageRegion, int* inOutWidth, - int* inOutHeight) { + struct Projection { + SkRect srcRect; + SkISize destSize; + }; + Projection calculateGainmapRegion(const SkIRect& mainImageRegion, SkISize dimensions) { const float scaleX = ((float)mGainmapBRD->width()) / mMainImageBRD->width(); const float scaleY = ((float)mGainmapBRD->height()) / mMainImageBRD->height(); - *inOutWidth *= scaleX; - *inOutHeight *= scaleY; - // TODO: Account for rounding error? - return SkIRect::MakeLTRB(mainImageRegion.left() * scaleX, mainImageRegion.top() * scaleY, - mainImageRegion.right() * scaleX, - mainImageRegion.bottom() * scaleY); + + if (uirenderer::Properties::resampleGainmapRegions) { + const auto srcRect = SkRect::MakeLTRB( + mainImageRegion.left() * scaleX, mainImageRegion.top() * scaleY, + mainImageRegion.right() * scaleX, mainImageRegion.bottom() * scaleY); + // Request a slightly larger destination size so that the gainmap + // subset we want fits entirely in this size. + const auto destSize = SkISize::Make(std::ceil(dimensions.width() * scaleX), + std::ceil(dimensions.height() * scaleY)); + return Projection{.srcRect = srcRect, .destSize = destSize}; + } else { + const auto srcRect = SkRect::Make(SkIRect::MakeLTRB( + mainImageRegion.left() * scaleX, mainImageRegion.top() * scaleY, + mainImageRegion.right() * scaleX, mainImageRegion.bottom() * scaleY)); + const auto destSize = + SkISize::Make(dimensions.width() * scaleX, dimensions.height() * scaleY); + return Projection{.srcRect = srcRect, .destSize = destSize}; + } } bool hasGainmap() { return mGainmapBRD != nullptr; } @@ -327,16 +368,16 @@ static jobject nativeDecodeRegion(JNIEnv* env, jobject, jlong brdHandle, jint in sp<uirenderer::Gainmap> gainmap; bool hasGainmap = brd->hasGainmap(); if (hasGainmap) { - int gainmapWidth = bitmap.width(); - int gainmapHeight = bitmap.height(); + SkISize gainmapDims = SkISize::Make(bitmap.width(), bitmap.height()); if (javaBitmap) { // If we are recycling we must match the inBitmap's relative dimensions - gainmapWidth = recycledBitmap->width(); - gainmapHeight = recycledBitmap->height(); + gainmapDims.fWidth = recycledBitmap->width(); + gainmapDims.fHeight = recycledBitmap->height(); } - SkIRect gainmapSubset = brd->calculateGainmapRegion(subset, &gainmapWidth, &gainmapHeight); - if (!brd->decodeGainmapRegion(&gainmap, gainmapWidth, gainmapHeight, gainmapSubset, - sampleSize, requireUnpremul)) { + BitmapRegionDecoderWrapper::Projection gainmapProjection = + brd->calculateGainmapRegion(subset, gainmapDims); + if (!brd->decodeGainmapRegion(&gainmap, gainmapProjection.destSize, + gainmapProjection.srcRect, sampleSize, requireUnpremul)) { // If there is an error decoding Gainmap - we don't fail. We just don't provide Gainmap hasGainmap = false; } diff --git a/libs/hwui/jni/Graphics.cpp b/libs/hwui/jni/Graphics.cpp index a88139d6b5d6..258bf91f2124 100644 --- a/libs/hwui/jni/Graphics.cpp +++ b/libs/hwui/jni/Graphics.cpp @@ -1,12 +1,14 @@ #include <assert.h> +#include <cutils/ashmem.h> +#include <hwui/Canvas.h> +#include <log/log.h> +#include <nativehelper/JNIHelp.h> #include <unistd.h> -#include "jni.h" -#include <nativehelper/JNIHelp.h> #include "GraphicsJNI.h" - #include "SkBitmap.h" #include "SkCanvas.h" +#include "SkColor.h" #include "SkColorSpace.h" #include "SkFontMetrics.h" #include "SkImageInfo.h" @@ -14,10 +16,9 @@ #include "SkPoint.h" #include "SkRect.h" #include "SkRegion.h" +#include "SkSamplingOptions.h" #include "SkTypes.h" -#include <cutils/ashmem.h> -#include <hwui/Canvas.h> -#include <log/log.h> +#include "jni.h" using namespace android; @@ -630,13 +631,15 @@ bool HeapAllocator::allocPixelRef(SkBitmap* bitmap) { //////////////////////////////////////////////////////////////////////////////// -RecyclingClippingPixelAllocator::RecyclingClippingPixelAllocator(android::Bitmap* recycledBitmap, - bool mustMatchColorType) +RecyclingClippingPixelAllocator::RecyclingClippingPixelAllocator( + android::Bitmap* recycledBitmap, bool mustMatchColorType, + std::optional<SkRect> desiredSubset) : mRecycledBitmap(recycledBitmap) , mRecycledBytes(recycledBitmap ? recycledBitmap->getAllocationByteCount() : 0) , mSkiaBitmap(nullptr) , mNeedsCopy(false) - , mMustMatchColorType(mustMatchColorType) {} + , mMustMatchColorType(mustMatchColorType) + , mDesiredSubset(getSourceBoundsForUpsample(desiredSubset)) {} RecyclingClippingPixelAllocator::~RecyclingClippingPixelAllocator() {} @@ -668,7 +671,8 @@ bool RecyclingClippingPixelAllocator::allocPixelRef(SkBitmap* bitmap) { const SkImageInfo maxInfo = bitmap->info().makeWH(maxWidth, maxHeight); const size_t rowBytes = maxInfo.minRowBytes(); const size_t bytesNeeded = maxInfo.computeByteSize(rowBytes); - if (bytesNeeded <= mRecycledBytes) { + + if (!mDesiredSubset && bytesNeeded <= mRecycledBytes) { // Here we take advantage of reconfigure() to reset the rowBytes // of mRecycledBitmap. It is very important that we pass in // mRecycledBitmap->info() for the SkImageInfo. According to the @@ -712,20 +716,31 @@ void RecyclingClippingPixelAllocator::copyIfNecessary() { if (mNeedsCopy) { mRecycledBitmap->ref(); android::Bitmap* recycledPixels = mRecycledBitmap; - void* dst = recycledPixels->pixels(); - const size_t dstRowBytes = mRecycledBitmap->rowBytes(); - const size_t bytesToCopy = std::min(mRecycledBitmap->info().minRowBytes(), - mSkiaBitmap->info().minRowBytes()); - const int rowsToCopy = std::min(mRecycledBitmap->info().height(), - mSkiaBitmap->info().height()); - for (int y = 0; y < rowsToCopy; y++) { - memcpy(dst, mSkiaBitmap->getAddr(0, y), bytesToCopy); - // Cast to bytes in order to apply the dstRowBytes offset correctly. - dst = reinterpret_cast<void*>( - reinterpret_cast<uint8_t*>(dst) + dstRowBytes); + if (mDesiredSubset) { + recycledPixels->setAlphaType(mSkiaBitmap->alphaType()); + recycledPixels->setColorSpace(mSkiaBitmap->refColorSpace()); + + auto canvas = SkCanvas(recycledPixels->getSkBitmap()); + SkRect destination = SkRect::Make(recycledPixels->info().bounds()); + destination.intersect(SkRect::Make(mSkiaBitmap->info().bounds())); + canvas.drawImageRect(mSkiaBitmap->asImage(), *mDesiredSubset, destination, + SkSamplingOptions(SkFilterMode::kLinear), nullptr, + SkCanvas::kFast_SrcRectConstraint); + } else { + void* dst = recycledPixels->pixels(); + const size_t dstRowBytes = mRecycledBitmap->rowBytes(); + const size_t bytesToCopy = std::min(mRecycledBitmap->info().minRowBytes(), + mSkiaBitmap->info().minRowBytes()); + const int rowsToCopy = + std::min(mRecycledBitmap->info().height(), mSkiaBitmap->info().height()); + for (int y = 0; y < rowsToCopy; y++) { + memcpy(dst, mSkiaBitmap->getAddr(0, y), bytesToCopy); + // Cast to bytes in order to apply the dstRowBytes offset correctly. + dst = reinterpret_cast<void*>(reinterpret_cast<uint8_t*>(dst) + dstRowBytes); + } + recycledPixels->setAlphaType(mSkiaBitmap->alphaType()); + recycledPixels->setColorSpace(mSkiaBitmap->refColorSpace()); } - recycledPixels->setAlphaType(mSkiaBitmap->alphaType()); - recycledPixels->setColorSpace(mSkiaBitmap->refColorSpace()); recycledPixels->notifyPixelsChanged(); recycledPixels->unref(); } @@ -733,6 +748,20 @@ void RecyclingClippingPixelAllocator::copyIfNecessary() { mSkiaBitmap = nullptr; } +std::optional<SkRect> RecyclingClippingPixelAllocator::getSourceBoundsForUpsample( + std::optional<SkRect> subset) { + if (!uirenderer::Properties::resampleGainmapRegions || !subset || subset->isEmpty()) { + return std::nullopt; + } + + if (subset->left() == floor(subset->left()) && subset->top() == floor(subset->top()) && + subset->right() == floor(subset->right()) && subset->bottom() == floor(subset->bottom())) { + return std::nullopt; + } + + return subset; +} + //////////////////////////////////////////////////////////////////////////////// AshmemPixelAllocator::AshmemPixelAllocator(JNIEnv *env) { diff --git a/libs/hwui/jni/GraphicsJNI.h b/libs/hwui/jni/GraphicsJNI.h index b0a1074d6693..4b08f8dc7a93 100644 --- a/libs/hwui/jni/GraphicsJNI.h +++ b/libs/hwui/jni/GraphicsJNI.h @@ -216,8 +216,8 @@ private: */ class RecyclingClippingPixelAllocator : public android::skia::BRDAllocator { public: - RecyclingClippingPixelAllocator(android::Bitmap* recycledBitmap, - bool mustMatchColorType = true); + RecyclingClippingPixelAllocator(android::Bitmap* recycledBitmap, bool mustMatchColorType = true, + std::optional<SkRect> desiredSubset = std::nullopt); ~RecyclingClippingPixelAllocator(); @@ -241,11 +241,24 @@ public: SkCodec::ZeroInitialized zeroInit() const override { return SkCodec::kNo_ZeroInitialized; } private: + /** + * Optionally returns a subset rectangle that we need to upsample from. + * E.g., a gainmap subset may be decoded in a slightly larger rectangle + * than is needed (in order to correctly preserve gainmap alignment when + * rendering at display time), so we need to re-sample the "intended" + * gainmap back up to the bitmap dimensions. + * + * If we don't need to upsample from a subregion, then returns an empty + * optional + */ + static std::optional<SkRect> getSourceBoundsForUpsample(std::optional<SkRect> subset); + android::Bitmap* mRecycledBitmap; const size_t mRecycledBytes; SkBitmap* mSkiaBitmap; bool mNeedsCopy; const bool mMustMatchColorType; + const std::optional<SkRect> mDesiredSubset; }; class AshmemPixelAllocator : public SkBitmap::Allocator { diff --git a/native/android/surface_control_input_receiver.cpp b/native/android/surface_control_input_receiver.cpp index a84ec7309a62..7caa943c3e60 100644 --- a/native/android/surface_control_input_receiver.cpp +++ b/native/android/surface_control_input_receiver.cpp @@ -41,7 +41,7 @@ public: const sp<IBinder>& clientToken, const sp<InputTransferToken>& inputTransferToken, AInputReceiverCallbacks* callbacks) : mCallbacks(callbacks), - mInputConsumer(inputChannel, looper, *this), + mInputConsumer(inputChannel, looper, *this, nullptr), mClientToken(clientToken), mInputTransferToken(inputTransferToken) {} 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/PackageInstaller/TEST_MAPPING b/packages/PackageInstaller/TEST_MAPPING index b3fb1e7b3034..ff836104f799 100644 --- a/packages/PackageInstaller/TEST_MAPPING +++ b/packages/PackageInstaller/TEST_MAPPING @@ -28,6 +28,17 @@ }, { "name": "CtsIntentSignatureTestCases" + }, + { + "name": "CtsPackageInstallerCUJTestCases", + "options":[ + { + "exclude-annotation":"androidx.test.filters.FlakyTest" + }, + { + "exclude-annotation":"org.junit.Ignore" + } + ] } ] } diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt index e2ab31662380..c61a2ac9f0dd 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt @@ -16,7 +16,9 @@ package com.android.packageinstaller.v2.ui -import android.app.Activity +import android.app.Activity.RESULT_CANCELED +import android.app.Activity.RESULT_FIRST_USER +import android.app.Activity.RESULT_OK import android.app.AppOpsManager import android.content.ActivityNotFoundException import android.content.Intent @@ -135,7 +137,7 @@ class InstallLaunch : FragmentActivity(), InstallActionListener { } InstallAborted.ABORT_REASON_POLICY -> showPolicyRestrictionDialog(aborted) - else -> setResult(Activity.RESULT_CANCELED, null, true) + else -> setResult(RESULT_CANCELED, null, true) } } @@ -169,7 +171,7 @@ class InstallLaunch : FragmentActivity(), InstallActionListener { val success = installStage as InstallSuccess if (success.shouldReturnResult) { val successIntent = success.resultIntent - setResult(Activity.RESULT_OK, successIntent, true) + setResult(RESULT_OK, successIntent, true) } else { val successDialog = InstallSuccessFragment(success) showDialogInner(successDialog) @@ -180,7 +182,7 @@ class InstallLaunch : FragmentActivity(), InstallActionListener { val failed = installStage as InstallFailed if (failed.shouldReturnResult) { val failureIntent = failed.resultIntent - setResult(Activity.RESULT_FIRST_USER, failureIntent, true) + setResult(RESULT_FIRST_USER, failureIntent, true) } else { val failureDialog = InstallFailedFragment(failed) showDialogInner(failureDialog) @@ -219,7 +221,7 @@ class InstallLaunch : FragmentActivity(), InstallActionListener { shouldFinish = blockedByPolicyDialog == null showDialogInner(blockedByPolicyDialog) } - setResult(Activity.RESULT_CANCELED, null, shouldFinish) + setResult(RESULT_CANCELED, null, shouldFinish) } /** @@ -257,6 +259,10 @@ class InstallLaunch : FragmentActivity(), InstallActionListener { fun setResult(resultCode: Int, data: Intent?, shouldFinish: Boolean) { super.setResult(resultCode, data) + if (resultCode != RESULT_OK) { + // Let callers know that the install was cancelled + installViewModel!!.cleanupInstall() + } if (shouldFinish) { finish() } @@ -282,7 +288,7 @@ class InstallLaunch : FragmentActivity(), InstallActionListener { if (stageCode == InstallStage.STAGE_USER_ACTION_REQUIRED) { installViewModel!!.cleanupInstall() } - setResult(Activity.RESULT_CANCELED, null, true) + setResult(RESULT_CANCELED, null, true) } override fun onNegativeResponse(resultCode: Int, data: Intent?) { @@ -318,7 +324,7 @@ class InstallLaunch : FragmentActivity(), InstallActionListener { if (localLogv) { Log.d(LOG_TAG, "Opening $intent") } - setResult(Activity.RESULT_OK, intent, true) + setResult(RESULT_OK, intent, true) if (intent != null && intent.hasCategory(Intent.CATEGORY_LAUNCHER)) { startActivity(intent) } diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout-v31/non_collapsing_toolbar_base_layout.xml b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout-v31/non_collapsing_toolbar_base_layout.xml new file mode 100644 index 000000000000..1e48443fcf13 --- /dev/null +++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout-v31/non_collapsing_toolbar_base_layout.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + 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. +--> +<androidx.coordinatorlayout.widget.CoordinatorLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/content_parent" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:fitsSystemWindows="true"> + + <include layout="@layout/non_collapsing_toolbar_content_layout"/> +</androidx.coordinatorlayout.widget.CoordinatorLayout> diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout-v31/non_collapsing_toolbar_content_layout.xml b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout-v31/non_collapsing_toolbar_content_layout.xml new file mode 100644 index 000000000000..33519cba2940 --- /dev/null +++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout-v31/non_collapsing_toolbar_content_layout.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + 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. +--> +<merge + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + <com.google.android.material.appbar.AppBarLayout + android:id="@+id/app_bar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:fitsSystemWindows="true" + android:outlineAmbientShadowColor="@android:color/transparent" + android:outlineSpotShadowColor="@android:color/transparent" + android:background="@android:color/transparent" + android:theme="@style/Theme.CollapsingToolbar.Settings"> + + <Toolbar + android:id="@+id/action_bar" + android:layout_width="match_parent" + android:layout_height="?attr/actionBarSize" + android:theme="?android:attr/actionBarTheme" + android:transitionName="shared_element_view" + app:layout_collapseMode="pin"/> + </com.google.android.material.appbar.AppBarLayout> + + <FrameLayout + android:id="@+id/content_frame" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:layout_behavior="@string/appbar_scrolling_view_behavior"/> +</merge> diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarAppCompatActivity.java b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarAppCompatActivity.java index 465905170347..f46f110e65b8 100644 --- a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarAppCompatActivity.java +++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarAppCompatActivity.java @@ -171,7 +171,7 @@ public class CollapsingToolbarAppCompatActivity extends AppCompatActivity { private CollapsingToolbarDelegate getToolbarDelegate() { if (mToolbardelegate == null) { - mToolbardelegate = new CollapsingToolbarDelegate(new DelegateCallback()); + mToolbardelegate = new CollapsingToolbarDelegate(new DelegateCallback(), true); } return mToolbardelegate; } diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarBaseActivity.java b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarBaseActivity.java index 3965303d3ba5..16ed5a8079fc 100644 --- a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarBaseActivity.java +++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarBaseActivity.java @@ -169,7 +169,7 @@ public class CollapsingToolbarBaseActivity extends FragmentActivity { private CollapsingToolbarDelegate getToolbarDelegate() { if (mToolbardelegate == null) { - mToolbardelegate = new CollapsingToolbarDelegate(new DelegateCallback()); + mToolbardelegate = new CollapsingToolbarDelegate(new DelegateCallback(), true); } return mToolbardelegate; } diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarBaseFragment.java b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarBaseFragment.java index b605074f72c8..da97c305ea51 100644 --- a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarBaseFragment.java +++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarBaseFragment.java @@ -57,7 +57,8 @@ public abstract class CollapsingToolbarBaseFragment extends Fragment { @Override public void onAttach(Context context) { super.onAttach(context); - mToolbardelegate = new CollapsingToolbarDelegate(new DelegateCallback()); + mToolbardelegate = + new CollapsingToolbarDelegate(new DelegateCallback(), useCollapsingToolbar()); } @Nullable @@ -98,4 +99,8 @@ public abstract class CollapsingToolbarBaseFragment extends Fragment { public FrameLayout getContentFrameLayout() { return mToolbardelegate.getContentFrameLayout(); } + + protected boolean useCollapsingToolbar() { + return true; + } } diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarDelegate.java b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarDelegate.java index b63333719334..2ab2abd03c87 100644 --- a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarDelegate.java +++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarDelegate.java @@ -21,6 +21,8 @@ import static android.text.Layout.HYPHENATION_FREQUENCY_NORMAL_FAST; import android.app.ActionBar; import android.app.Activity; import android.content.res.Configuration; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; import android.graphics.text.LineBreakConfig; import android.os.Build; import android.util.Log; @@ -80,8 +82,12 @@ public class CollapsingToolbarDelegate { @NonNull private final HostCallback mHostCallback; - public CollapsingToolbarDelegate(@NonNull HostCallback hostCallback) { + private boolean mUseCollapsingToolbar; + + public CollapsingToolbarDelegate(@NonNull HostCallback hostCallback, + boolean useCollapsingToolbar) { mHostCallback = hostCallback; + mUseCollapsingToolbar = useCollapsingToolbar; } /** Method to call that creates the root view of the collapsing toolbar. */ @@ -94,13 +100,32 @@ public class CollapsingToolbarDelegate { @SuppressWarnings("RestrictTo") View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, Activity activity) { - final View view = - inflater.inflate(R.layout.collapsing_toolbar_base_layout, container, false); + int layoutId; + boolean useCollapsingToolbar = + mUseCollapsingToolbar || Build.VERSION.SDK_INT < Build.VERSION_CODES.S; + if (useCollapsingToolbar) { + layoutId = R.layout.collapsing_toolbar_base_layout; + } else { + layoutId = R.layout.non_collapsing_toolbar_base_layout; + } + final View view = inflater.inflate(layoutId, container, false); if (view instanceof CoordinatorLayout) { mCoordinatorLayout = (CoordinatorLayout) view; } mCollapsingToolbarLayout = view.findViewById(R.id.collapsing_toolbar); mAppBarLayout = view.findViewById(R.id.app_bar); + + if (!useCollapsingToolbar) { + // In the non-collapsing toolbar layout, we need to set the background of the app bar to + // the same as the activity background so that it covers the items extending above the + // bounds of the list for edge-to-edge. + TypedArray ta = container.getContext().obtainStyledAttributes(new int[] { + android.R.attr.windowBackground}); + Drawable background = ta.getDrawable(0); + ta.recycle(); + mAppBarLayout.setBackground(background); + } + if (mCollapsingToolbarLayout != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { mCollapsingToolbarLayout.setLineSpacingMultiplier(TOOLBAR_LINE_SPACING_MULTIPLIER); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 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..c6eb9fddf2a7 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java @@ -178,7 +178,7 @@ public class CsipDeviceManager { } log("updateRelationshipOfGroupDevices: mCachedDevices list =" + mCachedDevices.toString()); - // Get the preferred main device by getPreferredMainDeviceWithoutConectionState + // Get the preferred main device by getPreferredMainDeviceWithoutConnectionState List<CachedBluetoothDevice> groupDevicesList = getGroupDevicesFromAllOfDevicesList(groupId); CachedBluetoothDevice preferredMainDevice = getPreferredMainDevice(groupId, groupDevicesList); @@ -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); @@ -373,6 +373,7 @@ public class CsipDeviceManager { preferredMainDevice.addMemberDevice(deviceItem); mCachedDevices.remove(deviceItem); mBtManager.getEventManager().dispatchDeviceRemoved(deviceItem); + preferredMainDevice.refresh(); hasChanged = true; } } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java index 27fcdbe0334f..26905b1d86d2 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java @@ -80,6 +80,7 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { "com.android.settings.action.BLUETOOTH_LE_AUDIO_SHARING_STATE_CHANGE"; public static final String EXTRA_LE_AUDIO_SHARING_STATE = "BLUETOOTH_LE_AUDIO_SHARING_STATE"; public static final String EXTRA_BLUETOOTH_DEVICE = "BLUETOOTH_DEVICE"; + public static final String EXTRA_START_LE_AUDIO_SHARING = "START_LE_AUDIO_SHARING"; public static final int BROADCAST_STATE_UNKNOWN = 0; public static final int BROADCAST_STATE_ON = 1; public static final int BROADCAST_STATE_OFF = 2; diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepository.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepository.kt index 9ff5c438e32a..326bb31bdb9f 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepository.kt +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepository.kt @@ -19,37 +19,39 @@ package com.android.settingslib.bluetooth.devicesettings.data.repository import android.bluetooth.BluetoothAdapter import android.content.Context import com.android.settingslib.bluetooth.CachedBluetoothDevice +import com.android.settingslib.bluetooth.devicesettings.ActionSwitchPreference import com.android.settingslib.bluetooth.devicesettings.DeviceSetting import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId -import com.android.settingslib.bluetooth.devicesettings.DeviceSettingPreferenceState +import com.android.settingslib.bluetooth.devicesettings.DeviceSettingItem import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsConfig -import java.util.concurrent.ConcurrentHashMap +import com.android.settingslib.bluetooth.devicesettings.MultiTogglePreference +import com.android.settingslib.bluetooth.devicesettings.ToggleInfo +import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel +import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigModel +import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingModel +import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel +import com.android.settingslib.bluetooth.devicesettings.shared.model.ToggleModel +import com.google.common.cache.CacheBuilder +import com.google.common.cache.CacheLoader +import com.google.common.cache.LoadingCache import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch /** Provides functionality to control bluetooth device settings. */ interface DeviceSettingRepository { /** Gets config for the bluetooth device, returns null if failed. */ - suspend fun getDeviceSettingsConfig(cachedDevice: CachedBluetoothDevice): DeviceSettingsConfig? - - /** Gets all device settings for the bluetooth device. */ - fun getDeviceSettingList( - cachedDevice: CachedBluetoothDevice, - ): Flow<List<DeviceSetting>?> + suspend fun getDeviceSettingsConfig( + cachedDevice: CachedBluetoothDevice + ): DeviceSettingConfigModel? /** Gets device setting for the bluetooth device. */ fun getDeviceSetting( cachedDevice: CachedBluetoothDevice, @DeviceSettingId settingId: Int - ): Flow<DeviceSetting?> - - /** Updates device setting for the bluetooth device. */ - suspend fun updateDeviceSettingState( - cachedDevice: CachedBluetoothDevice, - @DeviceSettingId deviceSettingId: Int, - deviceSettingPreferenceState: DeviceSettingPreferenceState, - ) + ): Flow<DeviceSettingModel?> } class DeviceSettingRepositoryImpl( @@ -58,40 +60,94 @@ class DeviceSettingRepositoryImpl( private val coroutineScope: CoroutineScope, private val backgroundCoroutineContext: CoroutineContext, ) : DeviceSettingRepository { - private val deviceSettings = - ConcurrentHashMap<CachedBluetoothDevice, DeviceSettingServiceConnection>() + private val connectionCache: + LoadingCache<CachedBluetoothDevice, DeviceSettingServiceConnection> = + CacheBuilder.newBuilder() + .weakValues() + .build( + object : CacheLoader<CachedBluetoothDevice, DeviceSettingServiceConnection>() { + override fun load( + cachedDevice: CachedBluetoothDevice + ): DeviceSettingServiceConnection = + DeviceSettingServiceConnection( + cachedDevice, + context, + bluetoothAdaptor, + coroutineScope, + backgroundCoroutineContext, + ) + } + ) override suspend fun getDeviceSettingsConfig( cachedDevice: CachedBluetoothDevice - ): DeviceSettingsConfig? = createConnectionIfAbsent(cachedDevice).getDeviceSettingsConfig() - - override fun getDeviceSettingList( - cachedDevice: CachedBluetoothDevice - ): Flow<List<DeviceSetting>?> = createConnectionIfAbsent(cachedDevice).getDeviceSettingList() + ): DeviceSettingConfigModel? = + connectionCache.get(cachedDevice).getDeviceSettingsConfig()?.toModel() override fun getDeviceSetting( cachedDevice: CachedBluetoothDevice, settingId: Int - ): Flow<DeviceSetting?> = createConnectionIfAbsent(cachedDevice).getDeviceSetting(settingId) + ): Flow<DeviceSettingModel?> = + connectionCache.get(cachedDevice).let { connection -> + connection.getDeviceSetting(settingId).map { it?.toModel(cachedDevice, connection) } + } - override suspend fun updateDeviceSettingState( - cachedDevice: CachedBluetoothDevice, - @DeviceSettingId deviceSettingId: Int, - deviceSettingPreferenceState: DeviceSettingPreferenceState, - ) = - createConnectionIfAbsent(cachedDevice) - .updateDeviceSettings(deviceSettingId, deviceSettingPreferenceState) + private fun DeviceSettingsConfig.toModel(): DeviceSettingConfigModel = + DeviceSettingConfigModel( + mainItems = mainContentItems.map { it.toModel() }, + moreSettingsItems = moreSettingsItems.map { it.toModel() }, + moreSettingsPageFooter = moreSettingsFooter + ) - private fun createConnectionIfAbsent( - cachedDevice: CachedBluetoothDevice - ): DeviceSettingServiceConnection = - deviceSettings.computeIfAbsent(cachedDevice) { - DeviceSettingServiceConnection( - cachedDevice, - context, - bluetoothAdaptor, - coroutineScope, - backgroundCoroutineContext, - ) + private fun DeviceSettingItem.toModel(): DeviceSettingConfigItemModel = + DeviceSettingConfigItemModel(settingId) + + private fun DeviceSetting.toModel( + cachedDevice: CachedBluetoothDevice, + connection: DeviceSettingServiceConnection + ): DeviceSettingModel = + when (val pref = preference) { + is ActionSwitchPreference -> + DeviceSettingModel.ActionSwitchPreference( + cachedDevice = cachedDevice, + id = settingId, + title = pref.title, + summary = pref.summary, + icon = pref.icon, + isAllowedChangingState = pref.isAllowedChangingState, + intent = pref.intent, + switchState = + if (pref.hasSwitch()) { + DeviceSettingStateModel.ActionSwitchPreferenceState(pref.checked) + } else { + null + }, + updateState = { newState -> + coroutineScope.launch(backgroundCoroutineContext) { + connection.updateDeviceSettings( + settingId, + newState.toParcelable(), + ) + } + }, + ) + is MultiTogglePreference -> + DeviceSettingModel.MultiTogglePreference( + cachedDevice = cachedDevice, + id = settingId, + title = pref.title, + toggles = pref.toggleInfos.map { it.toModel() }, + isAllowedChangingState = pref.isAllowedChangingState, + isActive = true, + state = DeviceSettingStateModel.MultiTogglePreferenceState(pref.state), + updateState = { newState -> + coroutineScope.launch(backgroundCoroutineContext) { + connection.updateDeviceSettings(settingId, newState.toParcelable()) + } + }, + ) + else -> DeviceSettingModel.Unknown(cachedDevice, settingId) } + + private fun ToggleInfo.toModel(): ToggleModel = ToggleModel(label, icon) } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/shared/model/DeviceSettingConfigModel.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/shared/model/DeviceSettingConfigModel.kt new file mode 100644 index 000000000000..cd597ee65bce --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/shared/model/DeviceSettingConfigModel.kt @@ -0,0 +1,33 @@ +/* + * 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.settingslib.bluetooth.devicesettings.shared.model + +import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId + +/** Models a device setting config. */ +data class DeviceSettingConfigModel( + /** Items need to be shown in device details main page. */ + val mainItems: List<DeviceSettingConfigItemModel>, + /** Items need to be shown in device details more settings page. */ + val moreSettingsItems: List<DeviceSettingConfigItemModel>, + /** Footer text in more settings page. */ + val moreSettingsPageFooter: String) + +/** Models a device setting item in config. */ +data class DeviceSettingConfigItemModel( + @DeviceSettingId val settingId: Int, +) 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..64e503b323b8 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) { @@ -175,7 +174,6 @@ public class ZenModesBackend { mNotificationManager.setZenMode(Settings.Global.ZEN_MODE_OFF, null, TAG, /* fromUser= */ true); } else { - // TODO: b/333527800 - This should (potentially) snooze the rule if it was active. mNotificationManager.setAutomaticZenRuleState(mode.getId(), new Condition(mode.getRule().getConditionId(), "", Condition.STATE_FALSE, Condition.SOURCE_USER_ACTION)); diff --git a/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java b/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java index 69c7410818dd..6198d80cefe6 100644 --- a/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java +++ b/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java @@ -181,14 +181,14 @@ public class CreateUserDialogController { * admin status. */ public Dialog createDialog(Activity activity, - ActivityStarter activityStarter, boolean isMultipleAdminEnabled, + ActivityStarter activityStarter, boolean canCreateAdminUser, NewUserData successCallback, Runnable cancelCallback) { mActivity = activity; mCustomDialogHelper = new CustomDialogHelper(activity); mSuccessCallback = successCallback; mCancelCallback = cancelCallback; mActivityStarter = activityStarter; - addCustomViews(isMultipleAdminEnabled); + addCustomViews(canCreateAdminUser); mUserCreationDialog = mCustomDialogHelper.getDialog(); updateLayout(); mUserCreationDialog.setOnDismissListener(view -> finish()); @@ -197,19 +197,19 @@ public class CreateUserDialogController { return mUserCreationDialog; } - private void addCustomViews(boolean isMultipleAdminEnabled) { + private void addCustomViews(boolean canCreateAdminUser) { addGrantAdminView(); addUserInfoEditView(); mCustomDialogHelper.setPositiveButton(R.string.next, view -> { mCurrentState++; - if (mCurrentState == GRANT_ADMIN_DIALOG && !isMultipleAdminEnabled) { + if (mCurrentState == GRANT_ADMIN_DIALOG && !canCreateAdminUser) { mCurrentState++; } updateLayout(); }); mCustomDialogHelper.setNegativeButton(R.string.back, view -> { mCurrentState--; - if (mCurrentState == GRANT_ADMIN_DIALOG && !isMultipleAdminEnabled) { + if (mCurrentState == GRANT_ADMIN_DIALOG && !canCreateAdminUser) { mCurrentState--; } updateLayout(); diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt index c88c4c94b5fb..0e71116db6cc 100644 --- a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt +++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt @@ -25,6 +25,7 @@ import android.media.AudioManager.OnCommunicationDeviceChangedListener import android.provider.Settings import androidx.concurrent.futures.DirectExecutor import com.android.internal.util.ConcurrentUtils +import com.android.settingslib.volume.shared.AudioLogger import com.android.settingslib.volume.shared.AudioManagerEventsReceiver import com.android.settingslib.volume.shared.model.AudioManagerEvent import com.android.settingslib.volume.shared.model.AudioStream @@ -99,7 +100,7 @@ class AudioRepositoryImpl( private val contentResolver: ContentResolver, private val backgroundCoroutineContext: CoroutineContext, private val coroutineScope: CoroutineScope, - private val logger: Logger, + private val logger: AudioLogger, ) : AudioRepository { private val streamSettingNames: Map<AudioStream, String> = @@ -117,10 +118,10 @@ class AudioRepositoryImpl( override val mode: StateFlow<Int> = callbackFlow { - val listener = AudioManager.OnModeChangedListener { newMode -> trySend(newMode) } - audioManager.addOnModeChangedListener(ConcurrentUtils.DIRECT_EXECUTOR, listener) - awaitClose { audioManager.removeOnModeChangedListener(listener) } - } + val listener = AudioManager.OnModeChangedListener { newMode -> trySend(newMode) } + audioManager.addOnModeChangedListener(ConcurrentUtils.DIRECT_EXECUTOR, listener) + awaitClose { audioManager.removeOnModeChangedListener(listener) } + } .onStart { emit(audioManager.mode) } .flowOn(backgroundCoroutineContext) .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), audioManager.mode) @@ -140,14 +141,14 @@ class AudioRepositoryImpl( override val communicationDevice: StateFlow<AudioDeviceInfo?> get() = callbackFlow { - val listener = OnCommunicationDeviceChangedListener { trySend(Unit) } - audioManager.addOnCommunicationDeviceChangedListener( - ConcurrentUtils.DIRECT_EXECUTOR, - listener, - ) + val listener = OnCommunicationDeviceChangedListener { trySend(Unit) } + audioManager.addOnCommunicationDeviceChangedListener( + ConcurrentUtils.DIRECT_EXECUTOR, + listener, + ) - awaitClose { audioManager.removeOnCommunicationDeviceChangedListener(listener) } - } + awaitClose { audioManager.removeOnCommunicationDeviceChangedListener(listener) } + } .filterNotNull() .map { audioManager.communicationDevice } .onStart { emit(audioManager.communicationDevice) } @@ -160,15 +161,15 @@ class AudioRepositoryImpl( override fun getAudioStream(audioStream: AudioStream): Flow<AudioStreamModel> { return merge( - audioManagerEventsReceiver.events.filter { - if (it is StreamAudioManagerEvent) { - it.audioStream == audioStream - } else { - true - } - }, - volumeSettingChanges(audioStream), - ) + audioManagerEventsReceiver.events.filter { + if (it is StreamAudioManagerEvent) { + it.audioStream == audioStream + } else { + true + } + }, + volumeSettingChanges(audioStream), + ) .conflate() .map { getCurrentAudioStream(audioStream) } .onStart { emit(getCurrentAudioStream(audioStream)) } @@ -251,11 +252,4 @@ class AudioRepositoryImpl( awaitClose { contentResolver.unregisterContentObserver(observer) } } } - - interface Logger { - - fun onSetVolumeRequested(audioStream: AudioStream, volume: Int) - - fun onVolumeUpdateReceived(audioStream: AudioStream, model: AudioStreamModel) - } } diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSharingRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSharingRepository.kt index 7a66335ef22f..ebba7f152b90 100644 --- a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSharingRepository.kt +++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSharingRepository.kt @@ -34,6 +34,7 @@ import com.android.settingslib.bluetooth.onServiceStateChanged import com.android.settingslib.bluetooth.onSourceConnectedOrRemoved import com.android.settingslib.volume.data.repository.AudioSharingRepository.Companion.AUDIO_SHARING_VOLUME_MAX import com.android.settingslib.volume.data.repository.AudioSharingRepository.Companion.AUDIO_SHARING_VOLUME_MIN +import com.android.settingslib.volume.shared.AudioSharingLogger import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -50,6 +51,7 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.runningFold import kotlinx.coroutines.flow.stateIn @@ -90,6 +92,7 @@ class AudioSharingRepositoryImpl( private val btManager: LocalBluetoothManager, private val coroutineScope: CoroutineScope, private val backgroundCoroutineContext: CoroutineContext, + private val logger: AudioSharingLogger ) : AudioSharingRepository { private val isAudioSharingProfilesReady: StateFlow<Boolean> = btManager.profileManager.onServiceStateChanged @@ -104,6 +107,7 @@ class AudioSharingRepositoryImpl( btManager.profileManager.leAudioBroadcastProfile.onBroadcastStartedOrStopped .map { isBroadcasting() } .onStart { emit(isBroadcasting()) } + .onEach { logger.onAudioSharingStateChanged(it) } .flowOn(backgroundCoroutineContext) } else { flowOf(false) @@ -156,6 +160,7 @@ class AudioSharingRepositoryImpl( .map { getSecondaryGroupId() }, primaryGroupId.map { getSecondaryGroupId() }) .onStart { emit(getSecondaryGroupId()) } + .onEach { logger.onSecondaryGroupIdChanged(it) } .flowOn(backgroundCoroutineContext) .stateIn( coroutineScope, @@ -202,6 +207,7 @@ class AudioSharingRepositoryImpl( acc } } + .onEach { logger.onVolumeMapChanged(it) } .flowOn(backgroundCoroutineContext) } else { emptyFlow() @@ -220,6 +226,7 @@ class AudioSharingRepositoryImpl( BluetoothUtils.getSecondaryDeviceForBroadcast(contentResolver, btManager) if (cachedDevice != null) { it.setDeviceVolume(cachedDevice.device, volume, /* isGroupOp= */ true) + logger.onSetDeviceVolumeRequested(volume) } } } diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/shared/AudioLogger.kt b/packages/SettingsLib/src/com/android/settingslib/volume/shared/AudioLogger.kt new file mode 100644 index 000000000000..84f7fcbd8b96 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/volume/shared/AudioLogger.kt @@ -0,0 +1,27 @@ +/* + * 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.settingslib.volume.shared + +import com.android.settingslib.volume.shared.model.AudioStream +import com.android.settingslib.volume.shared.model.AudioStreamModel + +/** A log interface for audio streams volume events. */ +interface AudioLogger { + fun onSetVolumeRequested(audioStream: AudioStream, volume: Int) + + fun onVolumeUpdateReceived(audioStream: AudioStream, model: AudioStreamModel) +}
\ No newline at end of file diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/shared/AudioSharingLogger.kt b/packages/SettingsLib/src/com/android/settingslib/volume/shared/AudioSharingLogger.kt new file mode 100644 index 000000000000..18a4c6d1748d --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/volume/shared/AudioSharingLogger.kt @@ -0,0 +1,29 @@ +/* + * 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.settingslib.volume.shared + +/** A log interface for audio sharing volume events. */ +interface AudioSharingLogger { + + fun onAudioSharingStateChanged(state: Boolean) + + fun onSecondaryGroupIdChanged(groupId: Int) + + fun onVolumeMapChanged(map: Map<Int, Int>) + + fun onSetDeviceVolumeRequested(volume: Int) +}
\ No newline at end of file diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioSharingRepositoryTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioSharingRepositoryTest.kt index 078f0c8adba5..8c5a0851cc92 100644 --- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioSharingRepositoryTest.kt +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioSharingRepositoryTest.kt @@ -48,6 +48,7 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest +import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test @@ -101,7 +102,7 @@ class AudioSharingRepositoryTest { @Captor private lateinit var assistantCallbackCaptor: - ArgumentCaptor<BluetoothLeBroadcastAssistant.Callback> + ArgumentCaptor<BluetoothLeBroadcastAssistant.Callback> @Captor private lateinit var btCallbackCaptor: ArgumentCaptor<BluetoothCallback> @@ -110,6 +111,7 @@ class AudioSharingRepositoryTest { @Captor private lateinit var volumeCallbackCaptor: ArgumentCaptor<BluetoothVolumeControl.Callback> + private val logger = FakeAudioSharingRepositoryLogger() private val testScope = TestScope() private val context: Context = ApplicationProvider.getApplicationContext() @Spy private val contentResolver: ContentResolver = context.contentResolver @@ -135,16 +137,23 @@ class AudioSharingRepositoryTest { Settings.Secure.putInt( contentResolver, BluetoothUtils.getPrimaryGroupIdUriForBroadcast(), - TEST_GROUP_ID_INVALID) + TEST_GROUP_ID_INVALID + ) underTest = AudioSharingRepositoryImpl( contentResolver, btManager, testScope.backgroundScope, testScope.testScheduler, + logger ) } + @After + fun tearDown() { + logger.reset() + } + @Test fun audioSharingStateChange_profileReady_emitValues() { testScope.runTest { @@ -160,6 +169,13 @@ class AudioSharingRepositoryTest { runCurrent() Truth.assertThat(states).containsExactly(false, true, false, true) + Truth.assertThat(logger.logs) + .containsAtLeastElementsIn( + listOf( + "onAudioSharingStateChanged state=true", + "onAudioSharingStateChanged state=false", + ) + ).inOrder() } } @@ -187,7 +203,8 @@ class AudioSharingRepositoryTest { Truth.assertThat(groupIds) .containsExactly( TEST_GROUP_ID_INVALID, - TEST_GROUP_ID2) + TEST_GROUP_ID2 + ) } } @@ -219,13 +236,16 @@ class AudioSharingRepositoryTest { triggerSourceAdded() runCurrent() triggerProfileConnectionChange( - BluetoothAdapter.STATE_CONNECTING, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT) + BluetoothAdapter.STATE_CONNECTING, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT + ) runCurrent() triggerProfileConnectionChange( - BluetoothAdapter.STATE_DISCONNECTED, BluetoothProfile.LE_AUDIO) + BluetoothAdapter.STATE_DISCONNECTED, BluetoothProfile.LE_AUDIO + ) runCurrent() triggerProfileConnectionChange( - BluetoothAdapter.STATE_DISCONNECTED, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT) + BluetoothAdapter.STATE_DISCONNECTED, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT + ) runCurrent() Truth.assertThat(groupIds) @@ -235,7 +255,16 @@ class AudioSharingRepositoryTest { TEST_GROUP_ID1, TEST_GROUP_ID_INVALID, TEST_GROUP_ID2, - TEST_GROUP_ID_INVALID) + TEST_GROUP_ID_INVALID + ) + Truth.assertThat(logger.logs) + .containsAtLeastElementsIn( + listOf( + "onSecondaryGroupIdChanged groupId=$TEST_GROUP_ID_INVALID", + "onSecondaryGroupIdChanged groupId=$TEST_GROUP_ID2", + "onSecondaryGroupIdChanged groupId=$TEST_GROUP_ID1", + ) + ).inOrder() } } @@ -257,11 +286,22 @@ class AudioSharingRepositoryTest { verify(volumeControl).unregisterCallback(any()) runCurrent() + val expectedMap1 = mapOf(TEST_GROUP_ID1 to TEST_VOLUME1) + val expectedMap2 = mapOf(TEST_GROUP_ID1 to TEST_VOLUME2) Truth.assertThat(volumeMaps) .containsExactly( emptyMap<Int, Int>(), - mapOf(TEST_GROUP_ID1 to TEST_VOLUME1), - mapOf(TEST_GROUP_ID1 to TEST_VOLUME2)) + expectedMap1, + expectedMap2 + ) + Truth.assertThat(logger.logs) + .containsAtLeastElementsIn( + listOf( + "onVolumeMapChanged map={}", + "onVolumeMapChanged map=$expectedMap1", + "onVolumeMapChanged map=$expectedMap2", + ) + ).inOrder() } } @@ -281,12 +321,19 @@ class AudioSharingRepositoryTest { Settings.Secure.putInt( contentResolver, BluetoothUtils.getPrimaryGroupIdUriForBroadcast(), - TEST_GROUP_ID2) + TEST_GROUP_ID2 + ) `when`(assistant.allConnectedDevices).thenReturn(listOf(device1, device2)) underTest.setSecondaryVolume(TEST_VOLUME1) runCurrent() verify(volumeControl).setDeviceVolume(device1, TEST_VOLUME1, true) + Truth.assertThat(logger.logs) + .isEqualTo( + listOf( + "onSetVolumeRequested volume=$TEST_VOLUME1", + ) + ) } } @@ -313,7 +360,8 @@ class AudioSharingRepositoryTest { Settings.Secure.putInt( contentResolver, BluetoothUtils.getPrimaryGroupIdUriForBroadcast(), - TEST_GROUP_ID1) + TEST_GROUP_ID1 + ) `when`(assistant.allConnectedDevices).thenReturn(listOf(device1, device2)) assistantCallbackCaptor.value.sourceAdded(device1, receiveState) } @@ -324,7 +372,8 @@ class AudioSharingRepositoryTest { Settings.Secure.putInt( contentResolver, BluetoothUtils.getPrimaryGroupIdUriForBroadcast(), - TEST_GROUP_ID1) + TEST_GROUP_ID1 + ) assistantCallbackCaptor.value.sourceRemoved(device2) } @@ -334,7 +383,8 @@ class AudioSharingRepositoryTest { Settings.Secure.putInt( contentResolver, BluetoothUtils.getPrimaryGroupIdUriForBroadcast(), - TEST_GROUP_ID1) + TEST_GROUP_ID1 + ) btCallbackCaptor.value.onProfileConnectionStateChanged(cachedDevice2, state, profile) } @@ -343,12 +393,14 @@ class AudioSharingRepositoryTest { .registerContentObserver( eq(Settings.Secure.getUriFor(BluetoothUtils.getPrimaryGroupIdUriForBroadcast())), eq(false), - contentObserverCaptor.capture()) + contentObserverCaptor.capture() + ) `when`(assistant.allConnectedDevices).thenReturn(listOf(device1, device2)) Settings.Secure.putInt( contentResolver, BluetoothUtils.getPrimaryGroupIdUriForBroadcast(), - TEST_GROUP_ID2) + TEST_GROUP_ID2 + ) contentObserverCaptor.value.primaryChanged() } @@ -380,8 +432,9 @@ class AudioSharingRepositoryTest { onBroadcastStopped(TEST_REASON, TEST_BROADCAST_ID) } val sourceAdded: - BluetoothLeBroadcastAssistant.Callback.( - sink: BluetoothDevice, state: BluetoothLeBroadcastReceiveState) -> Unit = + BluetoothLeBroadcastAssistant.Callback.( + sink: BluetoothDevice, state: BluetoothLeBroadcastReceiveState + ) -> Unit = { sink, state -> onReceiveStateChanged(sink, TEST_SOURCE_ID, state) } diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/FakeAudioRepositoryLogger.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/FakeAudioRepositoryLogger.kt index 389bf5304262..bd573fb2c675 100644 --- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/FakeAudioRepositoryLogger.kt +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/FakeAudioRepositoryLogger.kt @@ -16,10 +16,11 @@ package com.android.settingslib.volume.data.repository +import com.android.settingslib.volume.shared.AudioLogger import com.android.settingslib.volume.shared.model.AudioStream import com.android.settingslib.volume.shared.model.AudioStreamModel -class FakeAudioRepositoryLogger : AudioRepositoryImpl.Logger { +class FakeAudioRepositoryLogger : AudioLogger { private val mutableLogs: MutableList<String> = mutableListOf() val logs: List<String> diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/FakeAudioSharingRepositoryLogger.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/FakeAudioSharingRepositoryLogger.kt new file mode 100644 index 000000000000..cc4cc8d4ab96 --- /dev/null +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/FakeAudioSharingRepositoryLogger.kt @@ -0,0 +1,46 @@ +/* + * 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.settingslib.volume.data.repository + +import com.android.settingslib.volume.shared.AudioSharingLogger +import java.util.concurrent.CopyOnWriteArrayList + +class FakeAudioSharingRepositoryLogger : AudioSharingLogger { + private val mutableLogs = CopyOnWriteArrayList<String>() + val logs: List<String> + get() = mutableLogs.toList() + + fun reset() { + mutableLogs.clear() + } + + override fun onAudioSharingStateChanged(state: Boolean) { + mutableLogs.add("onAudioSharingStateChanged state=$state") + } + + override fun onSecondaryGroupIdChanged(groupId: Int) { + mutableLogs.add("onSecondaryGroupIdChanged groupId=$groupId") + } + + override fun onVolumeMapChanged(map: GroupIdToVolumes) { + mutableLogs.add("onVolumeMapChanged map=$map") + } + + override fun onSetDeviceVolumeRequested(volume: Int) { + mutableLogs.add("onSetVolumeRequested volume=$volume") + } +}
\ No newline at end of file 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..698eb8159846 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 @@ -19,7 +19,9 @@ package com.android.settingslib.bluetooth; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.bluetooth.BluetoothClass; @@ -145,18 +147,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 +255,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( @@ -352,4 +354,34 @@ public class CsipDeviceManagerTest { assertThat(mCachedDevice1.getMemberDevice()).contains(mCachedDevice3); assertThat(mCachedDevice1.getDevice()).isEqualTo(expectedMainBluetoothDevice); } + + @Test + public void onProfileConnectionStateChangedIfProcessed_addMemberDevice_refreshUI() { + mCachedDevice3.setGroupId(GROUP1); + + mCsipDeviceManager.onProfileConnectionStateChangedIfProcessed(mCachedDevice3, + BluetoothProfile.STATE_CONNECTED); + + verify(mCachedDevice1).refresh(); + } + + @Test + public void onProfileConnectionStateChangedIfProcessed_switchMainDevice_refreshUI() { + when(mDevice3.isConnected()).thenReturn(true); + when(mDevice2.isConnected()).thenReturn(false); + when(mDevice1.isConnected()).thenReturn(false); + mCachedDevice3.setGroupId(GROUP1); + mCsipDeviceManager.onProfileConnectionStateChangedIfProcessed(mCachedDevice3, + BluetoothProfile.STATE_CONNECTED); + + when(mDevice3.isConnected()).thenReturn(false); + mCsipDeviceManager.onProfileConnectionStateChangedIfProcessed(mCachedDevice3, + BluetoothProfile.STATE_DISCONNECTED); + when(mDevice1.isConnected()).thenReturn(true); + mCsipDeviceManager.onProfileConnectionStateChangedIfProcessed(mCachedDevice1, + BluetoothProfile.STATE_CONNECTED); + + verify(mCachedDevice3).switchMemberDeviceContent(mCachedDevice1); + verify(mCachedDevice3, atLeastOnce()).refresh(); + } } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt index b5457c517604..fee23945f7b5 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt @@ -22,6 +22,7 @@ import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.ServiceConnection +import android.graphics.Bitmap import com.android.settingslib.bluetooth.CachedBluetoothDevice import com.android.settingslib.bluetooth.devicesettings.ActionSwitchPreference import com.android.settingslib.bluetooth.devicesettings.ActionSwitchPreferenceState @@ -34,6 +35,14 @@ import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsConfig import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsConfigProviderService import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsListener import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsProviderService +import com.android.settingslib.bluetooth.devicesettings.MultiTogglePreference +import com.android.settingslib.bluetooth.devicesettings.MultiTogglePreferenceState +import com.android.settingslib.bluetooth.devicesettings.ToggleInfo +import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel +import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigModel +import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingModel +import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel +import com.android.settingslib.bluetooth.devicesettings.shared.model.ToggleModel import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay @@ -148,7 +157,7 @@ class DeviceSettingRepositoryTest { val config = underTest.getDeviceSettingsConfig(cachedDevice) - assertThat(config).isSameInstanceAs(DEVICE_SETTING_CONFIG) + assertConfig(config!!, DEVICE_SETTING_CONFIG) } } @@ -163,7 +172,7 @@ class DeviceSettingRepositoryTest { ) .thenReturn("".toByteArray()) - var config: DeviceSettingsConfig? = null + var config: DeviceSettingConfigModel? = null val job = launch { config = underTest.getDeviceSettingsConfig(cachedDevice) } delay(1000) verify(bluetoothAdapter) @@ -185,7 +194,7 @@ class DeviceSettingRepositoryTest { .thenReturn(BLUETOOTH_DEVICE_METADATA.toByteArray()) job.join() - assertThat(config).isSameInstanceAs(DEVICE_SETTING_CONFIG) + assertConfig(config!!, DEVICE_SETTING_CONFIG) } } @@ -202,7 +211,7 @@ class DeviceSettingRepositoryTest { } @Test - fun getDeviceSettingList_success() { + fun getDeviceSetting_actionSwitchPreference_success() { testScope.runTest { `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG) `when`(settingProviderService1.registerDeviceSettingsListener(any(), any())).then { @@ -211,64 +220,63 @@ class DeviceSettingRepositoryTest { .getArgument<IDeviceSettingsListener>(1) .onDeviceSettingsChanged(listOf(DEVICE_SETTING_1)) } + var setting: DeviceSettingModel? = null + + underTest + .getDeviceSetting(cachedDevice, DeviceSettingId.DEVICE_SETTING_ID_HEADER) + .onEach { setting = it } + .launchIn(backgroundScope) + runCurrent() + + assertDeviceSetting(setting!!, DEVICE_SETTING_1) + } + } + + @Test + fun getDeviceSetting_multiTogglePreference_success() { + testScope.runTest { + `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG) `when`(settingProviderService2.registerDeviceSettingsListener(any(), any())).then { input -> input .getArgument<IDeviceSettingsListener>(1) .onDeviceSettingsChanged(listOf(DEVICE_SETTING_2)) } - var settings: List<DeviceSetting>? = null + var setting: DeviceSettingModel? = null underTest - .getDeviceSettingList(cachedDevice) - .onEach { settings = it } + .getDeviceSetting(cachedDevice, DeviceSettingId.DEVICE_SETTING_ID_ANC) + .onEach { setting = it } .launchIn(backgroundScope) runCurrent() - assertThat(settings?.map { it.settingId }) - .containsExactly( - DeviceSettingId.DEVICE_SETTING_ID_HEADER, - DeviceSettingId.DEVICE_SETTING_ID_ANC - ) - assertThat(settings?.map { (it.preference as ActionSwitchPreference).title }) - .containsExactly( - "title1", - "title2", - ) + assertDeviceSetting(setting!!, DEVICE_SETTING_2) } } @Test - fun getDeviceSetting_oneServiceFailed_returnPartialResult() { + fun getDeviceSetting_noConfig_returnNull() { testScope.runTest { - `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG) `when`(settingProviderService1.registerDeviceSettingsListener(any(), any())).then { input -> input .getArgument<IDeviceSettingsListener>(1) .onDeviceSettingsChanged(listOf(DEVICE_SETTING_1)) } - var settings: List<DeviceSetting>? = null + var setting: DeviceSettingModel? = null underTest - .getDeviceSettingList(cachedDevice) - .onEach { settings = it } + .getDeviceSetting(cachedDevice, DeviceSettingId.DEVICE_SETTING_ID_HEADER) + .onEach { setting = it } .launchIn(backgroundScope) runCurrent() - assertThat(settings?.map { it.settingId }) - .containsExactly( - DeviceSettingId.DEVICE_SETTING_ID_HEADER, - ) - assertThat(settings?.map { (it.preference as ActionSwitchPreference).title }) - .containsExactly( - "title1", - ) + assertThat(setting).isNull() } } @Test - fun getDeviceSetting_success() { + fun updateDeviceSettingState_switchState_success() { testScope.runTest { `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG) `when`(settingProviderService1.registerDeviceSettingsListener(any(), any())).then { @@ -277,48 +285,123 @@ class DeviceSettingRepositoryTest { .getArgument<IDeviceSettingsListener>(1) .onDeviceSettingsChanged(listOf(DEVICE_SETTING_1)) } - var setting: DeviceSetting? = null + var setting: DeviceSettingModel? = null underTest .getDeviceSetting(cachedDevice, DeviceSettingId.DEVICE_SETTING_ID_HEADER) .onEach { setting = it } .launchIn(backgroundScope) runCurrent() + val updateFunc = (setting as DeviceSettingModel.ActionSwitchPreference).updateState!! + updateFunc(DeviceSettingStateModel.ActionSwitchPreferenceState(false)) + runCurrent() - assertThat(setting?.settingId).isEqualTo(DeviceSettingId.DEVICE_SETTING_ID_HEADER) - assertThat((setting?.preference as ActionSwitchPreference).title).isEqualTo("title1") + verify(settingProviderService1) + .updateDeviceSettings( + DEVICE_INFO, + DeviceSettingState.Builder() + .setSettingId(DeviceSettingId.DEVICE_SETTING_ID_HEADER) + .setPreferenceState( + ActionSwitchPreferenceState.Builder().setChecked(false).build() + ) + .build() + ) } } @Test - fun updateDeviceSetting_success() { + fun updateDeviceSettingState_multiToggleState_success() { testScope.runTest { `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG) - `when`(settingProviderService1.registerDeviceSettingsListener(any(), any())).then { + `when`(settingProviderService2.registerDeviceSettingsListener(any(), any())).then { input -> input .getArgument<IDeviceSettingsListener>(1) - .onDeviceSettingsChanged(listOf(DEVICE_SETTING_1)) + .onDeviceSettingsChanged(listOf(DEVICE_SETTING_2)) } + var setting: DeviceSettingModel? = null - underTest.updateDeviceSettingState( - cachedDevice, - DeviceSettingId.DEVICE_SETTING_ID_HEADER, - ActionSwitchPreferenceState.Builder().build() - ) + underTest + .getDeviceSetting(cachedDevice, DeviceSettingId.DEVICE_SETTING_ID_ANC) + .onEach { setting = it } + .launchIn(backgroundScope) + runCurrent() + val updateFunc = (setting as DeviceSettingModel.MultiTogglePreference).updateState + updateFunc(DeviceSettingStateModel.MultiTogglePreferenceState(2)) runCurrent() - verify(settingProviderService1) + verify(settingProviderService2) .updateDeviceSettings( DEVICE_INFO, DeviceSettingState.Builder() - .setSettingId(DeviceSettingId.DEVICE_SETTING_ID_HEADER) - .setPreferenceState(ActionSwitchPreferenceState.Builder().build()) + .setSettingId(DeviceSettingId.DEVICE_SETTING_ID_ANC) + .setPreferenceState( + MultiTogglePreferenceState.Builder().setState(2).build() + ) .build() ) } } + private fun assertDeviceSetting(actual: DeviceSettingModel, serviceResponse: DeviceSetting) { + assertThat(actual.id).isEqualTo(serviceResponse.settingId) + when (actual) { + is DeviceSettingModel.ActionSwitchPreference -> { + assertThat(serviceResponse.preference) + .isInstanceOf(ActionSwitchPreference::class.java) + val pref = serviceResponse.preference as ActionSwitchPreference + assertThat(actual.title).isEqualTo(pref.title) + assertThat(actual.summary).isEqualTo(pref.summary) + assertThat(actual.icon).isEqualTo(pref.icon) + assertThat(actual.isAllowedChangingState).isEqualTo(pref.isAllowedChangingState) + if (pref.hasSwitch()) { + assertThat(actual.switchState!!.checked).isEqualTo(pref.checked) + } else { + assertThat(actual.switchState).isNull() + } + } + is DeviceSettingModel.MultiTogglePreference -> { + assertThat(serviceResponse.preference) + .isInstanceOf(MultiTogglePreference::class.java) + val pref = serviceResponse.preference as MultiTogglePreference + assertThat(actual.title).isEqualTo(pref.title) + assertThat(actual.isAllowedChangingState).isEqualTo(pref.isAllowedChangingState) + assertThat(actual.toggles.size).isEqualTo(pref.toggleInfos.size) + for (i in 0..<actual.toggles.size) { + assertToggle(actual.toggles[i], pref.toggleInfos[i]) + } + } + else -> {} + } + } + + private fun assertToggle(actual: ToggleModel, serviceResponse: ToggleInfo) { + assertThat(actual.label).isEqualTo(serviceResponse.label) + assertThat(actual.icon).isEqualTo(serviceResponse.icon) + } + + private fun assertConfig( + actual: DeviceSettingConfigModel, + serviceResponse: DeviceSettingsConfig + ) { + assertThat(actual.mainItems.size).isEqualTo(serviceResponse.mainContentItems.size) + for (i in 0..<actual.mainItems.size) { + assertConfigItem(actual.mainItems[i], serviceResponse.mainContentItems[i]) + } + assertThat(actual.moreSettingsItems.size).isEqualTo(serviceResponse.moreSettingsItems.size) + for (i in 0..<actual.moreSettingsItems.size) { + assertConfigItem(actual.moreSettingsItems[i], serviceResponse.moreSettingsItems[i]) + } + assertThat(actual.moreSettingsPageFooter).isEqualTo(serviceResponse.moreSettingsFooter) + } + + private fun assertConfigItem( + actual: DeviceSettingConfigItemModel, + serviceResponse: DeviceSettingItem + ) { + assertThat(actual.settingId).isEqualTo(serviceResponse.settingId) + } + private companion object { const val BLUETOOTH_ADDRESS = "12:34:56:78" const val CONFIG_SERVICE_PACKAGE_NAME = "com.android.fake.configservice" @@ -377,10 +460,21 @@ class DeviceSettingRepositoryTest { DeviceSetting.Builder() .setSettingId(DeviceSettingId.DEVICE_SETTING_ID_ANC) .setPreference( - ActionSwitchPreference.Builder() - .setTitle("title2") - .setHasSwitch(true) - .setAllowedChangingState(true) + MultiTogglePreference.Builder() + .setTitle("title1") + .setAllowChangingState(true) + .addToggleInfo( + ToggleInfo.Builder() + .setLabel("label1") + .setIcon(Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)) + .build() + ) + .addToggleInfo( + ToggleInfo.Builder() + .setLabel("label2") + .setIcon(Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)) + .build() + ) .build() ) .build() 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/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModesBackendTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModesBackendTest.java index 00c7ae3e97d7..539519b3ec3b 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModesBackendTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModesBackendTest.java @@ -123,7 +123,6 @@ public class ZenModesBackendTest { zenRule.id = id; zenRule.pkg = "package"; zenRule.enabled = azr.isEnabled(); - zenRule.snoozing = false; zenRule.conditionId = azr.getConditionId(); zenRule.condition = new Condition(azr.getConditionId(), "", active ? Condition.STATE_TRUE : Condition.STATE_FALSE, 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/src/com/android/providers/settings/device_config_service.aconfig b/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig index f53dec6dc713..b1e6d6650226 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig +++ b/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig @@ -28,6 +28,13 @@ flag { } flag { + name: "use_new_storage_value" + namespace: "core_experiments_team_internal" + description: "When enabled, read the new storage value in aconfig codegen, and actually use it." + bug: "312235596" +} + +flag { name: "load_apex_aconfig_protobufs" namespace: "core_experiments_team_internal" description: "When enabled, loads aconfig default values in apex flag protobufs into DeviceConfig on boot." 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/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 9f3c2bfb237a..1d9f46971502 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -482,6 +482,15 @@ android:exported="true" android:theme="@style/Theme.AppCompat.NoActionBar"> <intent-filter> + <action android:name="com.android.systemui.action.TOUCHPAD_TUTORIAL"/> + <category android:name="android.intent.category.DEFAULT"/> + </intent-filter> + </activity> + + <activity android:name=".inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity" + android:exported="true" + android:theme="@style/Theme.AppCompat.NoActionBar"> + <intent-filter> <action android:name="com.android.systemui.action.TOUCHPAD_KEYBOARD_TUTORIAL"/> <category android:name="android.intent.category.DEFAULT"/> </intent-filter> 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/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuOverlayLayout.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuOverlayLayout.java index 2e036e651d4e..6bea30fa8d08 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuOverlayLayout.java +++ b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuOverlayLayout.java @@ -348,7 +348,17 @@ public class A11yMenuOverlayLayout { /** Toggles a11y menu layout visibility. */ public void toggleVisibility() { - mLayout.setVisibility((mLayout.getVisibility() == View.VISIBLE) ? View.GONE : View.VISIBLE); + if (mLayout.getVisibility() == View.VISIBLE) { + mLayout.setVisibility(View.GONE); + } else { + if (Flags.hideRestrictedActions()) { + // Reconfigure the shortcut list in case the set of restricted actions has changed. + mA11yMenuViewPager.configureViewPagerAndFooter( + mLayout, createShortcutList(), getPageIndex()); + updateViewLayout(); + } + mLayout.setVisibility(View.VISIBLE); + } } /** Shows hint text on a minimal Snackbar-like text view. */ diff --git a/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java b/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java index d16617fdc8e5..4ab771be55f4 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java +++ b/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java @@ -542,8 +542,6 @@ public class AccessibilityMenuServiceTest { final Context context = sInstrumentation.getTargetContext(); final UserManager userManager = context.getSystemService(UserManager.class); userManager.setUserRestriction(restriction, isRestricted); - // Re-enable the service for the restriction to take effect. - enableA11yMenuService(context); } private static void unlockSignal() throws IOException { diff --git a/packages/SystemUI/aconfig/biometrics_framework.aconfig b/packages/SystemUI/aconfig/biometrics_framework.aconfig index e81d5d5ece5a..95e4b593a72f 100644 --- a/packages/SystemUI/aconfig/biometrics_framework.aconfig +++ b/packages/SystemUI/aconfig/biometrics_framework.aconfig @@ -3,9 +3,3 @@ container: "system" # NOTE: Keep alphabetized to help limit merge conflicts from multiple simultaneous editors. -flag { - name: "constraint_bp" - namespace: "biometrics_framework" - description: "Refactors Biometric Prompt to use a ConstraintLayout" - bug: "288175072" -} diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 3767a27c2e6f..03149928249b 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -156,17 +156,6 @@ flag { } flag { - name: "pss_app_selector_abrupt_exit_fix" - namespace: "systemui" - description: "Fixes the app selector abruptly disappearing without an animation, when the" - "selected task is the foreground task." - bug: "314385883" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { name: "pss_app_selector_recents_split_screen" namespace: "systemui" description: "Allows recent apps selected for partial screenshare to be launched in split screen mode" @@ -475,18 +464,6 @@ flag { } flag { - name: "centralized_status_bar_height_fix" - namespace: "systemui" - description: "Refactors shade header and keyguard status bar to read status bar dimens from a" - " central place, instead of reading resources directly. This is to take into account display" - " cutouts and other special cases. " - bug: "317016114" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { name: "enable_layout_tracing" namespace: "systemui" description: "Enables detailed traversal slices during measure and layout in perfetto traces" @@ -1209,6 +1186,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." @@ -1249,6 +1237,16 @@ flag { } flag { + name: "relock_with_power_button_immediately" + namespace: "systemui" + description: "UDFPS unlock followed by immediate power button push should relock" + bug: "343327511" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "lockscreen_preview_renderer_create_on_main_thread" namespace: "systemui" description: "Force preview renderer to be created on the main thread" @@ -1272,4 +1270,21 @@ flag { namespace: "systemui" description: "Adding haptic component infrastructure to sliders in Compose." bug: "341968766" +} + +flag { + name: "new_picker_ui" + namespace: "systemui" + description: "Enables the BC25 design of the customization picker UI." + bug: "339081035" +} + +flag { + namespace: "systemui" + name: "settings_ext_register_content_observer_on_bg_thread" + description: "Register content observer in callback flow APIs on background thread in SettingsProxyExt." + bug: "355389014" + metadata { + purpose: PURPOSE_BUGFIX + } }
\ No newline at end of file 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/RegisterContentObserverViaContentResolverDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterContentObserverViaContentResolverDetector.kt new file mode 100644 index 000000000000..8f5cdbf0b2bc --- /dev/null +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterContentObserverViaContentResolverDetector.kt @@ -0,0 +1,105 @@ +/* + * 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.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 +import org.jetbrains.uast.UClass +import org.jetbrains.uast.getParentOfType + +/** + * Checks if registerContentObserver/registerContentObserverAsUser/unregisterContentObserver is + * called on a ContentResolver (or subclasses), and directs the caller to using + * com.android.systemui.util.settings.SettingsProxy or its sub-classes. + */ +@Suppress("UnstableApiUsage") +class RegisterContentObserverViaContentResolverDetector : Detector(), SourceCodeScanner { + + override fun getApplicableMethodNames(): List<String> { + return CONTENT_RESOLVER_METHOD_LIST + } + + override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) { + val classQualifiedName = node.getParentOfType(UClass::class.java)?.qualifiedName + if (classQualifiedName in CLASSNAME_ALLOWLIST) { + // Don't warn for class we want the developers to use. + return + } + + val evaluator = context.evaluator + if (evaluator.isMemberInSubClassOf(method, "android.content.ContentResolver")) { + context.report( + issue = CONTENT_RESOLVER_ERROR, + location = context.getNameLocation(node), + message = + "`ContentResolver.${method.name}()` should be replaced with " + + "an appropriate interface API call, for eg. " + + "`<SettingsProxy>/<UserSettingsProxy>.${method.name}()`" + ) + } + } + + companion object { + @JvmField + val CONTENT_RESOLVER_ERROR: Issue = + Issue.create( + id = "RegisterContentObserverViaContentResolver", + briefDescription = + "Content observer registration done via `ContentResolver`" + + "instead of `SettingsProxy or child interfaces.`", + // lint trims indents and converts \ to line continuations + explanation = + """ + Use registerContentObserver/unregisterContentObserver methods in \ + `SettingsProxy`, `UserSettingsProxy` or `GlobalSettings` class instead of \ + using `ContentResolver.registerContentObserver` or \ + `ContentResolver.unregisterContentObserver`.""", + category = Category.PERFORMANCE, + priority = 10, + severity = Severity.ERROR, + implementation = + Implementation( + RegisterContentObserverViaContentResolverDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) + ) + + private val CLASSNAME_ALLOWLIST = + listOf( + "com.android.systemui.util.settings.SettingsProxy", + "com.android.systemui.util.settings.UserSettingsProxy", + "com.android.systemui.util.settings.GlobalSettings", + "com.android.systemui.util.settings.SecureSettings", + "com.android.systemui.util.settings.SystemSettings" + ) + + private val CONTENT_RESOLVER_METHOD_LIST = + listOf( + "registerContentObserver", + "registerContentObserverAsUser", + "unregisterContentObserver" + ) + } +} 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..a1f4f5507e5f 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,13 @@ class SystemUIIssueRegistry : IssueRegistry() { DemotingTestWithoutBugDetector.ISSUE, TestFunctionNameViolationDetector.ISSUE, MissingApacheLicenseDetector.ISSUE, + RegisterContentObserverSyncViaSettingsProxyDetector.SYNC_WARNING, + RegisterContentObserverViaContentResolverDetector.CONTENT_RESOLVER_ERROR ) 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/checks/tests/com/android/internal/systemui/lint/RegisterContentObserverViaContentResolverDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterContentObserverViaContentResolverDetectorTest.kt new file mode 100644 index 000000000000..1d33bce8dea8 --- /dev/null +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterContentObserverViaContentResolverDetectorTest.kt @@ -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.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 + +class RegisterContentObserverViaContentResolverDetectorTest : SystemUILintDetectorTest() { + + override fun getDetector(): Detector = RegisterContentObserverViaContentResolverDetector() + + override fun getIssues(): List<Issue> = + listOf(RegisterContentObserverViaContentResolverDetector.CONTENT_RESOLVER_ERROR) + + @Test + fun testRegisterContentObserver_throwError() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import android.content.Context; + + public class TestClass { + public void register(Context context) { + context.getContentResolver(). + registerContentObserver(Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED), + false, mSettingObserver); + } + } + """ + ) + .indented(), + *androidStubs + ) + .issues(RegisterContentObserverViaContentResolverDetector.CONTENT_RESOLVER_ERROR) + .run() + .expect( + """ + src/test/pkg/TestClass.java:7: Error: ContentResolver.registerContentObserver() should be replaced with an appropriate interface API call, for eg. <SettingsProxy>/<UserSettingsProxy>.registerContentObserver() [RegisterContentObserverViaContentResolver] + registerContentObserver(Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED), + ~~~~~~~~~~~~~~~~~~~~~~~ + 1 errors, 0 warnings + """ + .trimIndent() + ) + } + + @Test + fun testRegisterContentObserverForUser_throwError() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import android.content.Context; + + public class TestClass { + public void register(Context context) { + context.getContentResolver(). + registerContentObserverAsUser(Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED), + false, mSettingObserver); + } + } + """ + ) + .indented(), + *androidStubs + ) + .issues(RegisterContentObserverViaContentResolverDetector.CONTENT_RESOLVER_ERROR) + .run() + .expect( + """ + src/test/pkg/TestClass.java:7: Error: ContentResolver.registerContentObserverAsUser() should be replaced with an appropriate interface API call, for eg. <SettingsProxy>/<UserSettingsProxy>.registerContentObserverAsUser() [RegisterContentObserverViaContentResolver] + registerContentObserverAsUser(Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +1 errors, 0 warnings + """ + .trimIndent() + ) + } + + @Test + fun testSuppressRegisterContentObserver() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import android.content.Context; + + public class TestClass { + @SuppressWarnings("RegisterContentObserverViaContentResolver") + public void register(Context context) { + context.getContentResolver(). + registerContentObserver(Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED), + false, mSettingObserver); + } + } + """ + ) + .indented(), + *androidStubs + ) + .issues(RegisterContentObserverViaContentResolverDetector.CONTENT_RESOLVER_ERROR) + .run() + .expectClean() + } + + @Test + fun testRegisterContentObserverInSettingsProxy_allowed() { + lint() + .files( + TestFiles.java( + """ + package com.android.systemui.util.settings; + import android.content.Context; + + public class SettingsProxy { + public void register(Context context) { + context.getContentResolver(). + registerContentObserver(Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED), + false, mSettingObserver); + } + } + """ + ) + .indented(), + *androidStubs + ) + .issues(RegisterContentObserverViaContentResolverDetector.CONTENT_RESOLVER_ERROR) + .run() + .expectClean() + } + + @Test + fun testNoopIfNoCall() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import android.content.Context; + + public class SettingsProxy { + public void register(Context context) { + } + } + """ + ) + .indented(), + *androidStubs + ) + .issues(RegisterContentObserverViaContentResolverDetector.CONTENT_RESOLVER_ERROR) + .run() + .expectClean() + } + + @Test + fun testUnRegisterContentObserver_throwError() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import android.content.Context; + + public class TestClass { + public void register(Context context) { + context.getContentResolver(). + unregisterContentObserver(mSettingObserver); + } + } + """ + ) + .indented(), + *androidStubs + ) + .issues(RegisterContentObserverViaContentResolverDetector.CONTENT_RESOLVER_ERROR) + .run() + .expect( + """ + src/test/pkg/TestClass.java:7: Error: ContentResolver.unregisterContentObserver() should be replaced with an appropriate interface API call, for eg. <SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver() [RegisterContentObserverViaContentResolver] + unregisterContentObserver(mSettingObserver); + ~~~~~~~~~~~~~~~~~~~~~~~~~ + 1 errors, 0 warnings + """ + .trimIndent() + ) + } +} 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/AlternateBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/AlternateBouncer.kt new file mode 100644 index 000000000000..04bcc3624532 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/AlternateBouncer.kt @@ -0,0 +1,198 @@ +/* + * 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.ui.composable + +import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.Crossfade +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +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.Color +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.android.compose.modifiers.background +import com.android.compose.modifiers.height +import com.android.compose.modifiers.width +import com.android.systemui.deviceentry.shared.model.BiometricMessage +import com.android.systemui.deviceentry.ui.binder.UdfpsAccessibilityOverlayBinder +import com.android.systemui.deviceentry.ui.view.UdfpsAccessibilityOverlay +import com.android.systemui.deviceentry.ui.viewmodel.AlternateBouncerUdfpsAccessibilityOverlayViewModel +import com.android.systemui.keyguard.ui.binder.AlternateBouncerUdfpsViewBinder +import com.android.systemui.keyguard.ui.view.DeviceEntryIconView +import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerDependencies +import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerMessageAreaViewModel +import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerUdfpsIconViewModel +import com.android.systemui.res.R +import kotlinx.coroutines.ExperimentalCoroutinesApi + +@ExperimentalCoroutinesApi +@Composable +fun AlternateBouncer( + alternateBouncerDependencies: AlternateBouncerDependencies, + modifier: Modifier = Modifier, +) { + + val isVisible by + alternateBouncerDependencies.viewModel.isVisible.collectAsStateWithLifecycle( + initialValue = false + ) + + val udfpsIconLocation by + alternateBouncerDependencies.udfpsIconViewModel.iconLocation.collectAsStateWithLifecycle( + initialValue = null + ) + + // TODO (b/353955910): back handling doesn't work + BackHandler { alternateBouncerDependencies.viewModel.onBackRequested() } + + AnimatedVisibility( + visible = isVisible, + enter = fadeIn(), + exit = fadeOut(), + modifier = modifier, + ) { + Box( + contentAlignment = Alignment.TopCenter, + modifier = + Modifier.background(color = Colors.AlternateBouncerBackgroundColor, alpha = { 1f }) + .pointerInput(Unit) { + detectTapGestures( + onTap = { alternateBouncerDependencies.viewModel.onTapped() } + ) + }, + ) { + StatusMessage( + viewModel = alternateBouncerDependencies.messageAreaViewModel, + ) + } + + udfpsIconLocation?.let { udfpsLocation -> + Box { + DeviceEntryIcon( + viewModel = alternateBouncerDependencies.udfpsIconViewModel, + modifier = + Modifier.width { udfpsLocation.width } + .height { udfpsLocation.height } + .fillMaxHeight() + .offset { + IntOffset( + x = udfpsLocation.left, + y = udfpsLocation.top, + ) + }, + ) + } + + UdfpsA11yOverlay( + viewModel = alternateBouncerDependencies.udfpsAccessibilityOverlayViewModel.get(), + modifier = Modifier.fillMaxHeight(), + ) + } + } +} + +@ExperimentalCoroutinesApi +@Composable +private fun StatusMessage( + viewModel: AlternateBouncerMessageAreaViewModel, + modifier: Modifier = Modifier, +) { + val message: BiometricMessage? by + viewModel.message.collectAsStateWithLifecycle(initialValue = null) + + Crossfade( + targetState = message, + label = "Alternate Bouncer message", + animationSpec = tween(), + modifier = modifier, + ) { biometricMessage -> + biometricMessage?.let { + Text( + textAlign = TextAlign.Center, + text = it.message ?: "", + color = Colors.AlternateBouncerTextColor, + fontSize = 18.sp, + lineHeight = 24.sp, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(top = 92.dp), + ) + } + } +} + +@ExperimentalCoroutinesApi +@Composable +private fun DeviceEntryIcon( + viewModel: AlternateBouncerUdfpsIconViewModel, + modifier: Modifier = Modifier, +) { + AndroidView( + modifier = modifier, + factory = { context -> + val view = + DeviceEntryIconView(context, null).apply { + id = R.id.alternate_bouncer_udfps_icon_view + contentDescription = + context.resources.getString(R.string.accessibility_fingerprint_label) + } + AlternateBouncerUdfpsViewBinder.bind(view, viewModel) + view + }, + ) +} + +/** TODO (b/353955910): Validate accessibility CUJs */ +@ExperimentalCoroutinesApi +@Composable +private fun UdfpsA11yOverlay( + viewModel: AlternateBouncerUdfpsAccessibilityOverlayViewModel, + modifier: Modifier = Modifier, +) { + AndroidView( + factory = { context -> + val view = + UdfpsAccessibilityOverlay(context).apply { + id = R.id.alternate_bouncer_udfps_accessibility_overlay + } + UdfpsAccessibilityOverlayBinder.bind(view, viewModel) + view + }, + modifier = modifier, + ) +} + +private object Colors { + val AlternateBouncerBackgroundColor: Color = Color.Black.copy(alpha = .66f) + val AlternateBouncerTextColor: Color = Color.White +} 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/keyguard/ui/composable/section/NotificationSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt index f0f407a52243..4e117d6ff4db 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt @@ -23,19 +23,16 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.android.compose.animation.scene.SceneScope import com.android.compose.modifiers.thenIf -import com.android.systemui.Flags import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.ui.composable.modifier.burnInAware import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters import com.android.systemui.notifications.ui.composable.ConstrainedNotificationStack -import com.android.systemui.res.R import com.android.systemui.shade.LargeScreenHeaderHelper import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView @@ -97,11 +94,7 @@ constructor( } val splitShadeTopMargin: Dp = - if (Flags.centralizedStatusBarHeightFix()) { - LargeScreenHeaderHelper.getLargeScreenHeaderHeight(LocalContext.current).dp - } else { - dimensionResource(id = R.dimen.large_screen_shade_header_height) - } + LargeScreenHeaderHelper.getLargeScreenHeaderHeight(LocalContext.current).dp ConstrainedNotificationStack( stackScrollView = stackScrollView.get(), 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 52374f1e817b..16972bc95e57 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 @@ -144,7 +146,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, @@ -156,12 +159,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( @@ -169,7 +176,7 @@ constructor( ) = ShadeScene( notificationStackScrollView.get(), - viewModel = viewModel, + viewModel = rememberViewModel { contentViewModelFactory.create() }, notificationsPlaceholderViewModel = notificationsPlaceholderViewModel, createTintedIconManager = tintedIconManagerFactory::create, createBatteryMeterViewController = batteryMeterViewControllerFactory::create, @@ -195,7 +202,7 @@ constructor( @Composable private fun SceneScope.ShadeScene( notificationStackScrollView: NotificationScrollView, - viewModel: ShadeSceneViewModel, + viewModel: ShadeSceneContentViewModel, notificationsPlaceholderViewModel: NotificationsPlaceholderViewModel, createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager, createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController, @@ -241,7 +248,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, @@ -260,7 +267,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 = @@ -305,9 +312,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) { @@ -315,7 +322,7 @@ private fun SceneScope.SingleShade( }, ) { CollapsedShadeHeader( - viewModel = viewModel.shadeHeaderViewModel, + viewModelFactory = viewModel.shadeHeaderViewModelFactory, createTintedIconManager = createTintedIconManager, createBatteryMeterViewController = createBatteryMeterViewController, statusBarIconController = statusBarIconController, @@ -367,6 +374,8 @@ private fun SceneScope.SingleShade( maxScrimTop = { maxNotifScrimTop.value }, shadeMode = ShadeMode.Single, shouldPunchHoleBehindScrim = shouldPunchHoleBehindScrim, + onEmptySpaceClick = + viewModel::onEmptySpaceClicked.takeIf { isEmptySpaceClickable }, ) }, ) @@ -413,7 +422,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, @@ -474,8 +483,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, @@ -487,6 +498,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 } @@ -516,7 +528,7 @@ private fun SceneScope.SplitShade( modifier = Modifier.fillMaxSize(), ) { CollapsedShadeHeader( - viewModel = viewModel.shadeHeaderViewModel, + viewModelFactory = viewModel.shadeHeaderViewModelFactory, createTintedIconManager = createTintedIconManager, createBatteryMeterViewController = createBatteryMeterViewController, statusBarIconController = statusBarIconController, @@ -535,7 +547,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 @@ -606,6 +618,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/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelRoot.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelRoot.kt index 9da2a1b06f30..5ffb6f82fbba 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelRoot.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelRoot.kt @@ -50,7 +50,7 @@ fun VolumePanelRoot( ) { val accessibilityTitle = stringResource(R.string.accessibility_volume_settings) val state: VolumePanelState by viewModel.volumePanelState.collectAsStateWithLifecycle() - val components by viewModel.componentsLayout.collectAsStateWithLifecycle(null) + val components by viewModel.componentsLayout.collectAsStateWithLifecycle() with(VolumePanelComposeScope(state)) { components?.let { componentsState -> diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateContent.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateContent.kt new file mode 100644 index 000000000000..5eabd2275285 --- /dev/null +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateContent.kt @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.compose.animation.scene + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.AnimationVector1D +import androidx.compose.animation.core.SpringSpec +import com.android.compose.animation.scene.content.state.ContentState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch + +internal fun CoroutineScope.animateContent( + transition: ContentState.Transition<*>, + oneOffAnimation: OneOffAnimation, + targetProgress: Float, + startTransition: () -> Unit, + finishTransition: () -> Unit, +) { + // Start the transition. This will compute the TransformationSpec associated to [transition], + // which we need to initialize the Animatable that will actually animate it. + startTransition() + + // The transition now contains the transformation spec that we should use to instantiate the + // Animatable. + val animationSpec = transition.transformationSpec.progressSpec + val visibilityThreshold = + (animationSpec as? SpringSpec)?.visibilityThreshold ?: ProgressVisibilityThreshold + val replacedTransition = transition.replacedTransition + val initialProgress = replacedTransition?.progress ?: 0f + val initialVelocity = replacedTransition?.progressVelocity ?: 0f + val animatable = + Animatable(initialProgress, visibilityThreshold = visibilityThreshold).also { + oneOffAnimation.animatable = it + } + + // Animate the progress to its target value. + // + // Important: We start atomically to make sure that we start the coroutine even if it is + // cancelled right after it is launched, so that finishTransition() is correctly called. + // Otherwise, this transition will never be stopped and we will never settle to Idle. + oneOffAnimation.job = + launch(start = CoroutineStart.ATOMIC) { + try { + animatable.animateTo(targetProgress, animationSpec, initialVelocity) + } finally { + finishTransition() + } + } +} + +internal class OneOffAnimation { + /** + * The animatable used to animate this transition. + * + * Note: This is lateinit because we need to first create this object so that + * [SceneTransitionLayoutState] can compute the transformations and animation spec associated to + * the transition, which is needed to initialize this Animatable. + */ + lateinit var animatable: Animatable<Float, AnimationVector1D> + + /** The job that is animating [animatable]. */ + lateinit var job: Job + + val progress: Float + get() = animatable.value + + val progressVelocity: Float + get() = animatable.velocity + + fun finish(): Job = job +} + +// TODO(b/290184746): Compute a good default visibility threshold that depends on the layout size +// and screen density. +internal const val ProgressVisibilityThreshold = 1e-3f diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt index 1fc1f989b095..68a6c9836875 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt @@ -16,15 +16,10 @@ package com.android.compose.animation.scene -import androidx.compose.animation.core.Animatable -import androidx.compose.animation.core.AnimationVector1D -import androidx.compose.animation.core.SpringSpec import com.android.compose.animation.scene.content.state.TransitionState import kotlin.math.absoluteValue import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Job -import kotlinx.coroutines.launch /** * Transition to [target] using a canned animation. This function will try to be smart and take over @@ -50,7 +45,7 @@ internal fun CoroutineScope.animateToScene( return when (transitionState) { is TransitionState.Idle -> { - animate( + animateToScene( layoutState, target, transitionKey, @@ -80,13 +75,11 @@ internal fun CoroutineScope.animateToScene( } else { // The transition is in progress: start the canned animation at the same // progress as it was in. - animate( + animateToScene( layoutState, target, transitionKey, isInitiatedByUserInput, - initialProgress = progress, - initialVelocity = transitionState.progressVelocity, replacedTransition = transitionState, ) } @@ -102,13 +95,11 @@ internal fun CoroutineScope.animateToScene( layoutState.finishTransition(transitionState, target) null } else { - animate( + animateToScene( layoutState, target, transitionKey, isInitiatedByUserInput, - initialProgress = progress, - initialVelocity = transitionState.progressVelocity, reversed = true, replacedTransition = transitionState, ) @@ -140,7 +131,7 @@ internal fun CoroutineScope.animateToScene( animateToScene(layoutState, animateFrom, transitionKey = null) } - animate( + animateToScene( layoutState, target, transitionKey, @@ -154,103 +145,68 @@ internal fun CoroutineScope.animateToScene( } } -private fun CoroutineScope.animate( +private fun CoroutineScope.animateToScene( layoutState: MutableSceneTransitionLayoutStateImpl, targetScene: SceneKey, transitionKey: TransitionKey?, isInitiatedByUserInput: Boolean, replacedTransition: TransitionState.Transition?, - initialProgress: Float = 0f, - initialVelocity: Float = 0f, reversed: Boolean = false, fromScene: SceneKey = layoutState.transitionState.currentScene, chain: Boolean = true, ): TransitionState.Transition { + val oneOffAnimation = OneOffAnimation() val targetProgress = if (reversed) 0f else 1f val transition = if (reversed) { - OneOffTransition( + OneOffSceneTransition( key = transitionKey, fromScene = targetScene, toScene = fromScene, currentScene = targetScene, isInitiatedByUserInput = isInitiatedByUserInput, - isUserInputOngoing = false, replacedTransition = replacedTransition, + oneOffAnimation = oneOffAnimation, ) } else { - OneOffTransition( + OneOffSceneTransition( key = transitionKey, fromScene = fromScene, toScene = targetScene, currentScene = targetScene, isInitiatedByUserInput = isInitiatedByUserInput, - isUserInputOngoing = false, replacedTransition = replacedTransition, + oneOffAnimation = oneOffAnimation, ) } - // Change the current layout state to start this new transition. This will compute the - // TransformationSpec associated to this transition, which we need to initialize the Animatable - // that will actually animate it. - layoutState.startTransition(transition, chain) - - // The transition now contains the transformation spec that we should use to instantiate the - // Animatable. - val animationSpec = transition.transformationSpec.progressSpec - val visibilityThreshold = - (animationSpec as? SpringSpec)?.visibilityThreshold ?: ProgressVisibilityThreshold - val animatable = - Animatable(initialProgress, visibilityThreshold = visibilityThreshold).also { - transition.animatable = it - } - - // Animate the progress to its target value. - // Important: We start atomically to make sure that we start the coroutine even if it is - // cancelled right after it is launched, so that finishTransition() is correctly called. - // Otherwise, this transition will never be stopped and we will never settle to Idle. - transition.job = - launch(start = CoroutineStart.ATOMIC) { - try { - animatable.animateTo(targetProgress, animationSpec, initialVelocity) - } finally { - layoutState.finishTransition(transition, targetScene) - } - } + animateContent( + transition = transition, + oneOffAnimation = oneOffAnimation, + targetProgress = targetProgress, + startTransition = { layoutState.startTransition(transition, chain) }, + finishTransition = { layoutState.finishTransition(transition, targetScene) }, + ) return transition } -private class OneOffTransition( +private class OneOffSceneTransition( override val key: TransitionKey?, fromScene: SceneKey, toScene: SceneKey, override val currentScene: SceneKey, override val isInitiatedByUserInput: Boolean, - override val isUserInputOngoing: Boolean, replacedTransition: TransitionState.Transition?, + private val oneOffAnimation: OneOffAnimation, ) : TransitionState.Transition(fromScene, toScene, replacedTransition) { - /** - * The animatable used to animate this transition. - * - * Note: This is lateinit because we need to first create this Transition object so that - * [SceneTransitionLayoutState] can compute the transformations and animation spec associated to - * it, which is need to initialize this Animatable. - */ - lateinit var animatable: Animatable<Float, AnimationVector1D> - - /** The job that is animating [animatable]. */ - lateinit var job: Job - override val progress: Float - get() = animatable.value + get() = oneOffAnimation.progress override val progressVelocity: Float - get() = animatable.velocity + get() = oneOffAnimation.progressVelocity - override fun finish(): Job = job -} + override val isUserInputOngoing: Boolean = false -// TODO(b/290184746): Compute a good default visibility threshold that depends on the layout size -// and screen density. -internal const val ProgressVisibilityThreshold = 1e-3f + override fun finish(): Job = oneOffAnimation.finish() +} diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt index a43028a340f4..712fe6b2ff50 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt @@ -459,38 +459,11 @@ private class DragControllerImpl( animateTo(targetScene = targetScene, targetOffset = targetOffset) } else { - // We are doing an overscroll animation between scenes. In this case, we can also start - // from the idle position. - - val startFromIdlePosition = swipeTransition.dragOffset == 0f - - if (startFromIdlePosition) { - // If there is a target scene, we start the overscroll animation. - val result = swipes.findUserActionResultStrict(velocity) - if (result == null) { - // We will not animate - swipeTransition.snapToScene(fromScene.key) - return 0f - } - - val newSwipeTransition = - SwipeTransition( - layoutState = layoutState, - coroutineScope = draggableHandler.coroutineScope, - fromScene = fromScene, - result = result, - swipes = swipes, - layoutImpl = draggableHandler.layoutImpl, - orientation = draggableHandler.orientation, - ) - .apply { _currentScene = swipeTransition._currentScene } - - updateTransition(newSwipeTransition) - animateTo(targetScene = fromScene, targetOffset = 0f) - } else { - // We were between two scenes: animate to the initial scene. - animateTo(targetScene = fromScene, targetOffset = 0f) + // We are doing an overscroll preview animation between scenes. + check(fromScene == swipeTransition._currentScene) { + "canChangeScene is false but currentScene != fromScene" } + animateTo(targetScene = fromScene, targetOffset = 0f) } // The onStop animation consumes any remaining velocity. 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/src/com/android/compose/animation/scene/TransitionDsl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt index a30b78049213..79b38563b8f9 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt @@ -17,6 +17,8 @@ package com.android.compose.animation.scene import androidx.compose.animation.core.AnimationSpec +import androidx.compose.animation.core.Easing +import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.SpringSpec import androidx.compose.foundation.gestures.Orientation import androidx.compose.ui.geometry.Offset @@ -140,6 +142,7 @@ interface BaseTransitionBuilder : PropertyTransformationBuilder { fun fractionRange( start: Float? = null, end: Float? = null, + easing: Easing = LinearEasing, builder: PropertyTransformationBuilder.() -> Unit, ) } @@ -182,6 +185,7 @@ interface TransitionBuilder : BaseTransitionBuilder { fun timestampRange( startMillis: Int? = null, endMillis: Int? = null, + easing: Easing = LinearEasing, builder: PropertyTransformationBuilder.() -> Unit, ) diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt index 6515cb8f68ca..a63b19a0306f 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt @@ -18,6 +18,7 @@ package com.android.compose.animation.scene import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.DurationBasedAnimationSpec +import androidx.compose.animation.core.Easing import androidx.compose.animation.core.Spring import androidx.compose.animation.core.SpringSpec import androidx.compose.animation.core.VectorConverter @@ -163,9 +164,10 @@ internal abstract class BaseTransitionBuilderImpl : BaseTransitionBuilder { override fun fractionRange( start: Float?, end: Float?, + easing: Easing, builder: PropertyTransformationBuilder.() -> Unit ) { - range = TransformationRange(start, end) + range = TransformationRange(start, end, easing) builder() range = null } @@ -251,6 +253,7 @@ internal class TransitionBuilderImpl : BaseTransitionBuilderImpl(), TransitionBu override fun timestampRange( startMillis: Int?, endMillis: Int?, + easing: Easing, builder: PropertyTransformationBuilder.() -> Unit ) { if (startMillis != null && (startMillis < 0 || startMillis > durationMillis)) { @@ -263,7 +266,7 @@ internal class TransitionBuilderImpl : BaseTransitionBuilderImpl(), TransitionBu val start = startMillis?.let { it.toFloat() / durationMillis } val end = endMillis?.let { it.toFloat() / durationMillis } - fractionRange(start, end, builder) + fractionRange(start, end, easing, builder) } } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt index 77ec89161d43..eda8edeceeb9 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt @@ -16,6 +16,8 @@ package com.android.compose.animation.scene.transformation +import androidx.compose.animation.core.Easing +import androidx.compose.animation.core.LinearEasing import androidx.compose.ui.util.fastCoerceAtLeast import androidx.compose.ui.util.fastCoerceAtMost import androidx.compose.ui.util.fastCoerceIn @@ -90,11 +92,13 @@ internal class RangedPropertyTransformation<T>( data class TransformationRange( val start: Float, val end: Float, + val easing: Easing, ) { constructor( start: Float? = null, - end: Float? = null - ) : this(start ?: BoundUnspecified, end ?: BoundUnspecified) + end: Float? = null, + easing: Easing = LinearEasing, + ) : this(start ?: BoundUnspecified, end ?: BoundUnspecified, easing) init { require(!start.isSpecified() || (start in 0f..1f)) @@ -103,17 +107,20 @@ data class TransformationRange( } /** Reverse this range. */ - fun reversed() = TransformationRange(start = reverseBound(end), end = reverseBound(start)) + fun reversed() = + TransformationRange(start = reverseBound(end), end = reverseBound(start), easing = easing) /** Get the progress of this range given the global [transitionProgress]. */ fun progress(transitionProgress: Float): Float { - return when { - start.isSpecified() && end.isSpecified() -> - ((transitionProgress - start) / (end - start)).fastCoerceIn(0f, 1f) - !start.isSpecified() && !end.isSpecified() -> transitionProgress - end.isSpecified() -> (transitionProgress / end).fastCoerceAtMost(1f) - else -> ((transitionProgress - start) / (1f - start)).fastCoerceAtLeast(0f) - } + val progress = + when { + start.isSpecified() && end.isSpecified() -> + ((transitionProgress - start) / (end - start)).fastCoerceIn(0f, 1f) + !start.isSpecified() && !end.isSpecified() -> transitionProgress + end.isSpecified() -> (transitionProgress / end).fastCoerceAtMost(1f) + else -> ((transitionProgress - start) / (1f - start)).fastCoerceAtLeast(0f) + } + return easing.transform(progress) } private fun Float.isSpecified() = this != BoundUnspecified 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/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt index 68240b5337fe..bed6cefa459d 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt @@ -16,6 +16,7 @@ package com.android.compose.animation.scene +import androidx.compose.animation.core.CubicBezierEasing import androidx.compose.animation.core.SpringSpec import androidx.compose.animation.core.TweenSpec import androidx.compose.animation.core.spring @@ -107,6 +108,13 @@ class TransitionDslTest { fractionRange(start = 0.1f, end = 0.8f) { fade(TestElements.Foo) } fractionRange(start = 0.2f) { fade(TestElements.Foo) } fractionRange(end = 0.9f) { fade(TestElements.Foo) } + fractionRange( + start = 0.1f, + end = 0.8f, + easing = CubicBezierEasing(0.1f, 0.1f, 0f, 1f) + ) { + fade(TestElements.Foo) + } } } @@ -118,6 +126,11 @@ class TransitionDslTest { TransformationRange(start = 0.1f, end = 0.8f), TransformationRange(start = 0.2f, end = TransformationRange.BoundUnspecified), TransformationRange(start = TransformationRange.BoundUnspecified, end = 0.9f), + TransformationRange( + start = 0.1f, + end = 0.8f, + CubicBezierEasing(0.1f, 0.1f, 0f, 1f) + ), ) } @@ -130,6 +143,13 @@ class TransitionDslTest { timestampRange(startMillis = 100, endMillis = 300) { fade(TestElements.Foo) } timestampRange(startMillis = 200) { fade(TestElements.Foo) } timestampRange(endMillis = 400) { fade(TestElements.Foo) } + timestampRange( + startMillis = 100, + endMillis = 300, + easing = CubicBezierEasing(0.1f, 0.1f, 0f, 1f) + ) { + fade(TestElements.Foo) + } } } @@ -141,6 +161,11 @@ class TransitionDslTest { TransformationRange(start = 100 / 500f, end = 300 / 500f), TransformationRange(start = 200 / 500f, end = TransformationRange.BoundUnspecified), TransformationRange(start = TransformationRange.BoundUnspecified, end = 400 / 500f), + TransformationRange( + start = 100 / 500f, + end = 300 / 500f, + easing = CubicBezierEasing(0.1f, 0.1f, 0f, 1f) + ), ) } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/EasingTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/EasingTest.kt new file mode 100644 index 000000000000..07901f27388d --- /dev/null +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/EasingTest.kt @@ -0,0 +1,126 @@ +/* + * 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.compose.animation.scene.transformation + +import androidx.compose.animation.core.CubicBezierEasing +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.ui.Modifier +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.unit.dp +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.compose.animation.scene.TestElements +import com.android.compose.animation.scene.testTransition +import com.android.compose.test.assertSizeIsEqualTo +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class EasingTest { + @get:Rule val rule = createComposeRule() + + @Test + fun testFractionRangeEasing() { + val easing = CubicBezierEasing(0.1f, 0.1f, 0f, 1f) + rule.testTransition( + fromSceneContent = { Box(Modifier.size(100.dp).element(TestElements.Foo)) }, + toSceneContent = { Box(Modifier.size(100.dp).element(TestElements.Bar)) }, + transition = { + // Scale during 4 frames. + spec = tween(16 * 4, easing = LinearEasing) + fractionRange(easing = easing) { + scaleSize(TestElements.Foo, width = 0f, height = 0f) + scaleSize(TestElements.Bar, width = 0f, height = 0f) + } + }, + ) { + // Foo is entering, is 100dp x 100dp at rest and is scaled by (2, 0.5) during the + // transition so it starts at 200dp x 50dp. + before { onElement(TestElements.Bar).assertDoesNotExist() } + at(0) { + onElement(TestElements.Foo).assertSizeIsEqualTo(100.dp, 100.dp) + onElement(TestElements.Bar).assertSizeIsEqualTo(0.dp, 0.dp) + } + at(16) { + // 25% linear progress is mapped to 68.5% eased progress + onElement(TestElements.Foo).assertSizeIsEqualTo(31.5.dp, 31.5.dp) + onElement(TestElements.Bar).assertSizeIsEqualTo(68.5.dp, 68.5.dp) + } + at(32) { + // 50% linear progress is mapped to 89.5% eased progress + onElement(TestElements.Foo).assertSizeIsEqualTo(10.5.dp, 10.5.dp) + onElement(TestElements.Bar).assertSizeIsEqualTo(89.5.dp, 89.5.dp) + } + at(48) { + // 75% linear progress is mapped to 97.8% eased progress + onElement(TestElements.Foo).assertSizeIsEqualTo(2.2.dp, 2.2.dp) + onElement(TestElements.Bar).assertSizeIsEqualTo(97.8.dp, 97.8.dp) + } + after { + onElement(TestElements.Foo).assertDoesNotExist() + onElement(TestElements.Bar).assertSizeIsEqualTo(100.dp, 100.dp) + } + } + } + + @Test + fun testTimestampRangeEasing() { + val easing = CubicBezierEasing(0.1f, 0.1f, 0f, 1f) + rule.testTransition( + fromSceneContent = { Box(Modifier.size(100.dp).element(TestElements.Foo)) }, + toSceneContent = { Box(Modifier.size(100.dp).element(TestElements.Bar)) }, + transition = { + // Scale during 4 frames. + spec = tween(16 * 4, easing = LinearEasing) + timestampRange(easing = easing) { + scaleSize(TestElements.Foo, width = 0f, height = 0f) + scaleSize(TestElements.Bar, width = 0f, height = 0f) + } + }, + ) { + // Foo is entering, is 100dp x 100dp at rest and is scaled by (2, 0.5) during the + // transition so it starts at 200dp x 50dp. + before { onElement(TestElements.Bar).assertDoesNotExist() } + at(0) { + onElement(TestElements.Foo).assertSizeIsEqualTo(100.dp, 100.dp) + onElement(TestElements.Bar).assertSizeIsEqualTo(0.dp, 0.dp) + } + at(16) { + // 25% linear progress is mapped to 68.5% eased progress + onElement(TestElements.Foo).assertSizeIsEqualTo(31.5.dp, 31.5.dp) + onElement(TestElements.Bar).assertSizeIsEqualTo(68.5.dp, 68.5.dp) + } + at(32) { + // 50% linear progress is mapped to 89.5% eased progress + onElement(TestElements.Foo).assertSizeIsEqualTo(10.5.dp, 10.5.dp) + onElement(TestElements.Bar).assertSizeIsEqualTo(89.5.dp, 89.5.dp) + } + at(48) { + // 75% linear progress is mapped to 97.8% eased progress + onElement(TestElements.Foo).assertSizeIsEqualTo(2.2.dp, 2.2.dp) + onElement(TestElements.Bar).assertSizeIsEqualTo(97.8.dp, 97.8.dp) + } + after { + onElement(TestElements.Foo).assertDoesNotExist() + onElement(TestElements.Bar).assertSizeIsEqualTo(100.dp, 100.dp) + } + } + } +} diff --git a/packages/SystemUI/lint-baseline.xml b/packages/SystemUI/lint-baseline.xml index b4c839f08607..7577147a6f16 100644 --- a/packages/SystemUI/lint-baseline.xml +++ b/packages/SystemUI/lint-baseline.xml @@ -32157,4 +32157,631 @@ column="6"/> </issue> + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`" + errorLine1=" contentResolver.registerContentObserver(" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/keyguard/ActiveUnlockConfig.kt" + line="154" + column="33"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`" + errorLine1=" resolver.registerContentObserver(ALWAYS_ON_DISPLAY_CONSTANTS_URI," + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/systemui/doze/AlwaysOnDisplayPolicy.java" + line="164" + column="22"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`" + errorLine1=" resolver.registerContentObserver(" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/systemui/util/animation/data/repository/AnimationStatusRepository.kt" + line="61" + column="26"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`" + errorLine1=" awaitClose { resolver.unregisterContentObserver(observer) }" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/systemui/util/animation/data/repository/AnimationStatusRepository.kt" + line="67" + column="39"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`" + errorLine1=" mContentResolver.unregisterContentObserver(mSettingObserver);" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java" + line="123" + column="38"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`" + errorLine1=" mContentResolver.unregisterContentObserver(mSettingObserver);" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java" + line="180" + column="26"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`" + errorLine1=" mContentResolver.registerContentObserver(" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java" + line="211" + column="26"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`" + errorLine1=" mContentResolver.registerContentObserver(" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java" + line="219" + column="26"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`" + errorLine1=" resolver.registerContentObserver(" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/systemui/demomode/DemoModeAvailabilityTracker.kt" + line="46" + column="18"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`" + errorLine1=" resolver.registerContentObserver(" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/systemui/demomode/DemoModeAvailabilityTracker.kt" + line="48" + column="18"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`" + errorLine1=" resolver.unregisterContentObserver(allowedObserver)" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/systemui/demomode/DemoModeAvailabilityTracker.kt" + line="54" + column="18"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`" + errorLine1=" resolver.unregisterContentObserver(onObserver)" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/systemui/demomode/DemoModeAvailabilityTracker.kt" + line="55" + column="18"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`" + errorLine1=" resolver.registerContentObserver(mQuickPickupGesture, false, this," + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java" + line="511" + column="26"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`" + errorLine1=" resolver.registerContentObserver(mPickupGesture, false, this, UserHandle.USER_ALL);" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java" + line="513" + column="26"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`" + errorLine1=" resolver.registerContentObserver(mAlwaysOnEnabled, false, this," + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java" + line="514" + column="26"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`" + errorLine1=" mContext.getContentResolver().registerContentObserver(" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java" + line="2470" + column="39"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`" + errorLine1=" mContext.getContentResolver().registerContentObserver(" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java" + line="3077" + column="39"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`" + errorLine1=" mContext.getContentResolver().unregisterContentObserver(mDeviceProvisionedObserver);" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java" + line="3168" + column="43"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`" + errorLine1=" mContext.getContentResolver().unregisterContentObserver(mDeviceProvisionedObserver);" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java" + line="3957" + column="43"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`" + errorLine1=" mContext.getContentResolver().unregisterContentObserver(mTimeFormatChangeObserver);" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java" + line="3961" + column="43"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`" + errorLine1=" contentResolver.registerContentObserver(" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt" + line="486" + column="25"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`" + errorLine1=" contentResolver.registerContentObserver(" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt" + line="492" + column="25"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`" + errorLine1=" contentResolver.unregisterContentObserver(settingsObserver)" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt" + line="543" + column="25"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`" + errorLine1=" contentResolver.registerContentObserver(" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/systemui/smartspace/filters/LockscreenTargetFilter.kt" + line="82" + column="25"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`" + errorLine1=" contentResolver.unregisterContentObserver(settingsObserver)" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/systemui/smartspace/filters/LockscreenTargetFilter.kt" + line="100" + column="25"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`" + errorLine1=" mContext.getContentResolver().unregisterContentObserver(mMenuTargetFeaturesContentObserver);" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java" + line="275" + column="39"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`" + errorLine1=" mContext.getContentResolver().unregisterContentObserver(mMenuSizeContentObserver);" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java" + line="276" + column="39"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`" + errorLine1=" mContext.getContentResolver().unregisterContentObserver(mMenuFadeOutContentObserver);" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java" + line="277" + column="39"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`" + errorLine1=" mContext.getContentResolver().registerContentObserver(Global.getUriFor(Global.MOBILE_DATA)," + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileSignalController.java" + line="200" + column="39"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`" + errorLine1=" mContext.getContentResolver().registerContentObserver(Global.getUriFor(" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileSignalController.java" + line="202" + column="39"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`" + errorLine1=" mContext.getContentResolver().unregisterContentObserver(mObserver);" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileSignalController.java" + line="212" + column="39"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`" + errorLine1=" context.contentResolver.registerContentObserver(" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/NaturalScrollingSettingObserver.kt" + line="54" + column="33"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`" + errorLine1=" mContentResolver.registerContentObserver(" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java" + line="253" + column="26"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`" + errorLine1=" mContentResolver.registerContentObserver(" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java" + line="256" + column="26"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`" + errorLine1=" mContentResolver.registerContentObserver(" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java" + line="259" + column="26"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`" + errorLine1=" mContentResolver.registerContentObserver(" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java" + line="262" + column="26"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`" + errorLine1=" mContentResolver.unregisterContentObserver(mAssistContentObserver);" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java" + line="295" + column="26"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`" + errorLine1=" mContext.getContentResolver().registerContentObserver(" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java" + line="395" + column="39"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`" + errorLine1=" mContext.getContentResolver().registerContentObserver(" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java" + line="400" + column="39"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`" + errorLine1=" mContentResolver.registerContentObserver(" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java" + line="3675" + column="26"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`" + errorLine1=" mContentResolver.unregisterContentObserver(mSettingsChangeObserver);" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java" + line="4705" + column="30"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`" + errorLine1=" resolver.registerContentObserver(Settings.Global.getUriFor(" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/systemui/power/PowerUI.java" + line="191" + column="18"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`" + errorLine1=" resolver.registerContentObserver(" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/systemui/power/PowerUI.java" + line="205" + column="18"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`" + errorLine1=" resolver.registerContentObserver(" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/systemui/power/PowerUI.java" + line="216" + column="18"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`" + errorLine1=" mContext.getContentResolver().registerContentObserver(" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/systemui/qs/QSFooterView.java" + line="178" + column="39"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`" + errorLine1=" mContext.getContentResolver().unregisterContentObserver(mDeveloperSettingsObserver);" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/systemui/qs/QSFooterView.java" + line="186" + column="39"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`" + errorLine1=" mContentResolver.registerContentObserver(" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/systemui/accessibility/SecureSettingsContentObserver.java" + line="83" + column="30"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`" + errorLine1=" mContentResolver.unregisterContentObserver(mContentObserver);" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/systemui/accessibility/SecureSettingsContentObserver.java" + line="100" + column="30"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`" + errorLine1=" mContentResolver.registerContentObserver(uri, false, mObserver, mCurrentUser);" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java" + line="219" + column="30"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`" + errorLine1=" mContentResolver.unregisterContentObserver(mObserver);" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java" + line="241" + column="26"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`" + errorLine1=" mContentResolver.registerContentObserver(uri, false, mObserver, mCurrentUser);" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java" + line="243" + column="30"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`" + errorLine1=" mContext.getContentResolver().registerContentObserver(ZEN_MODE_URI, false, this);" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java" + line="1235" + column="43"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`" + errorLine1=" mContext.getContentResolver().registerContentObserver(ZEN_MODE_CONFIG_URI, false, this);" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java" + line="1236" + column="43"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`" + errorLine1=" mContext.getContentResolver().unregisterContentObserver(this);" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java" + line="1240" + column="43"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`" + errorLine1=" mResolver.unregisterContentObserver(this);" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java" + line="377" + column="27"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`" + errorLine1=" mResolver.registerContentObserver(" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java" + line="379" + column="23"/> + </issue> + + <issue + id="RegisterContentObserverViaContentResolver" + message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`" + errorLine1=" mResolver.registerContentObserver(" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java" + line="381" + column="23"/> + </issue> + </issues> diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/AccessibilityQsShortcutsRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/AccessibilityQsShortcutsRepositoryImplTest.kt index 40ea0a066338..460461a003f6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/AccessibilityQsShortcutsRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/AccessibilityQsShortcutsRepositoryImplTest.kt @@ -37,7 +37,6 @@ import org.mockito.junit.MockitoRule @SmallTest @RunWith(AndroidJUnit4::class) -@android.platform.test.annotations.EnabledOnRavenwood class AccessibilityQsShortcutsRepositoryImplTest : SysuiTestCase() { @Rule @JvmField val mockitoRule: MockitoRule = MockitoJUnit.rule() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepositoryImplTest.kt index fa47a02d78c9..4e1f82c24bb6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepositoryImplTest.kt @@ -37,7 +37,6 @@ import org.junit.runner.RunWith @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) -@android.platform.test.annotations.EnabledOnRavenwood class ColorCorrectionRepositoryImplTest : SysuiTestCase() { private val testUser1 = UserHandle.of(1)!! diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt index 9c9ee53d9c56..b99dec44b519 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt @@ -37,7 +37,6 @@ import org.junit.runner.RunWith @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) -@android.platform.test.annotations.EnabledOnRavenwood class ColorInversionRepositoryImplTest : SysuiTestCase() { private val testUser1 = UserHandle.of(1)!! diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/OneHandedModeRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/OneHandedModeRepositoryImplTest.kt index c0d481c6e659..1378dac98eaa 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/OneHandedModeRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/OneHandedModeRepositoryImplTest.kt @@ -35,7 +35,6 @@ import org.junit.runner.RunWith @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) -@android.platform.test.annotations.EnabledOnRavenwood class OneHandedModeRepositoryImplTest : SysuiTestCase() { private val testUser1 = UserHandle.of(1)!! diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/UserA11yQsShortcutsRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/UserA11yQsShortcutsRepositoryTest.kt index ed3b4c0fe322..ce22e288e292 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/UserA11yQsShortcutsRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/UserA11yQsShortcutsRepositoryTest.kt @@ -31,7 +31,6 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) -@android.platform.test.annotations.EnabledOnRavenwood class UserA11yQsShortcutsRepositoryTest : SysuiTestCase() { private val secureSettings = FakeSettings() private val testDispatcher = StandardTestDispatcher() 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/biometrics/AuthControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java index 9b1d4eca8b2b..752c93e077ac 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java @@ -148,8 +148,6 @@ public class AuthControllerTest extends SysuiTestCase { @Mock private WakefulnessLifecycle mWakefulnessLifecycle; @Mock - private AuthDialogPanelInteractionDetector mPanelInteractionDetector; - @Mock private UserManager mUserManager; @Mock private LockPatternUtils mLockPatternUtils; @@ -1059,10 +1057,9 @@ public class AuthControllerTest extends SysuiTestCase { super(context, null /* applicationCoroutineScope */, mExecution, mCommandQueue, mActivityTaskManager, mWindowManager, mFingerprintManager, mFaceManager, () -> mUdfpsController, mDisplayManager, - mWakefulnessLifecycle, mPanelInteractionDetector, mUserManager, - mLockPatternUtils, () -> mUdfpsLogger, () -> mLogContextInteractor, - () -> mPromptSelectionInteractor, () -> mCredentialViewModel, - () -> mPromptViewModel, mInteractionJankMonitor, + mWakefulnessLifecycle, mUserManager, mLockPatternUtils, () -> mUdfpsLogger, + () -> mLogContextInteractor, () -> mPromptSelectionInteractor, + () -> mCredentialViewModel, () -> mPromptViewModel, mInteractionJankMonitor, mHandler, mBackgroundExecutor, mUdfpsUtils, mVibratorHelper); } @@ -1071,7 +1068,6 @@ public class AuthControllerTest extends SysuiTestCase { boolean requireConfirmation, int userId, int[] sensorIds, String opPackageName, boolean skipIntro, long operationId, long requestId, WakefulnessLifecycle wakefulnessLifecycle, - AuthDialogPanelInteractionDetector panelInteractionDetector, UserManager userManager, LockPatternUtils lockPatternUtils, PromptViewModel viewModel) { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapterTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapterTest.java deleted file mode 100644 index cd9189bef7f1..000000000000 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapterTest.java +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright (C) 2021 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.biometrics; - -import static org.junit.Assert.assertEquals; - -import android.hardware.biometrics.ComponentInfoInternal; -import android.hardware.biometrics.SensorLocationInternal; -import android.hardware.biometrics.SensorProperties; -import android.hardware.fingerprint.FingerprintSensorProperties; -import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.SmallTest; - -import com.android.systemui.SysuiTestCase; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.util.ArrayList; -import java.util.List; - -@RunWith(AndroidJUnit4.class) -@SmallTest -public class UdfpsDialogMeasureAdapterTest extends SysuiTestCase { - @Test - public void testUdfpsBottomSpacerHeightForPortrait() { - final int displayHeightPx = 3000; - final int navbarHeightPx = 10; - final int dialogBottomMarginPx = 20; - final int buttonBarHeightPx = 100; - final int textIndicatorHeightPx = 200; - - final int sensorLocationX = 540; - final int sensorLocationY = 1600; - final int sensorRadius = 100; - - final List<ComponentInfoInternal> componentInfo = new ArrayList<>(); - componentInfo.add(new ComponentInfoInternal("faceSensor" /* componentId */, - "vendor/model/revision" /* hardwareVersion */, "1.01" /* firmwareVersion */, - "00000001" /* serialNumber */, "" /* softwareVersion */)); - componentInfo.add(new ComponentInfoInternal("matchingAlgorithm" /* componentId */, - "" /* hardwareVersion */, "" /* firmwareVersion */, "" /* serialNumber */, - "vendor/version/revision" /* softwareVersion */)); - - final FingerprintSensorPropertiesInternal props = new FingerprintSensorPropertiesInternal( - 0 /* sensorId */, SensorProperties.STRENGTH_STRONG, 5 /* maxEnrollmentsPerUser */, - componentInfo, - FingerprintSensorProperties.TYPE_UDFPS_OPTICAL, - true /* halControlsIllumination */, - true /* resetLockoutRequiresHardwareAuthToken */, - List.of(new SensorLocationInternal("" /* displayId */, - sensorLocationX, sensorLocationY, sensorRadius))); - - assertEquals(970, - UdfpsDialogMeasureAdapter.calculateBottomSpacerHeightForPortrait( - props, displayHeightPx, textIndicatorHeightPx, buttonBarHeightPx, - dialogBottomMarginPx, navbarHeightPx, 1.0f /* resolutionScale */ - )); - } - - @Test - public void testUdfpsBottomSpacerHeightForLandscape_whenMoreSpaceAboveIcon() { - final int titleHeightPx = 320; - final int subtitleHeightPx = 240; - final int descriptionHeightPx = 200; - final int topSpacerHeightPx = 550; - final int textIndicatorHeightPx = 190; - final int buttonBarHeightPx = 160; - final int navbarBottomInsetPx = 75; - - assertEquals(885, - UdfpsDialogMeasureAdapter.calculateBottomSpacerHeightForLandscape( - titleHeightPx, subtitleHeightPx, descriptionHeightPx, topSpacerHeightPx, - textIndicatorHeightPx, buttonBarHeightPx, navbarBottomInsetPx)); - } - - @Test - public void testUdfpsBottomSpacerHeightForLandscape_whenMoreSpaceBelowIcon() { - final int titleHeightPx = 315; - final int subtitleHeightPx = 160; - final int descriptionHeightPx = 75; - final int topSpacerHeightPx = 220; - final int textIndicatorHeightPx = 290; - final int buttonBarHeightPx = 360; - final int navbarBottomInsetPx = 205; - - assertEquals(-85, - UdfpsDialogMeasureAdapter.calculateBottomSpacerHeightForLandscape( - titleHeightPx, subtitleHeightPx, descriptionHeightPx, topSpacerHeightPx, - textIndicatorHeightPx, buttonBarHeightPx, navbarBottomInsetPx)); - } - - @Test - public void testUdfpsHorizontalSpacerWidthForLandscape() { - final int displayWidthPx = 3000; - final int dialogMarginPx = 20; - final int navbarHorizontalInsetPx = 75; - - final int sensorLocationX = 540; - final int sensorLocationY = 1600; - final int sensorRadius = 100; - - final List<ComponentInfoInternal> componentInfo = new ArrayList<>(); - componentInfo.add(new ComponentInfoInternal("faceSensor" /* componentId */, - "vendor/model/revision" /* hardwareVersion */, "1.01" /* firmwareVersion */, - "00000001" /* serialNumber */, "" /* softwareVersion */)); - componentInfo.add(new ComponentInfoInternal("matchingAlgorithm" /* componentId */, - "" /* hardwareVersion */, "" /* firmwareVersion */, "" /* serialNumber */, - "vendor/version/revision" /* softwareVersion */)); - - final FingerprintSensorPropertiesInternal props = new FingerprintSensorPropertiesInternal( - 0 /* sensorId */, SensorProperties.STRENGTH_STRONG, 5 /* maxEnrollmentsPerUser */, - componentInfo, - FingerprintSensorProperties.TYPE_UDFPS_OPTICAL, - true /* halControlsIllumination */, - true /* resetLockoutRequiresHardwareAuthToken */, - List.of(new SensorLocationInternal("" /* displayId */, - sensorLocationX, sensorLocationY, sensorRadius))); - - assertEquals(1205, - UdfpsDialogMeasureAdapter.calculateHorizontalSpacerWidthForLandscape( - props, displayWidthPx, dialogMarginPx, navbarHorizontalInsetPx, - 1.0f /* resolutionScale */)); - } -} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/camera/data/repository/CameraAutoRotateRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/camera/data/repository/CameraAutoRotateRepositoryImplTest.kt index 9cfa57257053..667d364ddc69 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/camera/data/repository/CameraAutoRotateRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/camera/data/repository/CameraAutoRotateRepositoryImplTest.kt @@ -35,7 +35,6 @@ import org.junit.runner.RunWith @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) -@android.platform.test.annotations.EnabledOnRavenwood class CameraAutoRotateRepositoryImplTest : SysuiTestCase() { private val kosmos = Kosmos() private val testScope = kosmos.testScope diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalTutorialRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalTutorialRepositoryImplTest.kt index 2911a50c2737..c37b33e52fa6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalTutorialRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalTutorialRepositoryImplTest.kt @@ -40,7 +40,6 @@ import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidJUnit4::class) -@android.platform.test.annotations.EnabledOnRavenwood class CommunalTutorialRepositoryImplTest : SysuiTestCase() { @Mock private lateinit var tableLogBuffer: TableLogBuffer 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/communal/widgets/EditWidgetsActivityControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/EditWidgetsActivityControllerTest.kt new file mode 100644 index 000000000000..50fdb31b0414 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/EditWidgetsActivityControllerTest.kt @@ -0,0 +1,107 @@ +/* + * 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.communal.widgets + +import android.app.Activity +import android.app.Application.ActivityLifecycleCallbacks +import android.os.Bundle +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.clearInvocations +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify + +@ExperimentalCoroutinesApi +@SmallTest +@RunWith(AndroidJUnit4::class) +class EditWidgetsActivityControllerTest : SysuiTestCase() { + @Test + fun activityLifecycle_stoppedWhenNotWaitingForResult() { + val activity = mock<Activity>() + val controller = EditWidgetsActivity.ActivityController(activity) + + val callbackCapture = argumentCaptor<ActivityLifecycleCallbacks>() + verify(activity).registerActivityLifecycleCallbacks(callbackCapture.capture()) + + callbackCapture.lastValue.onActivityStopped(activity) + + verify(activity).finish() + } + + @Test + fun activityLifecycle_notStoppedWhenNotWaitingForResult() { + val activity = mock<Activity>() + val controller = EditWidgetsActivity.ActivityController(activity) + + val callbackCapture = argumentCaptor<ActivityLifecycleCallbacks>() + verify(activity).registerActivityLifecycleCallbacks(callbackCapture.capture()) + + controller.onWaitingForResult(true) + callbackCapture.lastValue.onActivityStopped(activity) + + verify(activity, never()).finish() + } + + @Test + fun activityLifecycle_stoppedAfterResultReturned() { + val activity = mock<Activity>() + val controller = EditWidgetsActivity.ActivityController(activity) + + val callbackCapture = argumentCaptor<ActivityLifecycleCallbacks>() + verify(activity).registerActivityLifecycleCallbacks(callbackCapture.capture()) + + controller.onWaitingForResult(true) + controller.onWaitingForResult(false) + callbackCapture.lastValue.onActivityStopped(activity) + + verify(activity).finish() + } + + @Test + fun activityLifecycle_statePreservedThroughInstanceSave() { + val activity = mock<Activity>() + val bundle = Bundle(1) + + run { + val controller = EditWidgetsActivity.ActivityController(activity) + val callbackCapture = argumentCaptor<ActivityLifecycleCallbacks>() + verify(activity).registerActivityLifecycleCallbacks(callbackCapture.capture()) + + controller.onWaitingForResult(true) + callbackCapture.lastValue.onActivitySaveInstanceState(activity, bundle) + } + + clearInvocations(activity) + + run { + val controller = EditWidgetsActivity.ActivityController(activity) + val callbackCapture = argumentCaptor<ActivityLifecycleCallbacks>() + verify(activity).registerActivityLifecycleCallbacks(callbackCapture.capture()) + + callbackCapture.lastValue.onActivityCreated(activity, bundle) + callbackCapture.lastValue.onActivityStopped(activity) + + verify(activity, never()).finish() + } + } +} 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 331db525c4ee..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 @@ -21,15 +21,14 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestableContext -import com.android.systemui.coroutines.collectLastValue 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.kosmos.Kosmos import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.google.common.truth.Truth.assertThat import java.io.File -import java.time.Clock -import java.time.Instant import javax.inject.Provider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.test.TestScope @@ -44,13 +43,13 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class ContextualEducationRepositoryTest : SysuiTestCase() { - private lateinit var underTest: ContextualEducationRepository + private lateinit var underTest: UserContextualEducationRepository private val kosmos = Kosmos() private val testScope = kosmos.testScope private val dsScopeProvider: Provider<CoroutineScope> = Provider { TestScope(kosmos.testDispatcher).backgroundScope } - private val clock: Clock = FakeEduClock(Instant.ofEpochMilli(1000)) + private val testUserId = 1111 // For deleting any test files created after the test @@ -61,8 +60,7 @@ class ContextualEducationRepositoryTest : SysuiTestCase() { // Create TestContext here because TemporaryFolder.create() is called in @Before. It is // needed before calling TemporaryFolder.newFolder(). val testContext = TestContext(context, tmpFolder.newFolder()) - val userRepository = UserContextualEducationRepository(testContext, dsScopeProvider) - underTest = ContextualEducationRepositoryImpl(clock, userRepository) + underTest = UserContextualEducationRepository(testContext, dsScopeProvider) underTest.setUser(testUserId) } @@ -70,7 +68,7 @@ class ContextualEducationRepositoryTest : SysuiTestCase() { fun changeRetrievedValueForNewUser() = testScope.runTest { // Update data for old user. - underTest.incrementSignalCount(BACK) + underTest.updateGestureEduModel(BACK) { it.copy(signalCount = 1) } val model by collectLastValue(underTest.readGestureEduModelFlow(BACK)) assertThat(model?.signalCount).isEqualTo(1) @@ -81,20 +79,19 @@ class ContextualEducationRepositoryTest : SysuiTestCase() { } @Test - fun incrementSignalCount() = - testScope.runTest { - underTest.incrementSignalCount(BACK) - val model by collectLastValue(underTest.readGestureEduModelFlow(BACK)) - assertThat(model?.signalCount).isEqualTo(1) - } - - @Test - fun dataAddedOnUpdateShortcutTriggerTime() = + fun dataChangedOnUpdate() = testScope.runTest { + val newModel = + GestureEduModel( + signalCount = 2, + educationShownCount = 1, + lastShortcutTriggeredTime = kosmos.fakeEduClock.instant(), + lastEducationTime = kosmos.fakeEduClock.instant(), + usageSessionStartTime = kosmos.fakeEduClock.instant(), + ) + underTest.updateGestureEduModel(BACK) { newModel } val model by collectLastValue(underTest.readGestureEduModelFlow(BACK)) - assertThat(model?.lastShortcutTriggeredTime).isNull() - underTest.updateShortcutTriggerTime(BACK) - assertThat(model?.lastShortcutTriggeredTime).isEqualTo(clock.instant()) + assertThat(model).isEqualTo(newModel) } /** Test context which allows overriding getFilesDir path */ 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 ae3302ca658d..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 @@ -19,13 +19,18 @@ package com.android.systemui.education.domain.interactor import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.coroutines.collectLastValue 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 @@ -36,8 +41,9 @@ import org.junit.runner.RunWith class KeyboardTouchpadEduInteractorTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope - private val repository = kosmos.contextualEducationRepository + private val contextualEduInteractor = kosmos.contextualEducationInteractor private val underTest: KeyboardTouchpadEduInteractor = kosmos.keyboardTouchpadEduInteractor + private val eduClock = kosmos.fakeEduClock @Before fun setup() { @@ -47,15 +53,35 @@ 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 { - repository.incrementSignalCount(BACK) + contextualEduInteractor.incrementSignalCount(BACK) val model by collectLastValue(underTest.educationTriggered) assertThat(model).isNull() } @@ -64,15 +90,34 @@ class KeyboardTouchpadEduInteractorTest : SysuiTestCase() { fun noEducationInfoWhenShortcutTriggeredPreviously() = testScope.runTest { val model by collectLastValue(underTest.educationTriggered) - repository.updateShortcutTriggerTime(BACK) - tryTriggeringEducation(BACK) + contextualEduInteractor.updateShortcutTriggerTime(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) { - repository.incrementSignalCount(gestureType) + contextualEduInteractor.incrementSignalCount(gestureType) } } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt new file mode 100644 index 000000000000..1f733472cbed --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt @@ -0,0 +1,82 @@ +/* + * 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.systemui.education.domain.ui.view + +import android.content.applicationContext +import android.widget.Toast +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.contextualeducation.GestureType +import com.android.systemui.contextualeducation.GestureType.BACK +import com.android.systemui.education.domain.interactor.KeyboardTouchpadEduInteractor +import com.android.systemui.education.domain.interactor.contextualEducationInteractor +import com.android.systemui.education.domain.interactor.keyboardTouchpadEduInteractor +import com.android.systemui.education.ui.view.ContextualEduUiCoordinator +import com.android.systemui.education.ui.viewmodel.ContextualEduViewModel +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.junit.MockitoJUnit +import org.mockito.kotlin.verify + +@SmallTest +@RunWith(AndroidJUnit4::class) +class ContextualEduUiCoordinatorTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val interactor = kosmos.contextualEducationInteractor + private lateinit var underTest: ContextualEduUiCoordinator + @Mock private lateinit var toast: Toast + + @get:Rule val mockitoRule = MockitoJUnit.rule() + + @Before + fun setUp() { + val viewModel = + ContextualEduViewModel( + kosmos.applicationContext.resources, + kosmos.keyboardTouchpadEduInteractor + ) + underTest = + ContextualEduUiCoordinator(kosmos.applicationCoroutineScope, viewModel) { _ -> toast } + underTest.start() + kosmos.keyboardTouchpadEduInteractor.start() + } + + @Test + @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) + fun showToastOnNewEdu() = + testScope.runTest { + triggerEducation(BACK) + runCurrent() + verify(toast).show() + } + + private suspend fun triggerEducation(gestureType: GestureType) { + for (i in 1..KeyboardTouchpadEduInteractor.MAX_SIGNAL_COUNT) { + interactor.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 d1f908dfc795..46b370fedf37 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt @@ -16,16 +16,27 @@ 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) @@ -110,4 +121,45 @@ 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/panels/domain/interactor/IconTilesInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorTest.kt index c3a5df06e2a4..661d4b01a1b2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorTest.kt @@ -67,19 +67,25 @@ class IconTilesInteractorTest : SysuiTestCase() { } } + @OptIn(ExperimentalCoroutinesApi::class) @Test fun resize_updatesSharedPreferences() = with(kosmos) { testScope.runTest { + qsPreferencesRepository.setLargeTilesSpecs(setOf()) + runCurrent() + val latest by collectLastValue(qsPreferencesRepository.largeTilesSpecs) val spec = TileSpec.create("large") // Assert that the tile is added to the large tiles after resizing - underTest.resize(spec, toIcon = false) + underTest.resize(spec) + runCurrent() assertThat(latest).contains(spec) // Assert that the tile is removed from the large tiles after resizing - underTest.resize(spec, toIcon = true) + underTest.resize(spec) + runCurrent() assertThat(latest).doesNotContain(spec) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropStateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropStateTest.kt index 45262ca0587c..b2f5765d0bc4 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropStateTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropStateTest.kt @@ -22,6 +22,8 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.shared.model.Text +import com.android.systemui.qs.panels.shared.model.SizedTile +import com.android.systemui.qs.panels.shared.model.SizedTileImpl import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel import com.android.systemui.qs.pipeline.shared.TileSpec import com.google.common.truth.Truth.assertThat @@ -37,15 +39,15 @@ class DragAndDropStateTest : SysuiTestCase() { @Test fun isMoving_returnsCorrectValue() { // Asserts no tiles is moving - TestEditTiles.forEach { assertThat(underTest.isMoving(it.tileSpec)).isFalse() } + TestEditTiles.forEach { assertThat(underTest.isMoving(it.tile.tileSpec)).isFalse() } // Start the drag movement underTest.onStarted(TestEditTiles[0]) // Assert that the correct tile is marked as moving TestEditTiles.forEach { - assertThat(underTest.isMoving(it.tileSpec)) - .isEqualTo(TestEditTiles[0].tileSpec == it.tileSpec) + assertThat(underTest.isMoving(it.tile.tileSpec)) + .isEqualTo(TestEditTiles[0].tile.tileSpec == it.tile.tileSpec) } } @@ -55,11 +57,11 @@ class DragAndDropStateTest : SysuiTestCase() { underTest.onStarted(TestEditTiles[0]) // Move the tile to the end of the list - underTest.onMoved(listState.tiles[5].tileSpec) + underTest.onMoved(listState.tiles[5].tile.tileSpec) assertThat(underTest.currentPosition()).isEqualTo(5) // Move the tile to the middle of the list - underTest.onMoved(listState.tiles[2].tileSpec) + underTest.onMoved(listState.tiles[2].tile.tileSpec) assertThat(underTest.currentPosition()).isEqualTo(2) } @@ -69,13 +71,13 @@ class DragAndDropStateTest : SysuiTestCase() { underTest.onStarted(TestEditTiles[0]) // Move the tile to the end of the list - underTest.onMoved(listState.tiles[5].tileSpec) + underTest.onMoved(listState.tiles[5].tile.tileSpec) // Drop the tile underTest.onDrop() // Asserts no tiles is moving - TestEditTiles.forEach { assertThat(underTest.isMoving(it.tileSpec)).isFalse() } + TestEditTiles.forEach { assertThat(underTest.isMoving(it.tile.tileSpec)).isFalse() } } @Test @@ -87,19 +89,24 @@ class DragAndDropStateTest : SysuiTestCase() { underTest.movedOutOfBounds() // Asserts the moving tile is not current - assertThat(listState.tiles.firstOrNull { it.tileSpec == TestEditTiles[0].tileSpec }) + assertThat( + listState.tiles.firstOrNull { it.tile.tileSpec == TestEditTiles[0].tile.tileSpec } + ) .isNull() } companion object { - private fun createEditTile(tileSpec: String): EditTileViewModel { - return EditTileViewModel( - tileSpec = TileSpec.create(tileSpec), - icon = Icon.Resource(0, null), - label = Text.Loaded("unused"), - appName = null, - isCurrent = true, - availableEditActions = emptySet(), + private fun createEditTile(tileSpec: String): SizedTile<EditTileViewModel> { + return SizedTileImpl( + EditTileViewModel( + tileSpec = TileSpec.create(tileSpec), + icon = Icon.Resource(0, null), + label = Text.Loaded("unused"), + appName = null, + isCurrent = true, + availableEditActions = emptySet(), + ), + 1, ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt index e76d3892cf53..a3a6a33f6408 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt @@ -21,6 +21,8 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.shared.model.Text +import com.android.systemui.qs.panels.shared.model.SizedTile +import com.android.systemui.qs.panels.shared.model.SizedTileImpl import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel import com.android.systemui.qs.pipeline.shared.TileSpec import com.google.common.truth.Truth.assertThat @@ -35,7 +37,7 @@ class EditTileListStateTest : SysuiTestCase() { @Test fun movingNonExistentTile_tileAdded() { val newTile = createEditTile("other_tile", false) - underTest.move(newTile, TestEditTiles[0].tileSpec) + underTest.move(newTile, TestEditTiles[0].tile.tileSpec) assertThat(underTest.tiles[0]).isEqualTo(newTile) assertThat(underTest.tiles.subList(1, underTest.tiles.size)) @@ -51,7 +53,7 @@ class EditTileListStateTest : SysuiTestCase() { @Test fun movingTileToItself_listUnchanged() { - underTest.move(TestEditTiles[0], TestEditTiles[0].tileSpec) + underTest.move(TestEditTiles[0], TestEditTiles[0].tile.tileSpec) assertThat(underTest.tiles).containsExactly(*TestEditTiles.toTypedArray()) } @@ -59,7 +61,7 @@ class EditTileListStateTest : SysuiTestCase() { @Test fun movingTileToSameSection_listUpdates() { // Move tile at index 0 to index 1. Tile 0 should remain current. - underTest.move(TestEditTiles[0], TestEditTiles[1].tileSpec) + underTest.move(TestEditTiles[0], TestEditTiles[1].tile.tileSpec) // Assert the tiles 0 and 1 have changed places. assertThat(underTest.tiles[0]).isEqualTo(TestEditTiles[1]) @@ -72,21 +74,27 @@ class EditTileListStateTest : SysuiTestCase() { fun removingTile_listUpdates() { // Remove tile at index 0 - underTest.remove(TestEditTiles[0].tileSpec) + underTest.remove(TestEditTiles[0].tile.tileSpec) // Assert the tile was removed assertThat(underTest.tiles).containsExactly(*TestEditTiles.subList(1, 6).toTypedArray()) } companion object { - private fun createEditTile(tileSpec: String, isCurrent: Boolean): EditTileViewModel { - return EditTileViewModel( - tileSpec = TileSpec.create(tileSpec), - icon = Icon.Resource(0, null), - label = Text.Loaded("unused"), - appName = null, - isCurrent = isCurrent, - availableEditActions = emptySet(), + private fun createEditTile( + tileSpec: String, + isCurrent: Boolean + ): SizedTile<EditTileViewModel> { + return SizedTileImpl( + EditTileViewModel( + tileSpec = TileSpec.create(tileSpec), + icon = Icon.Resource(0, null), + label = Text.Loaded("unused"), + appName = null, + isCurrent = isCurrent, + availableEditActions = emptySet(), + ), + 1, ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/PaginatableGridLayoutTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/PaginatableGridLayoutTest.kt index 6df3f8d1bdd5..0d93686714bf 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/PaginatableGridLayoutTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/PaginatableGridLayoutTest.kt @@ -19,7 +19,7 @@ package com.android.systemui.qs.panels.ui.compose import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.qs.panels.shared.model.SizedTile +import com.android.systemui.qs.panels.shared.model.SizedTileImpl import com.android.systemui.qs.panels.ui.viewmodel.MockTileViewModel import com.android.systemui.qs.pipeline.shared.TileSpec import com.google.common.truth.Truth.assertThat @@ -72,10 +72,10 @@ class PaginatableGridLayoutTest : SysuiTestCase() { } companion object { - fun extraLargeTile() = SizedTile(MockTileViewModel(TileSpec.create("XLarge")), 3) + fun extraLargeTile() = SizedTileImpl(MockTileViewModel(TileSpec.create("XLarge")), 3) - fun largeTile() = SizedTile(MockTileViewModel(TileSpec.create("large")), 2) + fun largeTile() = SizedTileImpl(MockTileViewModel(TileSpec.create("large")), 2) - fun smallTile() = SizedTile(MockTileViewModel(TileSpec.create("small")), 1) + fun smallTile() = SizedTileImpl(MockTileViewModel(TileSpec.create("small")), 1) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/AutoAddableSettingTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/AutoAddableSettingTest.kt index cfb84a7a6709..d153e9d1d361 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/AutoAddableSettingTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/AutoAddableSettingTest.kt @@ -16,7 +16,6 @@ package com.android.systemui.qs.pipeline.domain.autoaddable -import android.platform.test.annotations.EnabledOnRavenwood import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -36,7 +35,6 @@ import org.junit.runner.RunWith @OptIn(ExperimentalCoroutinesApi::class) @SmallTest -@EnabledOnRavenwood @RunWith(AndroidJUnit4::class) class AutoAddableSettingTest : SysuiTestCase() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/AirplaneModeMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/AirplaneModeMapperTest.kt new file mode 100644 index 000000000000..5a73fe28ee18 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/AirplaneModeMapperTest.kt @@ -0,0 +1,109 @@ +/* + * 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.tiles.impl + +import android.graphics.drawable.TestStubDrawable +import android.service.quicksettings.Tile +import android.widget.Switch +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.qs.tiles.impl.airplane.domain.AirplaneModeMapper +import com.android.systemui.qs.tiles.impl.airplane.domain.model.AirplaneModeTileModel +import com.android.systemui.qs.tiles.impl.airplane.qsAirplaneModeTileConfig +import com.android.systemui.qs.tiles.impl.custom.QSTileStateSubject +import com.android.systemui.qs.tiles.viewmodel.QSTileState +import com.android.systemui.res.R +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class AirplaneModeMapperTest : SysuiTestCase() { + private val kosmos = Kosmos() + private val airplaneModeConfig = kosmos.qsAirplaneModeTileConfig + + private lateinit var mapper: AirplaneModeMapper + + @Before + fun setup() { + mapper = + AirplaneModeMapper( + context.orCreateTestableResources + .apply { + addOverride(R.drawable.qs_airplane_icon_off, TestStubDrawable()) + addOverride(R.drawable.qs_airplane_icon_on, TestStubDrawable()) + } + .resources, + context.theme, + ) + } + + @Test + fun enabledModel_mapsCorrectly() { + val inputModel = AirplaneModeTileModel(true) + + val outputState = mapper.map(airplaneModeConfig, inputModel) + + val expectedState = + createAirplaneModeState( + QSTileState.ActivationState.ACTIVE, + context.resources.getStringArray(R.array.tile_states_airplane)[Tile.STATE_ACTIVE], + R.drawable.qs_airplane_icon_on + ) + QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState) + } + + @Test + fun disabledModel_mapsCorrectly() { + val inputModel = AirplaneModeTileModel(false) + + val outputState = mapper.map(airplaneModeConfig, inputModel) + + val expectedState = + createAirplaneModeState( + QSTileState.ActivationState.INACTIVE, + context.resources.getStringArray(R.array.tile_states_airplane)[Tile.STATE_INACTIVE], + R.drawable.qs_airplane_icon_off + ) + QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState) + } + + private fun createAirplaneModeState( + activationState: QSTileState.ActivationState, + secondaryLabel: String, + iconRes: Int + ): QSTileState { + val label = context.getString(R.string.airplane_mode) + return QSTileState( + { Icon.Loaded(context.getDrawable(iconRes)!!, null) }, + iconRes, + label, + activationState, + secondaryLabel, + setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK), + label, + null, + QSTileState.SideViewIcon.None, + QSTileState.EnabledState.ENABLED, + Switch::class.qualifiedName + ) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileMapperTest.kt index b4ff56566c75..f1d08c068150 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileMapperTest.kt @@ -18,6 +18,7 @@ package com.android.systemui.qs.tiles.impl.custom.domain.interactor import android.app.IUriGrantsManager import android.content.ComponentName +import android.content.Context import android.graphics.drawable.Drawable import android.graphics.drawable.Icon import android.graphics.drawable.TestStubDrawable @@ -51,11 +52,13 @@ import org.junit.runner.RunWith class CustomTileMapperTest : SysuiTestCase() { private val uriGrantsManager: IUriGrantsManager = mock {} + private val mockContext = + mock<Context> { whenever(createContextAsUser(any(), any())).thenReturn(context) } private val kosmos = testKosmos().apply { customTileSpec = TileSpec.Companion.create(TEST_COMPONENT) } private val underTest by lazy { CustomTileMapper( - context = mock { whenever(createContextAsUser(any(), any())).thenReturn(context) }, + context = mockContext, uriGrantsManager = uriGrantsManager, ) } @@ -164,7 +167,7 @@ class CustomTileMapperTest : SysuiTestCase() { ) val expected = createTileState( - activationState = QSTileState.ActivationState.INACTIVE, + activationState = QSTileState.ActivationState.UNAVAILABLE, icon = DEFAULT_DRAWABLE, ) @@ -173,7 +176,7 @@ class CustomTileMapperTest : SysuiTestCase() { } @Test - fun failedToLoadIconTileIsInactive() = + fun failedToLoadIconTileIsUnavailable() = with(kosmos) { testScope.runTest { val actual = @@ -187,13 +190,32 @@ class CustomTileMapperTest : SysuiTestCase() { val expected = createTileState( icon = null, - activationState = QSTileState.ActivationState.INACTIVE, + activationState = QSTileState.ActivationState.UNAVAILABLE, ) assertThat(actual).isEqualTo(expected) } } + @Test + fun nullUserContextDoesNotCauseExceptionReturnsNullIconAndUnavailableState() = + with(kosmos) { + testScope.runTest { + // map() will catch this exception + whenever(mockContext.createContextAsUser(any(), any())) + .thenThrow(IllegalStateException("Unable to create userContext")) + + val actual = underTest.map(customTileQsTileConfig, createModel()) + + val expected = + createTileState( + icon = null, + activationState = QSTileState.ActivationState.UNAVAILABLE, + ) + assertThat(actual).isEqualTo(expected) + } + } + private fun Kosmos.createModel( tileState: Int = Tile.STATE_ACTIVE, tileIcon: Icon = createIcon(DRAWABLE, false), diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractorTest.kt index 13d6411382cf..1ea8abc9b3b3 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractorTest.kt @@ -204,7 +204,7 @@ class InternetTileDataInteractorTest : SysuiTestCase() { val actualIcon = latest?.icon assertThat(actualIcon).isEqualTo(expectedIcon) - assertThat(latest?.iconId).isNull() + assertThat(latest?.iconId).isEqualTo(WifiIcons.WIFI_NO_INTERNET_ICONS[4]) assertThat(latest?.contentDescription.loadContentDescription(context)) .isEqualTo("$internet,test ssid") val expectedSd = wifiIcon.contentDescription @@ -443,15 +443,15 @@ class InternetTileDataInteractorTest : SysuiTestCase() { * on the mentioned context. Since that context does not have a looper assigned to it, the * handler instantiation will throw a RuntimeException. * - * TODO(b/338068066): Robolectric behavior differs in that it does not throw the exception - * So either we should make Robolectric behvase similar to the device test, or change this - * test to look for a different signal than the exception, when run by Robolectric. For now - * we just assume the test is not Robolectric. + * TODO(b/338068066): Robolectric behavior differs in that it does not throw the exception So + * either we should make Robolectric behave similar to the device test, or change this test to + * look for a different signal than the exception, when run by Robolectric. For now we just + * assume the test is not Robolectric. */ @Test(expected = java.lang.RuntimeException::class) fun mobileDefault_usesNetworkNameAndIcon_throwsRunTimeException() = testScope.runTest { - assumeFalse(isRobolectricTest()); + assumeFalse(isRobolectricTest()) collectLastValue(underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))) 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/domain/interactor/ShadeInteractorSceneContainerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt index 8a4319805802..fadb1d7c91a1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt @@ -22,13 +22,13 @@ import com.android.compose.animation.scene.ObservableTransitionState import com.android.systemui.SysuiTestCase import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.shared.model.StatusBarState import com.android.systemui.kosmos.testScope -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.shadeTestUtil import com.android.systemui.testKosmos import com.google.common.truth.Truth import com.google.common.truth.Truth.assertThat @@ -43,6 +43,7 @@ import org.junit.runner.RunWith @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) +@EnableSceneContainer class ShadeInteractorSceneContainerImplTest : SysuiTestCase() { private val kosmos = testKosmos() @@ -50,7 +51,7 @@ class ShadeInteractorSceneContainerImplTest : SysuiTestCase() { private val configurationRepository = kosmos.fakeConfigurationRepository private val keyguardRepository = kosmos.fakeKeyguardRepository private val sceneInteractor = kosmos.sceneInteractor - private val shadeRepository = kosmos.shadeRepository + private val shadeTestUtil = kosmos.shadeTestUtil private val underTest = kosmos.shadeInteractorSceneContainerImpl @@ -60,7 +61,7 @@ class ShadeInteractorSceneContainerImplTest : SysuiTestCase() { val actual by collectLastValue(underTest.qsExpansion) // WHEN split shade is enabled and QS is expanded - overrideResource(R.bool.config_use_split_notification_shade, true) + shadeTestUtil.setSplitShade(true) configurationRepository.onAnyConfigurationChange() runCurrent() val transitionState = @@ -89,7 +90,7 @@ class ShadeInteractorSceneContainerImplTest : SysuiTestCase() { // WHEN split shade is not enabled and QS is expanded keyguardRepository.setStatusBarState(StatusBarState.SHADE) - overrideResource(R.bool.config_use_split_notification_shade, false) + shadeTestUtil.setSplitShade(false) configurationRepository.onAnyConfigurationChange() runCurrent() val progress = MutableStateFlow(.3f) 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/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt index 9fea7a2bfbf6..733cac99f4ec 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt @@ -56,6 +56,7 @@ import com.android.systemui.keyguard.ui.viewmodel.aodBurnInViewModel import com.android.systemui.keyguard.ui.viewmodel.keyguardRootViewModel import com.android.systemui.kosmos.testScope import com.android.systemui.res.R +import com.android.systemui.shade.data.repository.fakeShadeRepository import com.android.systemui.shade.mockLargeScreenHeaderHelper import com.android.systemui.shade.shadeTestUtil import com.android.systemui.statusbar.notification.stack.domain.interactor.sharedNotificationContainerInteractor @@ -135,11 +136,14 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S val communalSceneRepository get() = kosmos.communalSceneRepository + val shadeRepository + get() = kosmos.fakeShadeRepository + lateinit var underTest: SharedNotificationContainerViewModel @Before fun setUp() { - overrideResource(R.bool.config_use_split_notification_shade, false) + shadeTestUtil.setSplitShade(false) movementFlow = MutableStateFlow(BurnInModel()) whenever(aodBurnInViewModel.movement(any())).thenReturn(movementFlow) underTest = kosmos.sharedNotificationContainerViewModel @@ -148,7 +152,7 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S @Test fun validateMarginStartInSplitShade() = testScope.runTest { - overrideResource(R.bool.config_use_split_notification_shade, true) + shadeTestUtil.setSplitShade(true) overrideResource(R.dimen.notification_panel_margin_horizontal, 20) val dimens by collectLastValue(underTest.configurationBasedDimensions) @@ -161,7 +165,7 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S @Test fun validateMarginStart() = testScope.runTest { - overrideResource(R.bool.config_use_split_notification_shade, false) + shadeTestUtil.setSplitShade(false) overrideResource(R.dimen.notification_panel_margin_horizontal, 20) val dimens by collectLastValue(underTest.configurationBasedDimensions) @@ -172,10 +176,10 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S } @Test - fun validatePaddingTopInSplitShade_refactorFlagOn_usesLargeHeaderHelper() = + fun validatePaddingTopInSplitShade_usesLargeHeaderHelper() = testScope.runTest { whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()).thenReturn(5) - overrideResource(R.bool.config_use_split_notification_shade, true) + shadeTestUtil.setSplitShade(true) overrideResource(R.bool.config_use_large_screen_shade_header, true) overrideResource(R.dimen.large_screen_shade_header_height, 10) overrideResource(R.dimen.keyguard_split_shade_top_margin, 50) @@ -191,7 +195,7 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S fun validatePaddingTopInNonSplitShade_usesLargeScreenHeader() = testScope.runTest { whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()).thenReturn(10) - overrideResource(R.bool.config_use_split_notification_shade, false) + shadeTestUtil.setSplitShade(false) overrideResource(R.bool.config_use_large_screen_shade_header, true) overrideResource(R.dimen.large_screen_shade_header_height, 10) overrideResource(R.dimen.keyguard_split_shade_top_margin, 50) @@ -207,7 +211,7 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S fun validatePaddingTopInNonSplitShade_doesNotUseLargeScreenHeader() = testScope.runTest { whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()).thenReturn(10) - overrideResource(R.bool.config_use_split_notification_shade, false) + shadeTestUtil.setSplitShade(false) overrideResource(R.bool.config_use_large_screen_shade_header, false) overrideResource(R.dimen.large_screen_shade_header_height, 10) overrideResource(R.dimen.keyguard_split_shade_top_margin, 50) @@ -508,7 +512,7 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S val bounds by collectLastValue(underTest.bounds) // When not in split shade - overrideResource(R.bool.config_use_split_notification_shade, false) + shadeTestUtil.setSplitShade(false) configurationRepository.onAnyConfigurationChange() runCurrent() @@ -567,7 +571,7 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S // When in split shade whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()).thenReturn(5) - overrideResource(R.bool.config_use_split_notification_shade, true) + shadeTestUtil.setSplitShade(true) overrideResource(R.bool.config_use_large_screen_shade_header, true) overrideResource(R.dimen.large_screen_shade_header_height, 10) overrideResource(R.dimen.keyguard_split_shade_top_margin, 50) @@ -628,7 +632,7 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S advanceTimeBy(50L) showLockscreen() - overrideResource(R.bool.config_use_split_notification_shade, false) + shadeTestUtil.setSplitShade(false) configurationRepository.onAnyConfigurationChange() assertThat(maxNotifications).isEqualTo(10) @@ -656,7 +660,7 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S advanceTimeBy(50L) showLockscreen() - overrideResource(R.bool.config_use_split_notification_shade, false) + shadeTestUtil.setSplitShade(false) configurationRepository.onAnyConfigurationChange() assertThat(maxNotifications).isEqualTo(10) @@ -690,7 +694,7 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S // Show lockscreen with shade expanded showLockscreenWithShadeExpanded() - overrideResource(R.bool.config_use_split_notification_shade, false) + shadeTestUtil.setSplitShade(false) configurationRepository.onAnyConfigurationChange() // -1 means No Limit 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/multivalentTests/src/com/android/systemui/util/settings/SettingsProxyExtTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/settings/SettingsProxyExtTest.kt new file mode 100644 index 000000000000..e281894a93ab --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/settings/SettingsProxyExtTest.kt @@ -0,0 +1,165 @@ +/* + * 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.util.settings + +import android.database.ContentObserver +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.Flags +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.android.systemui.util.settings.SettingsProxyExt.observerFlow +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.any +import org.mockito.kotlin.times +import org.mockito.kotlin.verify + +/** Tests for [SettingsProxyExt]. */ +@RunWith(AndroidJUnit4::class) +@SmallTest +@OptIn(ExperimentalCoroutinesApi::class) +class SettingsProxyExtTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + @Mock lateinit var settingsProxy: SettingsProxy + @Mock lateinit var userSettingsProxy: UserSettingsProxy + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + } + + @Test + @EnableFlags(Flags.FLAG_SETTINGS_EXT_REGISTER_CONTENT_OBSERVER_ON_BG_THREAD) + fun observeFlow_bgFlagEnabled_settingsProxy_registerContentObserverInvoked() = + testScope.runTest { + val unused by collectLastValue(settingsProxy.observerFlow(SETTING_1, SETTING_2)) + runCurrent() + verify(settingsProxy, times(2)) + .registerContentObserver(any<String>(), any<ContentObserver>()) + } + + @Test + @DisableFlags(Flags.FLAG_SETTINGS_EXT_REGISTER_CONTENT_OBSERVER_ON_BG_THREAD) + fun observeFlow_bgFlagDisabled_multipleSettings_SettingsProxy_registerContentObserverInvoked() = + testScope.runTest { + val unused by collectLastValue(settingsProxy.observerFlow(SETTING_1, SETTING_2)) + runCurrent() + verify(settingsProxy, times(2)) + .registerContentObserverSync(any<String>(), any<ContentObserver>()) + } + + @Test + @EnableFlags(Flags.FLAG_SETTINGS_EXT_REGISTER_CONTENT_OBSERVER_ON_BG_THREAD) + fun observeFlow_bgFlagEnabled_channelClosed_settingsProxy_unregisterContentObserverInvoked() = + testScope.runTest { + val job = Job() + val unused by + collectLastValue(settingsProxy.observerFlow(SETTING_1, SETTING_2), context = job) + runCurrent() + job.cancel() + runCurrent() + verify(settingsProxy).unregisterContentObserverAsync(any<ContentObserver>()) + } + + @Test + @DisableFlags(Flags.FLAG_SETTINGS_EXT_REGISTER_CONTENT_OBSERVER_ON_BG_THREAD) + fun observeFlow_bgFlagDisabled_channelClosed_settingsProxy_unregisterContentObserverInvoked() = + testScope.runTest { + val job = Job() + val unused by + collectLastValue(settingsProxy.observerFlow(SETTING_1, SETTING_2), context = job) + runCurrent() + job.cancel() + runCurrent() + verify(settingsProxy).unregisterContentObserverSync(any<ContentObserver>()) + } + + @Test + @EnableFlags(Flags.FLAG_SETTINGS_EXT_REGISTER_CONTENT_OBSERVER_ON_BG_THREAD) + fun observeFlow_bgFlagEnabled_userSettingsProxy_registerContentObserverForUserInvoked() = + testScope.runTest { + val unused by + collectLastValue(userSettingsProxy.observerFlow(userId = 0, SETTING_1, SETTING_2)) + runCurrent() + verify(userSettingsProxy, times(2)) + .registerContentObserverForUser(any<String>(), any<ContentObserver>(), any<Int>()) + } + + @Test + @DisableFlags(Flags.FLAG_SETTINGS_EXT_REGISTER_CONTENT_OBSERVER_ON_BG_THREAD) + fun observeFlow_bgFlagDisabled_userSettingsProxy_registerContentObserverForUserInvoked() = + testScope.runTest { + val unused by + collectLastValue(userSettingsProxy.observerFlow(userId = 0, SETTING_1, SETTING_2)) + runCurrent() + verify(userSettingsProxy, times(2)) + .registerContentObserverForUserSync( + any<String>(), + any<ContentObserver>(), + any<Int>() + ) + } + + @Test + @EnableFlags(Flags.FLAG_SETTINGS_EXT_REGISTER_CONTENT_OBSERVER_ON_BG_THREAD) + fun observeFlow_bgFlagEnabled_channelClosed_userSettingsProxy_unregisterContentObserverInvoked() = + testScope.runTest { + val job = Job() + val unused by + collectLastValue( + userSettingsProxy.observerFlow(userId = 0, SETTING_1, SETTING_2), + context = job + ) + runCurrent() + job.cancel() + runCurrent() + verify(userSettingsProxy).unregisterContentObserverAsync(any<ContentObserver>()) + } + + @Test + @DisableFlags(Flags.FLAG_SETTINGS_EXT_REGISTER_CONTENT_OBSERVER_ON_BG_THREAD) + fun observeFlow_bgFlagDisabled_channelClosed_userSettingsProxy_unregisterContentObserverInvoked() = + testScope.runTest { + val job = Job() + val unused by + collectLastValue( + userSettingsProxy.observerFlow(userId = 0, SETTING_1, SETTING_2), + context = job + ) + runCurrent() + job.cancel() + runCurrent() + verify(userSettingsProxy).unregisterContentObserverSync(any<ContentObserver>()) + } + + private companion object { + val SETTING_1 = "settings_1" + val SETTING_2 = "settings_2" + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractorImplTest.kt index ab184abdc963..f232d52615a4 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractorImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractorImplTest.kt @@ -28,6 +28,7 @@ import com.android.systemui.volume.panel.domain.model.ComponentModel import com.android.systemui.volume.panel.domain.unavailableCriteria import com.android.systemui.volume.panel.shared.model.VolumePanelComponentKey import com.android.systemui.volume.panel.ui.composable.enabledComponents +import com.android.systemui.volume.shared.volumePanelLogger import com.google.common.truth.Truth.assertThat import javax.inject.Provider import kotlinx.coroutines.test.runTest @@ -49,6 +50,7 @@ class ComponentsInteractorImplTest : SysuiTestCase() { enabledComponents, { defaultCriteria }, testScope.backgroundScope, + volumePanelLogger, criteriaByKey, ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModelTest.kt index 420b955e88e0..51a70bda6034 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModelTest.kt @@ -24,21 +24,30 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.broadcastDispatcher import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.dump.DumpManager import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testScope +import com.android.systemui.statusbar.policy.configurationController import com.android.systemui.statusbar.policy.fakeConfigurationController import com.android.systemui.testKosmos +import com.android.systemui.volume.panel.dagger.factory.volumePanelComponentFactory import com.android.systemui.volume.panel.data.repository.volumePanelGlobalStateRepository import com.android.systemui.volume.panel.domain.interactor.criteriaByKey +import com.android.systemui.volume.panel.domain.interactor.volumePanelGlobalStateInteractor import com.android.systemui.volume.panel.domain.unavailableCriteria import com.android.systemui.volume.panel.shared.model.VolumePanelComponentKey import com.android.systemui.volume.panel.shared.model.mockVolumePanelUiComponentProvider import com.android.systemui.volume.panel.ui.composable.componentByKey import com.android.systemui.volume.panel.ui.layout.DefaultComponentsLayoutManager import com.android.systemui.volume.panel.ui.layout.componentsLayoutManager +import com.android.systemui.volume.shared.volumePanelLogger import com.google.common.truth.Truth.assertThat +import java.io.PrintWriter +import java.io.StringWriter import javax.inject.Provider import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.launch import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Test @@ -55,6 +64,7 @@ class VolumePanelViewModelTest : SysuiTestCase() { volumePanelGlobalStateRepository.updateVolumePanelState { it.copy(isVisible = true) } } + private val realDumpManager = DumpManager() private val testableResources = context.orCreateTestableResources private lateinit var underTest: VolumePanelViewModel @@ -124,6 +134,60 @@ class VolumePanelViewModelTest : SysuiTestCase() { } @Test + fun testDumpableRegister_unregister() = + with(kosmos) { + testScope.runTest { + val job = launch { + applicationCoroutineScope = this + underTest = createViewModel() + + runCurrent() + + assertThat(realDumpManager.getDumpables().any { it.name == DUMPABLE_NAME }) + .isTrue() + } + + runCurrent() + job.cancel() + + assertThat(realDumpManager.getDumpables().any { it.name == DUMPABLE_NAME }).isTrue() + } + } + + @Test + fun testDumpingState() = + test({ + componentByKey = + mapOf( + COMPONENT_1 to mockVolumePanelUiComponentProvider, + COMPONENT_2 to mockVolumePanelUiComponentProvider, + BOTTOM_BAR to mockVolumePanelUiComponentProvider, + ) + criteriaByKey = mapOf(COMPONENT_2 to Provider { unavailableCriteria }) + }) { + testScope.runTest { + runCurrent() + + StringWriter().use { + underTest.dump(PrintWriter(it), emptyArray()) + + assertThat(it.buffer.toString()) + .isEqualTo( + "volumePanelState=" + + "VolumePanelState(orientation=1, isLargeScreen=false)\n" + + "componentsLayout=( " + + "headerComponents= " + + "contentComponents=" + + "test_component:1:visible=true, " + + "test_component:2:visible=false " + + "footerComponents= " + + "bottomBarComponent=test_bottom_bar:visible=true )\n" + ) + } + } + } + + @Test fun dismissBroadcast_dismissesPanel() = test { testScope.runTest { runCurrent() // run the flows to let allow the receiver to be registered @@ -140,11 +204,26 @@ class VolumePanelViewModelTest : SysuiTestCase() { private fun test(setup: Kosmos.() -> Unit = {}, test: Kosmos.() -> Unit) = with(kosmos) { setup() - underTest = volumePanelViewModel + underTest = createViewModel() + test() } + private fun Kosmos.createViewModel(): VolumePanelViewModel = + VolumePanelViewModel( + context.orCreateTestableResources.resources, + applicationCoroutineScope, + volumePanelComponentFactory, + configurationController, + broadcastDispatcher, + realDumpManager, + volumePanelLogger, + volumePanelGlobalStateInteractor, + ) + private companion object { + const val DUMPABLE_NAME = "VolumePanelViewModel" + const val BOTTOM_BAR: VolumePanelComponentKey = "test_bottom_bar" const val COMPONENT_1: VolumePanelComponentKey = "test_component:1" const val COMPONENT_2: VolumePanelComponentKey = "test_component:2" diff --git a/packages/SystemUI/res/drawable/ic_bugreport.xml b/packages/SystemUI/res/drawable/ic_bugreport.xml index ed1c6c723543..badbd8845050 100644 --- a/packages/SystemUI/res/drawable/ic_bugreport.xml +++ b/packages/SystemUI/res/drawable/ic_bugreport.xml @@ -19,14 +19,14 @@ android:height="24.0dp" android:viewportWidth="24.0" android:viewportHeight="24.0" - android:tint="?attr/colorControlNormal"> + android:tint="?android:attr/colorControlNormal"> <path - android:fillColor="#FF000000" + android:fillColor="#FFFFFFFF" android:pathData="M20,10V8h-2.81c-0.45,-0.78 -1.07,-1.46 -1.82,-1.96L17,4.41L15.59,3l-2.17,2.17c-0.03,-0.01 -0.05,-0.01 -0.08,-0.01c-0.16,-0.04 -0.32,-0.06 -0.49,-0.09c-0.06,-0.01 -0.11,-0.02 -0.17,-0.03C12.46,5.02 12.23,5 12,5h0c-0.49,0 -0.97,0.07 -1.42,0.18l0.02,-0.01L8.41,3L7,4.41l1.62,1.63l0.01,0C7.88,6.54 7.26,7.22 6.81,8H4v2h2.09C6.03,10.33 6,10.66 6,11v1H4v2h2v1c0,0.34 0.04,0.67 0.09,1H4v2h2.81c1.04,1.79 2.97,3 5.19,3h0c2.22,0 4.15,-1.21 5.19,-3H20v-2h-2.09l0,0c0.05,-0.33 0.09,-0.66 0.09,-1v-1h2v-2h-2v-1c0,-0.34 -0.04,-0.67 -0.09,-1l0,0H20zM16,15c0,2.21 -1.79,4 -4,4c-2.21,0 -4,-1.79 -4,-4v-4c0,-2.21 1.79,-4 4,-4h0c2.21,0 4,1.79 4,4V15z"/> <path - android:fillColor="#FF000000" + android:fillColor="#FFFFFFFF" android:pathData="M10,14h4v2h-4z"/> <path - android:fillColor="#FF000000" + android:fillColor="#FFFFFFFF" android:pathData="M10,10h4v2h-4z"/> </vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/biometric_prompt_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_layout.xml deleted file mode 100644 index ff89ed9e6e7a..000000000000 --- a/packages/SystemUI/res/layout/biometric_prompt_layout.xml +++ /dev/null @@ -1,208 +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. - --> -<com.android.systemui.biometrics.ui.BiometricPromptLayout - xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/contents" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:orientation="vertical"> - - <ImageView - android:id="@+id/logo" - android:layout_width="@dimen/biometric_auth_icon_size" - android:layout_height="@dimen/biometric_auth_icon_size" - android:layout_gravity="center" - android:scaleType="fitXY"/> - - <TextView - android:id="@+id/logo_description" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:gravity="@integer/biometric_dialog_text_gravity" - android:singleLine="true" - android:marqueeRepeatLimit="1" - android:ellipsize="marquee"/> - - <TextView - android:id="@+id/title" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:gravity="@integer/biometric_dialog_text_gravity" - android:singleLine="true" - android:marqueeRepeatLimit="1" - android:ellipsize="marquee" - style="@style/TextAppearance.AuthCredential.OldTitle"/> - - <TextView - android:id="@+id/subtitle" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:gravity="@integer/biometric_dialog_text_gravity" - android:singleLine="true" - android:marqueeRepeatLimit="1" - android:ellipsize="marquee" - style="@style/TextAppearance.AuthCredential.OldSubtitle"/> - - <TextView - android:id="@+id/description" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:gravity="@integer/biometric_dialog_text_gravity" - android:scrollbars ="vertical" - android:importantForAccessibility="no" - style="@style/TextAppearance.AuthCredential.OldDescription"/> - - <Space - android:id="@+id/space_above_content" - android:layout_width="match_parent" - android:layout_height="24dp" - android:visibility="gone" /> - - <LinearLayout - android:id="@+id/customized_view_container" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:fadeScrollbars="false" - android:gravity="center_vertical" - android:orientation="vertical" - android:scrollbars="vertical" - android:visibility="gone" /> - - <Space android:id="@+id/space_above_icon" - android:layout_width="match_parent" - android:layout_height="48dp" /> - - <FrameLayout - android:id="@+id/biometric_icon_frame" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center"> - - <com.android.systemui.biometrics.BiometricPromptLottieViewWrapper - android:id="@+id/biometric_icon" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center" - android:contentDescription="@null" - android:scaleType="fitXY" /> - - <com.android.systemui.biometrics.BiometricPromptLottieViewWrapper - android:id="@+id/biometric_icon_overlay" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center" - android:contentDescription="@null" - android:scaleType="fitXY" /> - </FrameLayout> - - <!-- For sensors such as UDFPS, this view is used during custom measurement/layout to add extra - padding so that the biometric icon is always in the right physical position. --> - <Space android:id="@+id/space_below_icon" - android:layout_width="match_parent" - android:layout_height="12dp" /> - - <TextView - android:id="@+id/indicator" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:paddingHorizontal="24dp" - android:textSize="12sp" - android:gravity="center_horizontal" - android:accessibilityLiveRegion="polite" - android:singleLine="true" - android:ellipsize="marquee" - android:marqueeRepeatLimit="marquee_forever" - android:scrollHorizontally="true" - android:fadingEdge="horizontal" - android:textColor="@color/biometric_dialog_gray"/> - - <LinearLayout - android:id="@+id/button_bar" - android:layout_width="match_parent" - android:layout_height="88dp" - style="?android:attr/buttonBarStyle" - android:orientation="horizontal" - android:paddingTop="24dp"> - - <Space android:id="@+id/leftSpacer" - android:layout_width="8dp" - android:layout_height="match_parent" - android:visibility="visible" /> - - <!-- Negative Button, reserved for app --> - <Button android:id="@+id/button_negative" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - style="@*android:style/Widget.DeviceDefault.Button.Borderless.Colored" - android:layout_gravity="center_vertical" - android:ellipsize="end" - android:maxLines="2" - android:maxWidth="@dimen/biometric_dialog_button_negative_max_width" - android:visibility="gone"/> - <!-- Cancel Button, replaces negative button when biometric is accepted --> - <Button android:id="@+id/button_cancel" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - style="@*android:style/Widget.DeviceDefault.Button.Borderless.Colored" - android:layout_gravity="center_vertical" - android:maxWidth="@dimen/biometric_dialog_button_negative_max_width" - android:text="@string/cancel" - android:visibility="gone"/> - <!-- "Use Credential" Button, replaces if device credential is allowed --> - <Button android:id="@+id/button_use_credential" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - style="@*android:style/Widget.DeviceDefault.Button.Borderless.Colored" - android:layout_gravity="center_vertical" - android:maxWidth="@dimen/biometric_dialog_button_negative_max_width" - android:visibility="gone"/> - - <Space android:id="@+id/middleSpacer" - android:layout_width="0dp" - android:layout_height="match_parent" - android:layout_weight="1" - android:visibility="visible"/> - - <!-- Positive Button --> - <Button android:id="@+id/button_confirm" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - style="@*android:style/Widget.DeviceDefault.Button.Colored" - android:layout_gravity="center_vertical" - android:ellipsize="end" - android:maxLines="2" - android:maxWidth="@dimen/biometric_dialog_button_positive_max_width" - android:text="@string/biometric_dialog_confirm" - android:visibility="gone"/> - <!-- Try Again Button --> - <Button android:id="@+id/button_try_again" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - style="@*android:style/Widget.DeviceDefault.Button.Colored" - android:layout_gravity="center_vertical" - android:ellipsize="end" - android:maxLines="2" - android:maxWidth="@dimen/biometric_dialog_button_positive_max_width" - android:text="@string/biometric_dialog_try_again" - android:visibility="gone"/> - - <Space android:id="@+id/rightSpacer" - android:layout_width="8dp" - android:layout_height="match_parent" - android:visibility="visible" /> - </LinearLayout> - -</com.android.systemui.biometrics.ui.BiometricPromptLayout> diff --git a/packages/SystemUI/res/layout/chipbar.xml b/packages/SystemUI/res/layout/chipbar.xml index 8fa975be43d2..e1b8ab469765 100644 --- a/packages/SystemUI/res/layout/chipbar.xml +++ b/packages/SystemUI/res/layout/chipbar.xml @@ -49,7 +49,7 @@ android:alpha="0.0" /> - <!-- LINT.IfChange textColor --> + <!-- LINT.IfChange --> <TextView android:id="@+id/text" android:layout_width="0dp" @@ -78,7 +78,7 @@ android:layout_height="@dimen/chipbar_end_icon_size" android:layout_marginStart="@dimen/chipbar_end_item_start_margin" android:src="@drawable/ic_warning" - android:tint="@color/GM2_red_600" + android:tint="@color/GM2_red_800" android:alpha="0.0" /> diff --git a/packages/SystemUI/res/layout/custom_trace_settings_dialog.xml b/packages/SystemUI/res/layout/custom_trace_settings_dialog.xml index 6180bf500d85..9e84052956dc 100644 --- a/packages/SystemUI/res/layout/custom_trace_settings_dialog.xml +++ b/packages/SystemUI/res/layout/custom_trace_settings_dialog.xml @@ -52,7 +52,8 @@ android:layout_weight="1" android:layout_gravity="fill_vertical" android:gravity="start" - android:textAppearance="@style/TextAppearance.Dialog.Body.Message" /> + android:textAppearance="@style/TextAppearance.Dialog.Body.Message" + android:importantForAccessibility="no" /> <Switch android:id="@+id/attach_to_bugreport_switch" @@ -80,7 +81,8 @@ android:layout_weight="1" android:layout_gravity="fill_vertical" android:gravity="start" - android:textAppearance="@style/TextAppearance.Dialog.Body.Message"/> + android:textAppearance="@style/TextAppearance.Dialog.Body.Message" + android:importantForAccessibility="no" /> <Switch android:id="@+id/winscope_switch" @@ -108,7 +110,8 @@ android:layout_weight="1" android:layout_gravity="fill_vertical" android:gravity="start" - android:textAppearance="@style/TextAppearance.Dialog.Body.Message" /> + android:textAppearance="@style/TextAppearance.Dialog.Body.Message" + android:importantForAccessibility="no" /> <Switch android:id="@+id/trace_debuggable_apps_switch" @@ -136,7 +139,8 @@ android:layout_weight="1" android:layout_gravity="fill_vertical" android:gravity="start" - android:textAppearance="@style/TextAppearance.Dialog.Body.Message" /> + android:textAppearance="@style/TextAppearance.Dialog.Body.Message" + android:importantForAccessibility="no" /> <Switch android:id="@+id/long_traces_switch" diff --git a/packages/SystemUI/res/raw/action_key_edu.json b/packages/SystemUI/res/raw/action_key_edu.json new file mode 100644 index 000000000000..014d83798da9 --- /dev/null +++ b/packages/SystemUI/res/raw/action_key_edu.json @@ -0,0 +1 @@ +{"v":"5.12.1","fr":60,"ip":0,"op":181,"w":554,"h":564,"nm":"Trackpad-JSON_ActionKey-EDU","ddd":0,"assets":[{"id":"comp_0","nm":"BlankButton","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".secondaryFixedDim","cl":"secondaryFixedDim","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[40,39.79,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[80,79.581]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":14.032},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.850980401039,0.768627464771,0.627451002598,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"shortcut symbols","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".onSecondaryFixedVariant","cl":"onSecondaryFixedVariant","sr":1,"ks":{"o":{"a":0,"k":80},"r":{"a":0,"k":0},"p":{"a":0,"k":[40,49.21,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[80,79.581]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":14.032},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.364705890417,0.258823543787,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"shadow","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0}]},{"id":"comp_1","nm":"actionKey_themed","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".onSecondaryFixed","cl":"onSecondaryFixed","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[0.288,-0.035,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0.579,-0.158],[0.605,0],[1.21,1.21],[0,1.684],[-1.184,1.184],[-1.684,0],[-1.21,-1.21],[0,-1.71],[0.184,-0.553],[0.316,-0.474],[0,0],[0,0]],"o":[[-0.474,0.316],[-0.553,0.158],[-1.684,0],[-1.184,-1.21],[0,-1.71],[1.21,-1.21],[1.71,0],[1.21,1.184],[0,0.605],[-0.158,0.553],[0,0],[0,0],[0,0]],"v":[[10.241,12.155],[8.663,12.866],[6.926,13.103],[2.585,11.287],[0.809,6.946],[2.585,2.605],[6.926,0.789],[11.307,2.605],[13.122,6.946],[12.846,8.682],[12.136,10.222],[16.911,14.997],[15.017,16.891]],"c":true}},"nm":"Path 1","hd":false},{"ind":1,"ty":"sh","ks":{"a":0,"k":{"i":[[1.21,1.21],[0,1.684],[-1.21,1.184],[-1.684,0],[-1.184,-1.21],[0,-1.736],[1.21,-1.21],[1.736,0]],"o":[[-1.21,-1.21],[0,-1.736],[1.21,-1.21],[1.736,0],[1.21,1.184],[0,1.684],[-1.184,1.21],[-1.684,0]],"v":[[-15.096,11.327],[-16.911,6.985],[-15.096,2.605],[-10.754,0.789],[-6.374,2.605],[-4.558,6.985],[-6.374,11.327],[-10.754,13.142]],"c":true}},"nm":"Path 2","hd":false},{"ind":2,"ty":"sh","ks":{"a":0,"k":{"i":[[-0.684,0.658],[0,0.947],[0.684,0.684],[0.973,0],[0.684,-0.684],[0,-0.973],[-0.658,-0.684],[-0.947,0]],"o":[[0.684,-0.684],[0,-0.973],[-0.684,-0.684],[-0.947,0],[-0.658,0.684],[0,0.947],[0.684,0.658],[0.973,0]],"v":[[-8.268,9.432],[-7.242,6.985],[-8.268,4.499],[-10.754,3.473],[-13.201,4.499],[-14.188,6.985],[-13.201,9.432],[-10.754,10.419]],"c":true}},"nm":"Path 3","hd":false},{"ind":3,"ty":"sh","ks":{"a":0,"k":{"i":[[-0.684,0.658],[0,0.947],[0.684,0.684],[0.973,0],[0.684,-0.684],[0,-0.973],[-0.684,-0.684],[-0.947,0]],"o":[[0.684,-0.684],[0,-0.973],[-0.684,-0.684],[-0.947,0],[-0.684,0.684],[0,0.947],[0.684,0.658],[0.973,0]],"v":[[9.413,9.432],[10.439,6.985],[9.413,4.499],[6.926,3.473],[4.479,4.499],[3.453,6.985],[4.479,9.432],[6.926,10.419]],"c":true}},"nm":"Path 4","hd":false},{"ind":4,"ty":"sh","ks":{"a":0,"k":{"i":[[1.21,1.21],[0,1.684],[-1.21,1.21],[-1.684,0],[-1.184,-1.21],[0,-1.71],[1.21,-1.21],[1.736,0]],"o":[[-1.21,-1.21],[0,-1.71],[1.21,-1.21],[1.736,0],[1.21,1.21],[0,1.684],[-1.184,1.21],[-1.684,0]],"v":[[-15.096,-6.354],[-16.911,-10.695],[-15.096,-15.076],[-10.754,-16.891],[-6.374,-15.076],[-4.558,-10.695],[-6.374,-6.354],[-10.754,-4.539]],"c":true}},"nm":"Path 5","hd":false},{"ind":5,"ty":"sh","ks":{"a":0,"k":{"i":[[1.21,1.21],[0,1.684],[-1.21,1.21],[-1.684,0],[-1.21,-1.21],[0,-1.71],[1.21,-1.21],[1.71,0]],"o":[[-1.21,-1.21],[0,-1.71],[1.21,-1.21],[1.71,0],[1.21,1.21],[0,1.684],[-1.21,1.21],[-1.684,0]],"v":[[2.585,-6.354],[0.77,-10.695],[2.585,-15.076],[6.926,-16.891],[11.307,-15.076],[13.122,-10.695],[11.307,-6.354],[6.926,-4.539]],"c":true}},"nm":"Path 6","hd":false},{"ind":6,"ty":"sh","ks":{"a":0,"k":{"i":[[-0.684,0.684],[0,0.947],[0.684,0.684],[0.973,0],[0.684,-0.684],[0,-0.973],[-0.658,-0.684],[-0.947,0]],"o":[[0.684,-0.684],[0,-0.973],[-0.684,-0.684],[-0.947,0],[-0.658,0.684],[0,0.947],[0.684,0.684],[0.973,0]],"v":[[-8.268,-8.248],[-7.242,-10.695],[-8.268,-13.182],[-10.754,-14.208],[-13.201,-13.182],[-14.188,-10.695],[-13.201,-8.248],[-10.754,-7.222]],"c":true}},"nm":"Path 7","hd":false},{"ind":7,"ty":"sh","ks":{"a":0,"k":{"i":[[-0.684,0.684],[0,0.947],[0.684,0.684],[0.973,0],[0.684,-0.684],[0,-0.973],[-0.684,-0.684],[-0.947,0]],"o":[[0.684,-0.684],[0,-0.973],[-0.684,-0.684],[-0.947,0],[-0.684,0.684],[0,0.947],[0.684,0.684],[0.973,0]],"v":[[9.413,-8.248],[10.439,-10.695],[9.413,-13.182],[6.926,-14.208],[4.479,-13.182],[3.453,-10.695],[4.479,-8.248],[6.926,-7.222]],"c":true}},"nm":"Path 8","hd":false},{"ty":"fl","c":{"a":0,"k":[0.145098045468,0.101960785687,0.015686275437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"icon","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primaryFixedDim","cl":"primaryFixedDim","parent":3,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.64],"y":[1]},"o":{"x":[0.33],"y":[0.52]},"t":34,"s":[0]},{"i":{"x":[0.64],"y":[1]},"o":{"x":[0.36],"y":[0]},"t":37,"s":[100]},{"i":{"x":[0.64],"y":[0.48]},"o":{"x":[0.36],"y":[0]},"t":40,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":46,"s":[0]},{"i":{"x":[0.64],"y":[1]},"o":{"x":[0.33],"y":[0.52]},"t":124,"s":[0]},{"i":{"x":[0.64],"y":[1]},"o":{"x":[0.36],"y":[0]},"t":127,"s":[100]},{"i":{"x":[0.64],"y":[0.48]},"o":{"x":[0.36],"y":[0]},"t":130,"s":[100]},{"t":136,"s":[0]}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[80,79.581]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":14.032},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.92549020052,0.752941191196,0.423529416323,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"shortcut symbols","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".secondaryFixedDim","cl":"secondaryFixedDim","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":1,"k":[{"i":{"x":0.072,"y":0.635},"o":{"x":0.424,"y":0.112},"t":27,"s":[40,39.79,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0,"y":1},"o":{"x":0.313,"y":0.131},"t":39,"s":[40,49.21,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":57,"s":[40,39.79,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.07,"y":0.63},"o":{"x":0.42,"y":0.11},"t":117,"s":[40,39.79,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0,"y":1},"o":{"x":0.313,"y":0.131},"t":129,"s":[40,49.21,0],"to":[0,0,0],"ti":[0,0,0]},{"t":147,"s":[40,39.79,0]}]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[80,79.581]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":14.032},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.850980392157,0.76862745098,0.627450980392,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"shortcut symbols","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".onSecondaryFixedVariant","cl":"onSecondaryFixedVariant","sr":1,"ks":{"o":{"a":0,"k":80},"r":{"a":0,"k":0},"p":{"a":0,"k":[40,49.21,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[80,79.581]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":14.032},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.364705890417,0.258823543787,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"shadow","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0}]},{"id":"comp_2","nm":"AllApps_Tray_themed","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".onSecondaryFixedVariant","cl":"onSecondaryFixedVariant","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[0,-8.859,0]},"a":{"a":0,"k":[277,256.562,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Safety app","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[400.047,330.391]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 4 App - 6","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Settings","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[350.828,330.391]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 4 App - 5","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"SoundCloud","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[301.609,330.391]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 4 App - 4","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Starbucks","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[252.391,330.391]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 4 App - 3","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Snapchat","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[203.172,330.391]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 4 App - 2","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Spotify","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[153.953,330.391]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 4 App - 1","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Safety app","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[400.047,281.172]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 3 App - 6","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Settings","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[350.828,281.172]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 3 App - 5","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"SoundCloud","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[301.609,281.172]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 3 App - 4","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Starbucks","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[252.391,281.172]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 3 App - 3","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Snapchat","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[203.172,281.172]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 3 App - 2","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Spotify","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[153.953,281.172]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 3 App - 1","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Safety app","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[400.047,231.953]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 2 App - 6","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Settings","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[350.828,231.953]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 2 App - 5","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"SoundCloud","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[301.609,231.953]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 2 App - 4","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Starbucks","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[252.391,231.953]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 2 App - 3","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Snapchat","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[203.172,231.953]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 2 App - 2","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Spotify","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[153.953,231.953]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 2 App - 1","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Safety app","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[400.047,182.734]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 1 App - 6","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Settings","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[350.828,182.734]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 1 App - 5","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"SoundCloud","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[301.609,182.734]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 1 App - 4","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Starbucks","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[252.391,182.734]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 1 App - 3","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Snapchat","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[203.172,182.734]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 1 App - 2","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Spotify","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[153.953,182.734]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 1 App - 1","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".secondaryFixedDim","cl":"secondaryFixedDim","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[0,-129.938,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[275.625,25.594]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.850980401039,0.768627464771,0.627451002598,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"560x52","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".secondaryFixedDim","cl":"secondaryFixedDim","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[0,-151.594,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[15.75,1.969]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.850980401039,0.768627464771,0.627451002598,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"handle","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".onSecondaryFixed","cl":"onSecondaryFixed","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":1,"k":[{"i":{"x":0.1,"y":1},"o":{"x":0.05,"y":0.7},"t":45,"s":[277,516,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.214,"y":0.214},"t":75,"s":[277,265.422,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.999,"y":1},"o":{"x":0.3,"y":0},"t":135,"s":[277,265.422,0],"to":[0,0,0],"ti":[0,0,0]},{"t":147,"s":[277,516,0]}]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[316.969,320.906]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":13.78},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.145098045468,0.101960785687,0.015686275437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"all apps","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0}]},{"id":"comp_3","nm":"AK_LofiLauncher","fr":60,"pfr":1,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Scale Down","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":45,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":51,"s":[50]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":135,"s":[50]},{"t":144,"s":[100]}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[252,157.5,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"i":{"x":[0.1,0.1,0.1],"y":[1,1,1]},"o":{"x":[0.153,0.153,0.153],"y":[0.074,0.074,0]},"t":45,"s":[100,100,100]},{"i":{"x":[0.841,0.841,0.841],"y":[1,1,1]},"o":{"x":[0.161,0.161,0.161],"y":[0,0,0]},"t":75,"s":[95,95,100]},{"i":{"x":[0,0,0],"y":[1,1,1]},"o":{"x":[0.2,0.2,0.2],"y":[0,0,0]},"t":135,"s":[95,95,100]},{"t":165,"s":[100,100,100]}]}},"ao":0,"ef":[{"ty":5,"nm":"Void","np":19,"mn":"Pseudo/250958","ix":1,"en":1,"ef":[{"ty":0,"nm":"Width","mn":"Pseudo/250958-0001","ix":1,"v":{"a":0,"k":100}},{"ty":0,"nm":"Height","mn":"Pseudo/250958-0002","ix":2,"v":{"a":0,"k":100}},{"ty":0,"nm":"Offset X","mn":"Pseudo/250958-0003","ix":3,"v":{"a":0,"k":0}},{"ty":0,"nm":"Offset Y","mn":"Pseudo/250958-0004","ix":4,"v":{"a":0,"k":0}},{"ty":0,"nm":"Roundness","mn":"Pseudo/250958-0005","ix":5,"v":{"a":0,"k":0}},{"ty":6,"nm":"About","mn":"Pseudo/250958-0006","ix":6,"v":0},{"ty":6,"nm":"Plague of null layers.","mn":"Pseudo/250958-0007","ix":7,"v":0},{"ty":6,"nm":"Void","mn":"Pseudo/250958-0008","ix":8,"v":0},{"ty":6,"nm":"Following projects","mn":"Pseudo/250958-0009","ix":9,"v":0},{"ty":6,"nm":"Void","mn":"Pseudo/250958-0010","ix":10,"v":0},{"ty":6,"nm":"through time.","mn":"Pseudo/250958-0011","ix":11,"v":0},{"ty":6,"nm":"Void","mn":"Pseudo/250958-0012","ix":12,"v":0},{"ty":6,"nm":"Be free of the past.","mn":"Pseudo/250958-0013","ix":13,"v":0},{"ty":6,"nm":"Void","mn":"Pseudo/250958-0014","ix":14,"v":0},{"ty":6,"nm":"Copyright 2023 Battle Axe Inc","mn":"Pseudo/250958-0015","ix":15,"v":0},{"ty":6,"nm":"Void","mn":"Pseudo/250958-0016","ix":16,"v":0},{"ty":6,"nm":"Void","mn":"Pseudo/250958-0017","ix":17,"v":0}]}],"shapes":[],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".onSecondaryFixedVariant","cl":"onSecondaryFixedVariant","parent":1,"sr":1,"ks":{"o":{"k":[{"s":[100],"t":45,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[70],"t":51,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[70],"t":135,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[100],"t":144,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[0,117.5,0]},"a":{"a":0,"k":[252,275,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[444,275]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"hotseat - 5","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[396,275]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"hotseat - 4","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[348,275]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"hotseat - 3","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[300,275]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"hotseat - 2","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[252,275]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"hotseat - 1","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[168,20]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":15},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"qsb","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[132,275]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"qsb","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".onSecondaryFixedVariant","cl":"onSecondaryFixedVariant","parent":1,"sr":1,"ks":{"o":{"k":[{"s":[100],"t":45,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[70],"t":51,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[70],"t":135,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[100],"t":144,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[0,-29.497,0]},"a":{"a":0,"k":[252,128.003,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[20.144,20.144],[20.144,-20.144],[0,0],[-20.144,-20.144],[-20.144,20.144],[0,0]],"o":[[-20.144,-20.144],[0,0],[-20.144,20.144],[20.144,20.144],[0,0],[20.144,-20.144]],"v":[[44.892,-44.892],[-28.057,-44.892],[-44.892,-28.057],[-44.892,44.892],[28.057,44.892],[44.892,28.057]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"widgets","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[108,152.004]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"widgets weather","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[4.782,-2.684],[0,0],[2.63,-0.033],[0,0],[2.807,-4.716],[0,0],[2.263,-1.343],[0,0],[0.066,-5.485],[0,0],[1.292,-2.295],[0,0],[-2.683,-4.784],[0,0],[-0.033,-2.63],[0,0],[-4.716,-2.807],[0,0],[-1.338,-2.263],[0,0],[-5.483,-0.066],[0,0],[-2.296,-1.292],[0,0],[-4.782,2.683],[0,0],[-2.63,0.033],[0,0],[-2.807,4.716],[0,0],[-2.263,1.338],[0,0],[-0.066,5.483],[0,0],[-1.292,2.295],[0,0],[2.683,4.784],[0,0],[0.033,2.631],[0,0],[4.716,2.801],[0,0],[1.338,2.262],[0,0],[5.483,0.068],[0,0],[2.296,1.287]],"o":[[-4.782,-2.684],[0,0],[-2.296,1.287],[0,0],[-5.483,0.068],[0,0],[-1.338,2.262],[0,0],[-4.716,2.801],[0,0],[-0.033,2.631],[0,0],[-2.683,4.784],[0,0],[1.292,2.295],[0,0],[0.066,5.483],[0,0],[2.263,1.338],[0,0],[2.807,4.716],[0,0],[2.63,0.033],[0,0],[4.782,2.683],[0,0],[2.296,-1.292],[0,0],[5.483,-0.066],[0,0],[1.338,-2.263],[0,0],[4.716,-2.807],[0,0],[0.033,-2.63],[0,0],[2.683,-4.784],[0,0],[-1.292,-2.295],[0,0],[-0.066,-5.485],[0,0],[-2.263,-1.343],[0,0],[-2.807,-4.716],[0,0],[-2.63,-0.033],[0,0]],"v":[[7.7,-57.989],[-7.7,-57.989],[-11.019,-56.128],[-18.523,-54.117],[-22.327,-54.07],[-35.668,-46.369],[-37.609,-43.1],[-43.099,-37.605],[-46.372,-35.663],[-54.072,-22.324],[-54.118,-18.522],[-56.132,-11.016],[-57.988,-7.7],[-57.988,7.703],[-56.132,11.019],[-54.118,18.524],[-54.072,22.328],[-46.372,35.669],[-43.099,37.611],[-37.609,43.101],[-35.668,46.373],[-22.327,54.074],[-18.523,54.12],[-11.019,56.133],[-7.7,57.99],[7.7,57.99],[11.019,56.133],[18.523,54.12],[22.327,54.074],[35.668,46.373],[37.609,43.101],[43.099,37.611],[46.372,35.669],[54.072,22.328],[54.118,18.524],[56.132,11.019],[57.988,7.703],[57.988,-7.7],[56.132,-11.016],[54.118,-18.522],[54.072,-22.324],[46.372,-35.663],[43.099,-37.605],[37.609,-43.1],[35.668,-46.369],[22.327,-54.07],[18.523,-54.117],[11.019,-56.128]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"widgets","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[396,104.003]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"widgets clock","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".onSecondaryFixedVariant","cl":"onSecondaryFixedVariant","parent":1,"sr":1,"ks":{"o":{"k":[{"s":[100],"t":45,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[70],"t":51,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[70],"t":135,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[100],"t":144,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[0,-29.497,0]},"a":{"a":0,"k":[252,128.003,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[444,200.004]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"app - 7","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[348,200.004]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"app - 6","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[252,200.004]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"app - 5","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[252,128.004]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"app - 4","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[252,56.002]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"app - 3","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[156,56.004]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"app - 2","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[60,56.004]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"app - 1","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"BlankButton","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[133,455.5,0]},"a":{"a":0,"k":[40,44.5,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"w":80,"h":89,"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"BlankButton","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[229,455.5,0]},"a":{"a":0,"k":[40,44.5,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"w":80,"h":89,"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"BlankButton","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[421,455.5,0]},"a":{"a":0,"k":[40,44.5,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"w":80,"h":89,"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"actionKey_themed","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[325,455.5,0]},"a":{"a":0,"k":[40,44.5,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"w":80,"h":89,"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"matte","td":1,"sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[277,197.5,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[504,315]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":28},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"illustrations: action key","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":6,"ty":0,"nm":"AllApps_Tray_themed","tt":1,"tp":5,"refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[277,282,0]},"a":{"a":0,"k":[277,282,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"w":554,"h":564,"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":7,"ty":0,"nm":"AK_LofiLauncher","refId":"comp_3","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[277,197.5,0]},"a":{"a":0,"k":[252,157.5,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"w":504,"h":315,"ip":0,"op":451,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":".onSecondaryFixedVariant","cl":"onSecondaryFixedVariant","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[277,197.5,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[504,315]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":28},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":50},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"illustrations: action key","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":".secondaryFixedDim","cl":"secondaryFixedDim","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[277,197.5,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[504,315]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":28},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.850980401039,0.768627464771,0.627451002598,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"illustrations: action key","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":".secondaryFixedDim","cl":"secondaryFixedDim","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[277,197.5,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[504,315]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":28},"nm":"Rectangle Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.850980392157,0.76862745098,0.627450980392,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":14},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"op","nm":"Stroke align: Outside","a":{"k":[{"s":[7],"t":0,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[7],"t":180,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}]},"lj":1,"ml":{"a":0,"k":4},"hd":false},{"ty":"fl","c":{"a":0,"k":[0.850980392157,0.76862745098,0.627450980392,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"frame","bm":0,"hd":false}],"ip":0,"op":451,"st":0,"ct":1,"bm":0}],"markers":[],"props":{}}
\ No newline at end of file diff --git a/packages/SystemUI/res/raw/action_key_success.json b/packages/SystemUI/res/raw/action_key_success.json new file mode 100644 index 000000000000..cae7344d04b9 --- /dev/null +++ b/packages/SystemUI/res/raw/action_key_success.json @@ -0,0 +1 @@ +{"v":"5.12.1","fr":60,"ip":0,"op":50,"w":554,"h":564,"nm":"Trackpad-JSON_ActionKey-Success","ddd":0,"assets":[{"id":"comp_0","nm":"TrackpadAK_Success_Checkmark","fr":60,"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"Check Rotate","parent":2,"sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"i":{"x":[0.12],"y":[1]},"o":{"x":[0.44],"y":[0]},"t":2,"s":[-16]},{"t":20,"s":[6]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[95.049,95.049,100]}},"ao":0,"ip":0,"op":228,"st":-72,"bm":0},{"ddd":0,"ind":2,"ty":3,"nm":"Bounce","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"i":{"x":[0.12],"y":[1]},"o":{"x":[0.44],"y":[0]},"t":12,"s":[0]},{"t":36,"s":[-6]}]},"p":{"a":0,"k":[81,127,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"i":{"x":[0.263,0.263,0.833],"y":[1.126,1.126,1]},"o":{"x":[0.05,0.05,0.05],"y":[0.958,0.958,0]},"t":1,"s":[80,80,100]},{"i":{"x":[0.1,0.1,0.1],"y":[1,1,1]},"o":{"x":[0.45,0.45,0.167],"y":[0.325,0.325,0]},"t":20,"s":[105,105,100]},{"t":36,"s":[100,100,100]}]}},"ao":0,"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".secondaryFixedDim","cl":"secondaryFixedDim","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":-0.289},"p":{"a":0,"k":[14.364,-33.591,0]},"a":{"a":0,"k":[-0.125,0,0]},"s":{"a":0,"k":[104.744,104.744,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[-1.401,-0.007],[-10.033,11.235]],"o":[[5.954,7.288],[1.401,0.007],[0,0]],"v":[[-28.591,4.149],[-10.73,26.013],[31.482,-21.255]],"c":false}},"nm":"Path 1","hd":false},{"ty":"tm","s":{"a":0,"k":0},"e":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.4],"y":[0]},"t":3,"s":[0]},{"i":{"x":[0.22],"y":[1]},"o":{"x":[0.001],"y":[0.149]},"t":10,"s":[29]},{"t":27,"s":[100]}]},"o":{"a":0,"k":0},"m":1,"nm":"Trim Paths 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.850980392157,0.76862745098,0.627450980392,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":11},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":5,"op":44,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".onSecondaryFixedVariant","cl":"onSecondaryFixedVariant","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[95,95,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"i":{"x":[0.275,0.275,0.21],"y":[1.102,1.102,1]},"o":{"x":[0.037,0.037,0.05],"y":[0.476,0.476,0]},"t":0,"s":[0,0,100]},{"i":{"x":[0.1,0.1,0.1],"y":[1,1,1]},"o":{"x":[0.252,0.252,0.47],"y":[0.159,0.159,0]},"t":16,"s":[120,120,100]},{"t":28,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.1,0.1],"y":[1,1]},"o":{"x":[0.32,0.32],"y":[0.11,0.11]},"t":16,"s":[148,148]},{"t":28,"s":[136,136]}]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":88},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Checkbox - Widget","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0}]},{"id":"comp_1","nm":"actionKey_themed-static","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".onSecondaryFixed","cl":"onSecondaryFixed","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[0.288,-0.035,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0.579,-0.158],[0.605,0],[1.21,1.21],[0,1.684],[-1.184,1.184],[-1.684,0],[-1.21,-1.21],[0,-1.71],[0.184,-0.553],[0.316,-0.474],[0,0],[0,0]],"o":[[-0.474,0.316],[-0.553,0.158],[-1.684,0],[-1.184,-1.21],[0,-1.71],[1.21,-1.21],[1.71,0],[1.21,1.184],[0,0.605],[-0.158,0.553],[0,0],[0,0],[0,0]],"v":[[10.241,12.155],[8.663,12.866],[6.926,13.103],[2.585,11.287],[0.809,6.946],[2.585,2.605],[6.926,0.789],[11.307,2.605],[13.122,6.946],[12.846,8.682],[12.136,10.222],[16.911,14.997],[15.017,16.891]],"c":true}},"nm":"Path 1","hd":false},{"ind":1,"ty":"sh","ks":{"a":0,"k":{"i":[[1.21,1.21],[0,1.684],[-1.21,1.184],[-1.684,0],[-1.184,-1.21],[0,-1.736],[1.21,-1.21],[1.736,0]],"o":[[-1.21,-1.21],[0,-1.736],[1.21,-1.21],[1.736,0],[1.21,1.184],[0,1.684],[-1.184,1.21],[-1.684,0]],"v":[[-15.096,11.327],[-16.911,6.985],[-15.096,2.605],[-10.754,0.789],[-6.374,2.605],[-4.558,6.985],[-6.374,11.327],[-10.754,13.142]],"c":true}},"nm":"Path 2","hd":false},{"ind":2,"ty":"sh","ks":{"a":0,"k":{"i":[[-0.684,0.658],[0,0.947],[0.684,0.684],[0.973,0],[0.684,-0.684],[0,-0.973],[-0.658,-0.684],[-0.947,0]],"o":[[0.684,-0.684],[0,-0.973],[-0.684,-0.684],[-0.947,0],[-0.658,0.684],[0,0.947],[0.684,0.658],[0.973,0]],"v":[[-8.268,9.432],[-7.242,6.985],[-8.268,4.499],[-10.754,3.473],[-13.201,4.499],[-14.188,6.985],[-13.201,9.432],[-10.754,10.419]],"c":true}},"nm":"Path 3","hd":false},{"ind":3,"ty":"sh","ks":{"a":0,"k":{"i":[[-0.684,0.658],[0,0.947],[0.684,0.684],[0.973,0],[0.684,-0.684],[0,-0.973],[-0.684,-0.684],[-0.947,0]],"o":[[0.684,-0.684],[0,-0.973],[-0.684,-0.684],[-0.947,0],[-0.684,0.684],[0,0.947],[0.684,0.658],[0.973,0]],"v":[[9.413,9.432],[10.439,6.985],[9.413,4.499],[6.926,3.473],[4.479,4.499],[3.453,6.985],[4.479,9.432],[6.926,10.419]],"c":true}},"nm":"Path 4","hd":false},{"ind":4,"ty":"sh","ks":{"a":0,"k":{"i":[[1.21,1.21],[0,1.684],[-1.21,1.21],[-1.684,0],[-1.184,-1.21],[0,-1.71],[1.21,-1.21],[1.736,0]],"o":[[-1.21,-1.21],[0,-1.71],[1.21,-1.21],[1.736,0],[1.21,1.21],[0,1.684],[-1.184,1.21],[-1.684,0]],"v":[[-15.096,-6.354],[-16.911,-10.695],[-15.096,-15.076],[-10.754,-16.891],[-6.374,-15.076],[-4.558,-10.695],[-6.374,-6.354],[-10.754,-4.539]],"c":true}},"nm":"Path 5","hd":false},{"ind":5,"ty":"sh","ks":{"a":0,"k":{"i":[[1.21,1.21],[0,1.684],[-1.21,1.21],[-1.684,0],[-1.21,-1.21],[0,-1.71],[1.21,-1.21],[1.71,0]],"o":[[-1.21,-1.21],[0,-1.71],[1.21,-1.21],[1.71,0],[1.21,1.21],[0,1.684],[-1.21,1.21],[-1.684,0]],"v":[[2.585,-6.354],[0.77,-10.695],[2.585,-15.076],[6.926,-16.891],[11.307,-15.076],[13.122,-10.695],[11.307,-6.354],[6.926,-4.539]],"c":true}},"nm":"Path 6","hd":false},{"ind":6,"ty":"sh","ks":{"a":0,"k":{"i":[[-0.684,0.684],[0,0.947],[0.684,0.684],[0.973,0],[0.684,-0.684],[0,-0.973],[-0.658,-0.684],[-0.947,0]],"o":[[0.684,-0.684],[0,-0.973],[-0.684,-0.684],[-0.947,0],[-0.658,0.684],[0,0.947],[0.684,0.684],[0.973,0]],"v":[[-8.268,-8.248],[-7.242,-10.695],[-8.268,-13.182],[-10.754,-14.208],[-13.201,-13.182],[-14.188,-10.695],[-13.201,-8.248],[-10.754,-7.222]],"c":true}},"nm":"Path 7","hd":false},{"ind":7,"ty":"sh","ks":{"a":0,"k":{"i":[[-0.684,0.684],[0,0.947],[0.684,0.684],[0.973,0],[0.684,-0.684],[0,-0.973],[-0.684,-0.684],[-0.947,0]],"o":[[0.684,-0.684],[0,-0.973],[-0.684,-0.684],[-0.947,0],[-0.684,0.684],[0,0.947],[0.684,0.684],[0.973,0]],"v":[[9.413,-8.248],[10.439,-10.695],[9.413,-13.182],[6.926,-14.208],[4.479,-13.182],[3.453,-10.695],[4.479,-8.248],[6.926,-7.222]],"c":true}},"nm":"Path 8","hd":false},{"ty":"fl","c":{"a":0,"k":[0.145098045468,0.101960785687,0.015686275437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"icon","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primaryFixedDim","cl":"primaryFixedDim","parent":3,"sr":1,"ks":{"o":{"a":0,"k":0},"r":{"a":0,"k":0},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[80,79.581]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":14.032},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.92549020052,0.752941191196,0.423529416323,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"shortcut symbols","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".secondaryFixedDim","cl":"secondaryFixedDim","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[40,39.79,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[80,79.581]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":14.032},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.850980392157,0.76862745098,0.627450980392,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"shortcut symbols","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".onSecondaryFixedVariant","cl":"onSecondaryFixedVariant","sr":1,"ks":{"o":{"a":0,"k":80},"r":{"a":0,"k":0},"p":{"a":0,"k":[40,49.21,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[80,79.581]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":14.032},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.364705890417,0.258823543787,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"shadow","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0}]},{"id":"comp_2","nm":"BlankButton","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".secondaryFixedDim","cl":"secondaryFixedDim","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[40,39.79,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[80,79.581]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":14.032},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.850980401039,0.768627464771,0.627451002598,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"shortcut symbols","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".onSecondaryFixedVariant","cl":"onSecondaryFixedVariant","sr":1,"ks":{"o":{"a":0,"k":80},"r":{"a":0,"k":0},"p":{"a":0,"k":[40,49.21,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[80,79.581]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":14.032},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.364705890417,0.258823543787,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"shadow","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0}]},{"id":"comp_3","nm":"AK_LofiLauncher","fr":60,"pfr":1,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Scale Down","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":45,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":51,"s":[70]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":135,"s":[70]},{"t":144,"s":[100]}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[252,157.5,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"i":{"x":[0.1,0.1,0.1],"y":[1,1,1]},"o":{"x":[0.153,0.153,0.153],"y":[0.074,0.074,0]},"t":45,"s":[100,100,100]},{"i":{"x":[0.841,0.841,0.841],"y":[1,1,1]},"o":{"x":[0.161,0.161,0.161],"y":[0,0,0]},"t":75,"s":[95,95,100]},{"i":{"x":[0,0,0],"y":[1,1,1]},"o":{"x":[0.2,0.2,0.2],"y":[0,0,0]},"t":135,"s":[95,95,100]},{"t":165,"s":[100,100,100]}]}},"ao":0,"ef":[{"ty":5,"nm":"Void","np":19,"mn":"Pseudo/250958","ix":1,"en":1,"ef":[{"ty":0,"nm":"Width","mn":"Pseudo/250958-0001","ix":1,"v":{"a":0,"k":100}},{"ty":0,"nm":"Height","mn":"Pseudo/250958-0002","ix":2,"v":{"a":0,"k":100}},{"ty":0,"nm":"Offset X","mn":"Pseudo/250958-0003","ix":3,"v":{"a":0,"k":0}},{"ty":0,"nm":"Offset Y","mn":"Pseudo/250958-0004","ix":4,"v":{"a":0,"k":0}},{"ty":0,"nm":"Roundness","mn":"Pseudo/250958-0005","ix":5,"v":{"a":0,"k":0}},{"ty":6,"nm":"About","mn":"Pseudo/250958-0006","ix":6,"v":0},{"ty":6,"nm":"Plague of null layers.","mn":"Pseudo/250958-0007","ix":7,"v":0},{"ty":6,"nm":"Void","mn":"Pseudo/250958-0008","ix":8,"v":0},{"ty":6,"nm":"Following projects","mn":"Pseudo/250958-0009","ix":9,"v":0},{"ty":6,"nm":"Void","mn":"Pseudo/250958-0010","ix":10,"v":0},{"ty":6,"nm":"through time.","mn":"Pseudo/250958-0011","ix":11,"v":0},{"ty":6,"nm":"Void","mn":"Pseudo/250958-0012","ix":12,"v":0},{"ty":6,"nm":"Be free of the past.","mn":"Pseudo/250958-0013","ix":13,"v":0},{"ty":6,"nm":"Void","mn":"Pseudo/250958-0014","ix":14,"v":0},{"ty":6,"nm":"Copyright 2023 Battle Axe Inc","mn":"Pseudo/250958-0015","ix":15,"v":0},{"ty":6,"nm":"Void","mn":"Pseudo/250958-0016","ix":16,"v":0},{"ty":6,"nm":"Void","mn":"Pseudo/250958-0017","ix":17,"v":0}]}],"shapes":[],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".onSecondaryFixedVariant","cl":"onSecondaryFixedVariant","parent":1,"sr":1,"ks":{"o":{"k":[{"s":[100],"t":45,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[80],"t":49,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[0,117.5,0]},"a":{"a":0,"k":[252,275,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[444,275]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"hotseat - 5","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[396,275]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"hotseat - 4","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[348,275]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"hotseat - 3","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[300,275]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"hotseat - 2","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[252,275]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"hotseat - 1","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[168,20]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":15},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"qsb","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[132,275]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"qsb","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".onSecondaryFixedVariant","cl":"onSecondaryFixedVariant","parent":1,"sr":1,"ks":{"o":{"k":[{"s":[100],"t":45,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[80],"t":49,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[0,-29.497,0]},"a":{"a":0,"k":[252,128.003,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[20.144,20.144],[20.144,-20.144],[0,0],[-20.144,-20.144],[-20.144,20.144],[0,0]],"o":[[-20.144,-20.144],[0,0],[-20.144,20.144],[20.144,20.144],[0,0],[20.144,-20.144]],"v":[[44.892,-44.892],[-28.057,-44.892],[-44.892,-28.057],[-44.892,44.892],[28.057,44.892],[44.892,28.057]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"widgets","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[108,152.004]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"widgets weather","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[4.782,-2.684],[0,0],[2.63,-0.033],[0,0],[2.807,-4.716],[0,0],[2.263,-1.343],[0,0],[0.066,-5.485],[0,0],[1.292,-2.295],[0,0],[-2.683,-4.784],[0,0],[-0.033,-2.63],[0,0],[-4.716,-2.807],[0,0],[-1.338,-2.263],[0,0],[-5.483,-0.066],[0,0],[-2.296,-1.292],[0,0],[-4.782,2.683],[0,0],[-2.63,0.033],[0,0],[-2.807,4.716],[0,0],[-2.263,1.338],[0,0],[-0.066,5.483],[0,0],[-1.292,2.295],[0,0],[2.683,4.784],[0,0],[0.033,2.631],[0,0],[4.716,2.801],[0,0],[1.338,2.262],[0,0],[5.483,0.068],[0,0],[2.296,1.287]],"o":[[-4.782,-2.684],[0,0],[-2.296,1.287],[0,0],[-5.483,0.068],[0,0],[-1.338,2.262],[0,0],[-4.716,2.801],[0,0],[-0.033,2.631],[0,0],[-2.683,4.784],[0,0],[1.292,2.295],[0,0],[0.066,5.483],[0,0],[2.263,1.338],[0,0],[2.807,4.716],[0,0],[2.63,0.033],[0,0],[4.782,2.683],[0,0],[2.296,-1.292],[0,0],[5.483,-0.066],[0,0],[1.338,-2.263],[0,0],[4.716,-2.807],[0,0],[0.033,-2.63],[0,0],[2.683,-4.784],[0,0],[-1.292,-2.295],[0,0],[-0.066,-5.485],[0,0],[-2.263,-1.343],[0,0],[-2.807,-4.716],[0,0],[-2.63,-0.033],[0,0]],"v":[[7.7,-57.989],[-7.7,-57.989],[-11.019,-56.128],[-18.523,-54.117],[-22.327,-54.07],[-35.668,-46.369],[-37.609,-43.1],[-43.099,-37.605],[-46.372,-35.663],[-54.072,-22.324],[-54.118,-18.522],[-56.132,-11.016],[-57.988,-7.7],[-57.988,7.703],[-56.132,11.019],[-54.118,18.524],[-54.072,22.328],[-46.372,35.669],[-43.099,37.611],[-37.609,43.101],[-35.668,46.373],[-22.327,54.074],[-18.523,54.12],[-11.019,56.133],[-7.7,57.99],[7.7,57.99],[11.019,56.133],[18.523,54.12],[22.327,54.074],[35.668,46.373],[37.609,43.101],[43.099,37.611],[46.372,35.669],[54.072,22.328],[54.118,18.524],[56.132,11.019],[57.988,7.703],[57.988,-7.7],[56.132,-11.016],[54.118,-18.522],[54.072,-22.324],[46.372,-35.663],[43.099,-37.605],[37.609,-43.1],[35.668,-46.369],[22.327,-54.07],[18.523,-54.117],[11.019,-56.128]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"widgets","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[396,104.003]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"widgets clock","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".onSecondaryFixedVariant","cl":"onSecondaryFixedVariant","parent":1,"sr":1,"ks":{"o":{"k":[{"s":[100],"t":45,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[80],"t":49,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[0,-29.497,0]},"a":{"a":0,"k":[252,128.003,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[444,200.004]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"app - 7","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[348,200.004]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"app - 6","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[252,200.004]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"app - 5","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[252,128.004]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"app - 4","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[252,56.002]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"app - 3","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[156,56.004]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"app - 2","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[60,56.004]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"app - 1","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"TrackpadAK_Success_Checkmark","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[277,198.5,0]},"a":{"a":0,"k":[95,95,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"w":190,"h":190,"ip":6,"op":50,"st":6,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".onSecondaryFixed","cl":"onSecondaryFixed","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":1,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":7,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":389,"s":[100]},{"t":392,"s":[0]}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[277,197.5,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"ef":[{"ty":5,"nm":"Global Position","np":4,"mn":"Pseudo/88900","ix":1,"en":1,"ef":[{"ty":10,"nm":"Master Parent","mn":"Pseudo/88900-0001","ix":1,"v":{"a":0,"k":2}},{"ty":3,"nm":"Global Position","mn":"Pseudo/88900-0002","ix":2,"v":{"k":[{"s":[277,197.5],"t":0,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,197.5],"t":49,"i":{"x":1,"y":1},"o":{"x":0,"y":0}}]}}]}],"shapes":[{"ty":"rc","d":1,"s":{"a":0,"k":[504,315]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":28},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.145098039216,0.101960784314,0.01568627451,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false}],"ip":0,"op":50,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"actionKey_themed-static","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[325,455.5,0]},"a":{"a":0,"k":[40,44.5,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"w":80,"h":89,"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"BlankButton","refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[421,455.5,0]},"a":{"a":0,"k":[40,44.5,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"w":80,"h":89,"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":5,"ty":0,"nm":"BlankButton","refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[229,455.5,0]},"a":{"a":0,"k":[40,44.5,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"w":80,"h":89,"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":6,"ty":0,"nm":"BlankButton","refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[133,455.5,0]},"a":{"a":0,"k":[40,44.5,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"w":80,"h":89,"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":7,"ty":0,"nm":"AK_LofiLauncher","refId":"comp_3","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[277,197.5,0]},"a":{"a":0,"k":[252,157.5,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"w":504,"h":315,"ip":0,"op":50,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":".onSecondaryFixedVariant","cl":"onSecondaryFixedVariant","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[277,197.5,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"ef":[{"ty":5,"nm":"Global Position","np":4,"mn":"Pseudo/88900","ix":1,"en":1,"ef":[{"ty":10,"nm":"Master Parent","mn":"Pseudo/88900-0001","ix":1,"v":{"a":0,"k":2}},{"ty":3,"nm":"Global Position","mn":"Pseudo/88900-0002","ix":2,"v":{"k":[{"s":[277,197.5],"t":0,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,197.5],"t":49,"i":{"x":1,"y":1},"o":{"x":0,"y":0}}]}}]}],"shapes":[{"ty":"rc","d":1,"s":{"a":0,"k":[504,315]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":28},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":50},"r":1,"bm":0,"nm":"Fill 1","hd":false}],"ip":0,"op":50,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":".secondaryFixedDim","cl":"secondaryFixedDim","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[277,197.5,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[504,315]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":28},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.850980401039,0.768627464771,0.627451002598,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"illustrations: action key","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":".secondaryFixedDim","cl":"secondaryFixedDim","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[277,197.5,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[504,315]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":28},"nm":"Rectangle Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.850980392157,0.76862745098,0.627450980392,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":14},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"op","nm":"Stroke align: Outside","a":{"k":[{"s":[7],"t":0,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[7],"t":49,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}]},"lj":1,"ml":{"a":0,"k":4},"hd":false},{"ty":"fl","c":{"a":0,"k":[0.850980392157,0.76862745098,0.627450980392,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"frame","bm":0,"hd":false}],"ip":0,"op":50,"st":0,"ct":1,"bm":0}],"markers":[],"props":{}}
\ No newline at end of file diff --git a/packages/SystemUI/res/raw/trackpad_home_edu.json b/packages/SystemUI/res/raw/trackpad_home_edu.json new file mode 100644 index 000000000000..27db9fd752e3 --- /dev/null +++ b/packages/SystemUI/res/raw/trackpad_home_edu.json @@ -0,0 +1 @@ +{"v":"5.12.1","fr":60,"ip":0,"op":426,"w":554,"h":564,"nm":"Trackpad-JSON_HomeGesture-EDU","ddd":0,"assets":[{"id":"comp_0","nm":"Home_Dismiss","fr":60,"pfr":1,"layers":[{"ddd":0,"ind":2,"ty":3,"nm":"gesture:scale","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"k":[{"s":[277,197.321,0],"t":148,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,197.13,0],"t":151,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,197.036,0],"t":152,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,196.921,0],"t":153,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,196.779,0],"t":154,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,196.606,0],"t":155,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,196.39,0],"t":156,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,196.122,0],"t":157,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,195.786,0],"t":158,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,195.354,0],"t":159,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,194.78,0],"t":160,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,193.975,0],"t":161,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,192.883,0],"t":162,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,191.652,0],"t":163,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,190.304,0],"t":164,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,188.897,0],"t":165,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,187.507,0],"t":166,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,186.208,0],"t":167,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,185.036,0],"t":168,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,183.998,0],"t":169,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,183.082,0],"t":170,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,182.274,0],"t":171,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,181.557,0],"t":172,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,180.918,0],"t":173,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,180.344,0],"t":174,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,179.824,0],"t":175,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,179.353,0],"t":176,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,178.924,0],"t":177,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,178.532,0],"t":178,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,178.174,0],"t":179,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,177.843,0],"t":180,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,177.538,0],"t":181,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,177.256,0],"t":182,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,176.995,0],"t":183,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,176.752,0],"t":184,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,176.527,0],"t":185,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,176.319,0],"t":186,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,176.124,0],"t":187,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,175.943,0],"t":188,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,175.776,0],"t":189,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,175.619,0],"t":190,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,175.474,0],"t":191,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,175.339,0],"t":192,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,175.213,0],"t":193,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,175.095,0],"t":194,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,174.985,0],"t":195,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,174.884,0],"t":196,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,174.789,0],"t":197,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,174.62,0],"t":199,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,174.476,0],"t":201,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,174.353,0],"t":203,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,174.209,0],"t":206,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,174.039,0],"t":212,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,174,0],"t":380,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,174.212,0],"t":381,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,174.896,0],"t":382,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,176.197,0],"t":383,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,178.536,0],"t":384,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,183.4,0],"t":385,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,188.939,0],"t":386,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,191.375,0],"t":387,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,192.791,0],"t":388,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,193.751,0],"t":389,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,194.459,0],"t":390,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,195.006,0],"t":391,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,195.442,0],"t":392,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,195.798,0],"t":393,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,196.092,0],"t":394,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,196.339,0],"t":395,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,196.546,0],"t":396,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,196.721,0],"t":397,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,196.87,0],"t":398,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,196.995,0],"t":399,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,197.191,0],"t":401,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,197.378,0],"t":404,"i":{"x":1,"y":1},"o":{"x":0,"y":0}}]},"a":{"a":0,"k":[0,0,0]},"s":{"k":[{"s":[99.914,99.914,100],"t":146,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[99.848,99.848,100],"t":148,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[99.751,99.751,100],"t":150,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[99.685,99.685,100],"t":151,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[99.605,99.605,100],"t":152,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[99.507,99.507,100],"t":153,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[99.387,99.387,100],"t":154,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[99.239,99.239,100],"t":155,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[99.056,99.056,100],"t":156,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[98.829,98.829,100],"t":157,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[98.542,98.542,100],"t":158,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[98.174,98.174,100],"t":159,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[97.686,97.686,100],"t":160,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[97,97,100],"t":161,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[96.071,96.071,100],"t":162,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[95.025,95.025,100],"t":163,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[93.878,93.878,100],"t":164,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[92.678,92.678,100],"t":165,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[91.495,91.495,100],"t":166,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[90.39,90.39,100],"t":167,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[89.393,89.393,100],"t":168,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[88.508,88.508,100],"t":169,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[87.729,87.729,100],"t":170,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[87.041,87.041,100],"t":171,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[86.43,86.43,100],"t":172,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[85.886,85.886,100],"t":173,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[85.397,85.397,100],"t":174,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[84.956,84.956,100],"t":175,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[84.555,84.555,100],"t":176,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[84.191,84.191,100],"t":177,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[83.857,83.857,100],"t":178,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[83.552,83.552,100],"t":179,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[83.271,83.271,100],"t":180,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[83.011,83.011,100],"t":181,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[82.771,82.771,100],"t":182,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[82.549,82.549,100],"t":183,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[82.342,82.342,100],"t":184,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[82.151,82.151,100],"t":185,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[81.973,81.973,100],"t":186,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[81.807,81.807,100],"t":187,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[81.653,81.653,100],"t":188,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[81.51,81.51,100],"t":189,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[81.376,81.376,100],"t":190,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[81.251,81.251,100],"t":191,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[81.135,81.135,100],"t":192,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[81.027,81.027,100],"t":193,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[80.926,80.926,100],"t":194,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[80.833,80.833,100],"t":195,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[80.746,80.746,100],"t":196,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[80.665,80.665,100],"t":197,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[80.591,80.591,100],"t":198,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[80.522,80.522,100],"t":199,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[80.458,80.458,100],"t":200,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[80.4,80.4,100],"t":201,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[80.346,80.346,100],"t":202,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[80.298,80.298,100],"t":203,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[80.253,80.253,100],"t":204,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[80.176,80.176,100],"t":206,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[80.115,80.115,100],"t":208,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[80.049,80.049,100],"t":211,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[80,80,100],"t":380,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[80.179,80.179,100],"t":381,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[80.757,80.757,100],"t":382,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[81.87,81.87,100],"t":383,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[83.86,83.86,100],"t":384,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[88,88,100],"t":385,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[92.714,92.714,100],"t":386,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[94.789,94.789,100],"t":387,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[95.992,95.992,100],"t":388,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[96.809,96.809,100],"t":389,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[97.412,97.412,100],"t":390,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[97.878,97.878,100],"t":391,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[98.249,98.249,100],"t":392,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[98.553,98.553,100],"t":393,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[98.803,98.803,100],"t":394,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[99.012,99.012,100],"t":395,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[99.188,99.188,100],"t":396,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[99.337,99.337,100],"t":397,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[99.464,99.464,100],"t":398,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[99.57,99.57,100],"t":399,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[99.661,99.661,100],"t":400,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[99.737,99.737,100],"t":401,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[99.8,99.8,100],"t":402,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[99.896,99.896,100],"t":404,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}},{"s":[99.99,99.99,100],"t":408,"i":{"x":[1,1,1],"y":[1,1,1]},"o":{"x":[0,0,0],"y":[0,0,0]}}]}},"ao":0,"ef":[{"ty":5,"nm":"Void","np":19,"mn":"Pseudo/250958","ix":1,"en":1,"ef":[{"ty":0,"nm":"Width","mn":"Pseudo/250958-0001","ix":1,"v":{"a":0,"k":100}},{"ty":0,"nm":"Height","mn":"Pseudo/250958-0002","ix":2,"v":{"a":0,"k":100}},{"ty":0,"nm":"Offset X","mn":"Pseudo/250958-0003","ix":3,"v":{"a":0,"k":0}},{"ty":0,"nm":"Offset Y","mn":"Pseudo/250958-0004","ix":4,"v":{"a":0,"k":0}},{"ty":0,"nm":"Roundness","mn":"Pseudo/250958-0005","ix":5,"v":{"a":0,"k":0}},{"ty":6,"nm":"About","mn":"Pseudo/250958-0006","ix":6,"v":0},{"ty":6,"nm":"Plague of null layers.","mn":"Pseudo/250958-0007","ix":7,"v":0},{"ty":6,"nm":"Void","mn":"Pseudo/250958-0008","ix":8,"v":0},{"ty":6,"nm":"Following projects","mn":"Pseudo/250958-0009","ix":9,"v":0},{"ty":6,"nm":"Void","mn":"Pseudo/250958-0010","ix":10,"v":0},{"ty":6,"nm":"through time.","mn":"Pseudo/250958-0011","ix":11,"v":0},{"ty":6,"nm":"Void","mn":"Pseudo/250958-0012","ix":12,"v":0},{"ty":6,"nm":"Be free of the past.","mn":"Pseudo/250958-0013","ix":13,"v":0},{"ty":6,"nm":"Void","mn":"Pseudo/250958-0014","ix":14,"v":0},{"ty":6,"nm":"Copyright 2023 Battle Axe Inc","mn":"Pseudo/250958-0015","ix":15,"v":0},{"ty":6,"nm":"Void","mn":"Pseudo/250958-0016","ix":16,"v":0},{"ty":6,"nm":"Void","mn":"Pseudo/250958-0017","ix":17,"v":0}]}],"ip":0,"op":451,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".primaryFixedDim","cl":"primaryFixedDim","parent":4,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":37,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":47,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":197,"s":[100]},{"t":203,"s":[0]}]},"r":{"a":0,"k":0},"p":{"k":[{"s":[0,29.984,0],"t":127,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,29.965,0],"t":128,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,29.936,0],"t":129,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,29.894,0],"t":130,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,29.84,0],"t":131,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,29.77,0],"t":132,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,29.682,0],"t":133,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,29.574,0],"t":134,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,29.445,0],"t":135,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,29.294,0],"t":136,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,29.121,0],"t":137,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,28.925,0],"t":138,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,28.746,0],"t":139,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,28.548,0],"t":140,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,28.33,0],"t":141,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,28.092,0],"t":142,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,27.832,0],"t":143,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,27.548,0],"t":144,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,27.239,0],"t":145,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,26.903,0],"t":146,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,26.536,0],"t":147,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,26.14,0],"t":148,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,25.709,0],"t":149,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,25.241,0],"t":150,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,24.73,0],"t":151,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,24.171,0],"t":152,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,23.563,0],"t":153,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,22.898,0],"t":154,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,22.171,0],"t":155,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,21.373,0],"t":156,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,20.496,0],"t":157,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,19.524,0],"t":158,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,18.451,0],"t":159,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,17.263,0],"t":160,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,15.943,0],"t":161,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,14.475,0],"t":162,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,12.841,0],"t":163,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,11.018,0],"t":164,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,9.023,0],"t":165,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,6.87,0],"t":166,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,4.614,0],"t":167,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,2.333,0],"t":168,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,0.106,0],"t":169,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,-1.975,0],"t":170,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,-3.877,0],"t":171,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,-5.591,0],"t":172,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,-7.125,0],"t":173,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,-8.492,0],"t":174,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,-9.714,0],"t":175,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,-10.799,0],"t":176,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,-11.771,0],"t":177,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,-12.643,0],"t":178,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,-13.428,0],"t":179,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,-14.138,0],"t":180,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,-14.777,0],"t":181,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,-15.355,0],"t":182,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,-15.879,0],"t":183,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,-16.354,0],"t":184,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,-16.784,0],"t":185,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,-17.177,0],"t":186,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,-17.532,0],"t":187,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,-17.854,0],"t":188,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,-18.146,0],"t":189,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,-18.409,0],"t":190,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,-18.645,0],"t":191,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,-18.858,0],"t":192,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,-19.048,0],"t":193,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,-19.217,0],"t":194,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,-19.366,0],"t":195,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,-19.496,0],"t":196,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,-19.61,0],"t":197,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,-19.707,0],"t":198,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,-19.788,0],"t":199,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,-19.856,0],"t":200,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,-19.911,0],"t":201,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,-19.954,0],"t":202,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[0,-19.984,0],"t":203,"i":{"x":1,"y":1},"o":{"x":0,"y":0}}]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"ef":[{"ty":5,"nm":"Super Slider","np":3,"mn":"ADBE Slider Control","ix":1,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":1,"k":[{"i":{"x":[0.64],"y":[0.48]},"o":{"x":[0.36],"y":[0]},"t":121,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":138,"s":[17.5]},{"t":205,"s":[100]}]}}]}],"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.56,0.56],"y":[1,1]},"o":{"x":[0.44,0.44],"y":[0,0]},"t":62,"s":[36,36]},{"i":{"x":[0.56,0.56],"y":[1,1]},"o":{"x":[0.44,0.44],"y":[0,0]},"t":72,"s":[28,28]},{"i":{"x":[0.56,0.56],"y":[1,1]},"o":{"x":[0.44,0.44],"y":[0,0]},"t":195,"s":[28,28]},{"t":205,"s":[36,36]}]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.925490196078,0.752941176471,0.423529411765,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.56,"y":1},"o":{"x":0.44,"y":0},"t":62,"s":[41,0],"to":[0,0],"ti":[0,0]},{"i":{"x":0.56,"y":0.56},"o":{"x":0.44,"y":0.44},"t":72,"s":[33,0],"to":[0,0],"ti":[0,0]},{"i":{"x":0.56,"y":1},"o":{"x":0.44,"y":0},"t":195,"s":[33,0],"to":[0,0],"ti":[0,0]},{"t":205,"s":[41,0]}]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"right circle","bm":0,"hd":false},{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.56,0.56],"y":[1,1]},"o":{"x":[0.44,0.44],"y":[0,0]},"t":62,"s":[36,36]},{"i":{"x":[0.56,0.56],"y":[1,1]},"o":{"x":[0.44,0.44],"y":[0,0]},"t":72,"s":[28,28]},{"i":{"x":[0.56,0.56],"y":[1,1]},"o":{"x":[0.44,0.44],"y":[0,0]},"t":195,"s":[28,28]},{"t":205,"s":[36,36]}]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.925490196078,0.752941176471,0.423529411765,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.56,"y":1},"o":{"x":0.44,"y":0},"t":62,"s":[-41,0],"to":[0,0],"ti":[0,0]},{"i":{"x":0.56,"y":0.56},"o":{"x":0.44,"y":0.44},"t":72,"s":[-33,0],"to":[0,0],"ti":[0,0]},{"i":{"x":0.56,"y":1},"o":{"x":0.44,"y":0},"t":195,"s":[-33,0],"to":[0,0],"ti":[0,0]},{"t":205,"s":[-41,0]}]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"left circle","bm":0,"hd":false},{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.56,0.56],"y":[1,1]},"o":{"x":[0.44,0.44],"y":[0,0]},"t":62,"s":[36,36]},{"i":{"x":[0.56,0.56],"y":[1,1]},"o":{"x":[0.44,0.44],"y":[0,0]},"t":72,"s":[28,28]},{"i":{"x":[0.56,0.56],"y":[1,1]},"o":{"x":[0.44,0.44],"y":[0,0]},"t":195,"s":[28,28]},{"t":205,"s":[36,36]}]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.925490196078,0.752941176471,0.423529411765,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"size","bm":0,"hd":false}],"ip":37,"op":345,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".onPrimaryFixedVariant","cl":"onPrimaryFixedVariant","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[277,459,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[200,128]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":18},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.364705882353,0.258823529412,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Frame 1321317559","bm":0,"hd":false}],"ip":0,"op":451,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":".onPrimaryFixedVariant","cl":"onPrimaryFixedVariant","parent":6,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":198,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":201,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":389,"s":[100]},{"t":392,"s":[0]}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"ef":[{"ty":5,"nm":"Global Position","np":4,"mn":"Pseudo/88900","ix":1,"en":1,"ef":[{"ty":10,"nm":"Master Parent","mn":"Pseudo/88900-0001","ix":1,"v":{"a":0,"k":2}},{"ty":3,"nm":"Global Position","mn":"Pseudo/88900-0002","ix":2,"v":{"k":[{"s":[277,197.321],"t":148,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,197.13],"t":151,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,197.036],"t":152,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,196.921],"t":153,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,196.779],"t":154,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,196.606],"t":155,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,196.39],"t":156,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,196.122],"t":157,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,195.786],"t":158,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,195.354],"t":159,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,194.781],"t":160,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,193.975],"t":161,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,192.883],"t":162,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,191.652],"t":163,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,190.304],"t":164,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,188.897],"t":165,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,187.507],"t":166,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,186.208],"t":167,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,185.036],"t":168,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,183.998],"t":169,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,183.082],"t":170,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,182.274],"t":171,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,181.557],"t":172,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,180.918],"t":173,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,180.344],"t":174,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,179.824],"t":175,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,179.353],"t":176,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,178.924],"t":177,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,178.532],"t":178,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,178.174],"t":179,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,177.843],"t":180,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,177.538],"t":181,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,177.256],"t":182,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,176.995],"t":183,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,176.752],"t":184,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,176.528],"t":185,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,176.319],"t":186,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,176.124],"t":187,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,175.943],"t":188,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,175.776],"t":189,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,175.619],"t":190,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,175.474],"t":191,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,175.339],"t":192,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,175.213],"t":193,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,175.095],"t":194,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,174.985],"t":195,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,175.638],"t":196,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,178.587],"t":197,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,185.09],"t":198,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,193.793],"t":199,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,201.516],"t":200,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,207.702],"t":201,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,212.767],"t":202,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,217.041],"t":203,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,220.728],"t":204,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,223.965],"t":205,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,226.837],"t":206,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,229.392],"t":207,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,231.662],"t":208,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,233.68],"t":209,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,235.467],"t":210,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,237.042],"t":211,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,238.421],"t":212,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,239.622],"t":213,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,240.66],"t":214,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,241.55],"t":215,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,242.299],"t":216,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,242.916],"t":217,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,243.407],"t":218,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,243.572],"t":220,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,243.29],"t":221,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,242.866],"t":222,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,242.351],"t":223,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,241.782],"t":224,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,241.175],"t":225,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,240.597],"t":226,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,240.08],"t":227,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,239.638],"t":228,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,239.281],"t":229,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,239.017],"t":230,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,239.165],"t":238,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,239.365],"t":240,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,239.555],"t":242,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,239.785],"t":245,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,239.579],"t":383,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,239.389],"t":384,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,240.278],"t":385,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,234.833],"t":386,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,221.896],"t":387,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,215.604],"t":388,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,211.894],"t":389,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,209.347],"t":390,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,207.439],"t":391,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,205.933],"t":392,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,204.711],"t":393,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,203.696],"t":394,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,202.839],"t":395,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,202.106],"t":396,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,201.474],"t":397,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,200.925],"t":398,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,200.444],"t":399,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,200.022],"t":400,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,199.649],"t":401,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,199.32],"t":402,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,199.03],"t":403,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,198.776],"t":404,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,198.552],"t":405,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,198.355],"t":406,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,198.183],"t":407,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,198.034],"t":408,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,197.902],"t":409,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,197.787],"t":410,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,197.62],"t":412,"i":{"x":1,"y":1},"o":{"x":0,"y":0}}]}}]}],"shapes":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0,0],"y":[1,1]},"o":{"x":[0.2,0.2],"y":[0,0]},"t":195,"s":[504,315]},{"i":{"x":[0,0],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0,0]},"t":225,"s":[30,30]},{"i":{"x":[0.8,0.8],"y":[0.15,0.15]},"o":{"x":[0.3,0.3],"y":[0,0]},"t":380,"s":[30,30]},{"i":{"x":[0.1,0.1],"y":[1,1]},"o":{"x":[0.05,0.05],"y":[0.7,0.7]},"t":386,"s":[219.6,144]},{"t":416,"s":[504,315]}]},"p":{"a":0,"k":[0,0]},"r":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.2],"y":[0]},"t":195,"s":[28]},{"i":{"x":[0],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":225,"s":[30]},{"i":{"x":[0.8],"y":[0.15]},"o":{"x":[0.3],"y":[0]},"t":380,"s":[30]},{"i":{"x":[0.1],"y":[1]},"o":{"x":[0.05],"y":[0.7]},"t":386,"s":[29.2]},{"t":416,"s":[28]}]},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.364705882353,0.258823529412,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false}],"ip":0,"op":451,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"matte","parent":2,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.2,"y":0},"t":195,"s":[0,0,0],"to":[0,0,0],"ti":[0,0,0]},{"t":225,"s":[0,82.5,0],"h":1},{"i":{"x":0.8,"y":0.15},"o":{"x":0.3,"y":0},"t":380,"s":[0,82.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.1,"y":1},"o":{"x":0.05,"y":0.7},"t":386,"s":[0,49.5,0],"to":[0,0,0],"ti":[0,0,0]},{"t":416,"s":[0,0,0]}]},"a":{"a":1,"k":[{"i":{"x":0.5,"y":1},"o":{"x":0.28,"y":0},"t":200,"s":[0,0,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.573,"y":1},"o":{"x":0.236,"y":0},"t":218,"s":[0,-6,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.28,"y":0},"t":232,"s":[0,1.5,0],"to":[0,0,0],"ti":[0,0,0]},{"t":252,"s":[0,0,0]}]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0,0],"y":[1,1]},"o":{"x":[0.2,0.2],"y":[0,0]},"t":195,"s":[504,315]},{"i":{"x":[0,0],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0,0]},"t":225,"s":[30,30]},{"i":{"x":[0.8,0.8],"y":[0.15,0.15]},"o":{"x":[0.3,0.3],"y":[0,0]},"t":380,"s":[30,30]},{"i":{"x":[0.1,0.1],"y":[1,1]},"o":{"x":[0.05,0.05],"y":[0.7,0.7]},"t":386,"s":[219.6,144]},{"t":416,"s":[504,315]}]},"p":{"a":0,"k":[0,0]},"r":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.2],"y":[0]},"t":195,"s":[28]},{"i":{"x":[0],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":225,"s":[30]},{"i":{"x":[0.8],"y":[0.15]},"o":{"x":[0.3],"y":[0]},"t":380,"s":[30]},{"i":{"x":[0.1],"y":[1]},"o":{"x":[0.05],"y":[0.7]},"t":386,"s":[29.2]},{"t":416,"s":[28]}]},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false}],"ip":0,"op":451,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":7,"ty":0,"nm":"Home_LofiApp","parent":6,"tt":1,"tp":6,"refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[252,157.5,0]},"s":{"a":1,"k":[{"i":{"x":[0,0,0],"y":[1,1,1]},"o":{"x":[0.2,0.2,0.2],"y":[0,0,0]},"t":195,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":225,"s":[10,10,100]},{"i":{"x":[0.8,0.8,0.8],"y":[0.15,0.15,1]},"o":{"x":[0.3,0.3,0.3],"y":[0,0,0]},"t":380,"s":[10,10,100]},{"i":{"x":[0.1,0.1,0.1],"y":[1,1,1]},"o":{"x":[0.05,0.05,0.05],"y":[0.7,0.7,0]},"t":386,"s":[46,46,100]},{"t":416,"s":[100,100,100]}]}},"ao":0,"w":504,"h":315,"ip":0,"op":451,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":".onPrimaryFixedVariant","cl":"onPrimaryFixedVariant","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[277,197.5,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[503.5,314.5]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":28},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.364705890417,0.258823543787,0,1]},"o":{"a":0,"k":50},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"frame","bm":0,"hd":false}],"ip":0,"op":451,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":9,"ty":0,"nm":"Home_LofiLauncher","refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[277,197.5,0]},"a":{"a":0,"k":[252,157.5,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"w":504,"h":315,"ip":0,"op":451,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":".primaryFixedDim","cl":"primaryFixedDim","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[277,197.5,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[504,315]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":28},"nm":"Rectangle Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.925490196078,0.752941176471,0.423529411765,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":14},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"op","nm":"Stroke align: Outside","a":{"k":[{"s":[7],"t":25,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[7],"t":450,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}]},"lj":1,"ml":{"a":0,"k":4},"hd":false},{"ty":"fl","c":{"a":0,"k":[0.925490196078,0.752941176471,0.423529411765,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"frame","bm":0,"hd":false}],"ip":0,"op":451,"st":0,"ct":1,"bm":0}]},{"id":"comp_1","nm":"Home_LofiApp","fr":60,"pfr":1,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".onPrimaryFixedVariant","cl":"onPrimaryFixedVariant","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[339.937,151.75,0]},"a":{"a":0,"k":[339.937,151.75,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[1.021,-1.766],[0,0],[-2.043,0],[0,0],[1.022,1.767]],"o":[[-1.021,-1.766],[0,0],[-1.022,1.767],[0,0],[2.043,0],[0,0]],"v":[[2.297,-7.675],[-2.297,-7.675],[-9.64,5.025],[-7.343,9],[7.343,9],[9.64,5.025]],"c":true}},"nm":"Path 1","hd":false},{"ty":"rd","nm":"Round Corners 1","r":{"a":0,"k":9},"hd":false},{"ty":"fl","c":{"a":0,"k":[0.364705882353,0.258823529412,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Triangle","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[481.874,21]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Triangle","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[18,18]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":200},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.364705882353,0.258823529412,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Rectangle","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[457.874,21]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Rectangle","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[292,25]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":200},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.364705882353,0.258823529412,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Text field","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[334,279]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Text field","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[109,28]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":12},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.364705882353,0.258823529412,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Sent","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[425.5,208.5]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Sent","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[160,56]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":14},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.364705882353,0.258823529412,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Sent","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[400,158.5]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Sent","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[126,40]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":14},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.364705882353,0.258823529412,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Received","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[251,78.5]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Received","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".onPrimaryFixed","cl":"onPrimaryFixed","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[334,157.5,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[340,315]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":16},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.149019607843,0.098039215686,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Message","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".onPrimaryFixedVariant","cl":"onPrimaryFixedVariant","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[82,171.125,0]},"a":{"a":0,"k":[82,171.125,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[64,8]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":39.375},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.364705882353,0.258823529412,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Line 2","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[80,177.125]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Line 4","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[92,8]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":39.375},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.364705882353,0.258823529412,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Line 1","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[94,165.125]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Line 3","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[20,20]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":39.375},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.364705882353,0.258823529412,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Avatar","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[34,171.125]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"circle 2","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".onPrimaryFixed","cl":"onPrimaryFixed","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[82,140,0]},"a":{"a":0,"k":[82,140.938,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[132,22]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":39.375},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.149019607843,0.098039215686,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Search","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[82,31.5]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"header","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[64,8]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":200},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.149019607843,0.098039215686,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Line 2","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[80,257.375]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Line 6","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[92,8]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":200},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.149019607843,0.098039215686,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Line 1","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[94,245.375]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Line 5","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[20,20]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":200},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.149019607843,0.098039215686,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Avatar","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[34,251.375]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"circle 3","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[132,64]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":12},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.149019607843,0.098039215686,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Message","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[82,171]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"block","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[64,8]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":200},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.149019607843,0.098039215686,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Line 2","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[80,96.875]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Line 2","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[92,8]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":200},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.149019607843,0.098039215686,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Line 1","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[94,84.875]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Line 1","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[20,20]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":200},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.149019607843,0.098039215686,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Avatar","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[34,90.875]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"circle 1","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":".onPrimaryFixedVariant","cl":"onPrimaryFixedVariant","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[252,157.5,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[504,315]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":28},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.364705882353,0.258823529412,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"app only","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0}]},{"id":"comp_2","nm":"Home_LofiLauncher","fr":60,"pfr":1,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".onPrimaryFixedVariant","cl":"onPrimaryFixedVariant","parent":4,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":195,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":204,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":383,"s":[100]},{"t":389,"s":[0]}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[0,117.5,0]},"a":{"a":0,"k":[252,275,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.364705882353,0.258823529412,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[444,275]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"hotseat - 5","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.364705882353,0.258823529412,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[396,275]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"hotseat - 4","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.364705882353,0.258823529412,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[348,275]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"hotseat - 3","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.364705882353,0.258823529412,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[300,275]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"hotseat - 2","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.364705882353,0.258823529412,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[252,275]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"hotseat - 1","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[168,20]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":15},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.364705882353,0.258823529412,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"qsb","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[132,275]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"qsb","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".onPrimaryFixedVariant","cl":"onPrimaryFixedVariant","parent":4,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":195,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":204,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":383,"s":[100]},{"t":389,"s":[0]}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[0,-29.497,0]},"a":{"a":0,"k":[252,128.003,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[20.144,20.144],[20.144,-20.144],[0,0],[-20.144,-20.144],[-20.144,20.144],[0,0]],"o":[[-20.144,-20.144],[0,0],[-20.144,20.144],[20.144,20.144],[0,0],[20.144,-20.144]],"v":[[44.892,-44.892],[-28.057,-44.892],[-44.892,-28.057],[-44.892,44.892],[28.057,44.892],[44.892,28.057]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.364705882353,0.258823529412,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"widgets","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[108,152.004]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"widgets weather","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[4.782,-2.684],[0,0],[2.63,-0.033],[0,0],[2.807,-4.716],[0,0],[2.263,-1.343],[0,0],[0.066,-5.485],[0,0],[1.292,-2.295],[0,0],[-2.683,-4.784],[0,0],[-0.033,-2.63],[0,0],[-4.716,-2.807],[0,0],[-1.338,-2.263],[0,0],[-5.483,-0.066],[0,0],[-2.296,-1.292],[0,0],[-4.782,2.683],[0,0],[-2.63,0.033],[0,0],[-2.807,4.716],[0,0],[-2.263,1.338],[0,0],[-0.066,5.483],[0,0],[-1.292,2.295],[0,0],[2.683,4.784],[0,0],[0.033,2.631],[0,0],[4.716,2.801],[0,0],[1.338,2.262],[0,0],[5.483,0.068],[0,0],[2.296,1.287]],"o":[[-4.782,-2.684],[0,0],[-2.296,1.287],[0,0],[-5.483,0.068],[0,0],[-1.338,2.262],[0,0],[-4.716,2.801],[0,0],[-0.033,2.631],[0,0],[-2.683,4.784],[0,0],[1.292,2.295],[0,0],[0.066,5.483],[0,0],[2.263,1.338],[0,0],[2.807,4.716],[0,0],[2.63,0.033],[0,0],[4.782,2.683],[0,0],[2.296,-1.292],[0,0],[5.483,-0.066],[0,0],[1.338,-2.263],[0,0],[4.716,-2.807],[0,0],[0.033,-2.63],[0,0],[2.683,-4.784],[0,0],[-1.292,-2.295],[0,0],[-0.066,-5.485],[0,0],[-2.263,-1.343],[0,0],[-2.807,-4.716],[0,0],[-2.63,-0.033],[0,0]],"v":[[7.7,-57.989],[-7.7,-57.989],[-11.019,-56.128],[-18.523,-54.117],[-22.327,-54.07],[-35.668,-46.369],[-37.609,-43.1],[-43.099,-37.605],[-46.372,-35.663],[-54.072,-22.324],[-54.118,-18.522],[-56.132,-11.016],[-57.988,-7.7],[-57.988,7.703],[-56.132,11.019],[-54.118,18.524],[-54.072,22.328],[-46.372,35.669],[-43.099,37.611],[-37.609,43.101],[-35.668,46.373],[-22.327,54.074],[-18.523,54.12],[-11.019,56.133],[-7.7,57.99],[7.7,57.99],[11.019,56.133],[18.523,54.12],[22.327,54.074],[35.668,46.373],[37.609,43.101],[43.099,37.611],[46.372,35.669],[54.072,22.328],[54.118,18.524],[56.132,11.019],[57.988,7.703],[57.988,-7.7],[56.132,-11.016],[54.118,-18.522],[54.072,-22.324],[46.372,-35.663],[43.099,-37.605],[37.609,-43.1],[35.668,-46.369],[22.327,-54.07],[18.523,-54.117],[11.019,-56.128]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.364705882353,0.258823529412,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"widgets","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[396,104.003]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"widgets clock","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".onPrimaryFixedVariant","cl":"onPrimaryFixedVariant","parent":4,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":195,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":204,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":383,"s":[100]},{"t":389,"s":[0]}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[0,-29.497,0]},"a":{"a":0,"k":[252,128.003,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.364705882353,0.258823529412,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[444,200.004]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"app - 7","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.364705882353,0.258823529412,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[348,200.004]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"app - 6","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.364705882353,0.258823529412,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[252,128.004]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"app - 4","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.364705882353,0.258823529412,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[252,56.002]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"app - 3","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.364705882353,0.258823529412,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[156,56.004]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"app - 2","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.364705882353,0.258823529412,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[60,56.004]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"app - 1","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":3,"nm":"Scale Up","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[252,157.5,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"i":{"x":[0.8,0.8,0.8],"y":[0.15,0.15,1]},"o":{"x":[0.3,0.3,0.3],"y":[0,0,0]},"t":195,"s":[85,85,100]},{"i":{"x":[0.1,0.1,0.1],"y":[1,1,1]},"o":{"x":[0.05,0.05,0.05],"y":[0.7,0.7,0]},"t":201,"s":[91,91,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":231,"s":[100,100,100]},{"i":{"x":[0.8,0.8,0.8],"y":[0.15,0.15,1]},"o":{"x":[0.3,0.3,0.3],"y":[0,0,0]},"t":380,"s":[100,100,100]},{"i":{"x":[0.1,0.1,0.1],"y":[1,1,1]},"o":{"x":[0.05,0.05,0.05],"y":[0.7,0.7,0]},"t":386,"s":[96,96,100]},{"t":416,"s":[90,90,100]}]}},"ao":0,"ef":[{"ty":5,"nm":"Void","np":19,"mn":"Pseudo/250958","ix":1,"en":1,"ef":[{"ty":0,"nm":"Width","mn":"Pseudo/250958-0001","ix":1,"v":{"a":0,"k":100}},{"ty":0,"nm":"Height","mn":"Pseudo/250958-0002","ix":2,"v":{"a":0,"k":100}},{"ty":0,"nm":"Offset X","mn":"Pseudo/250958-0003","ix":3,"v":{"a":0,"k":0}},{"ty":0,"nm":"Offset Y","mn":"Pseudo/250958-0004","ix":4,"v":{"a":0,"k":0}},{"ty":0,"nm":"Roundness","mn":"Pseudo/250958-0005","ix":5,"v":{"a":0,"k":0}},{"ty":6,"nm":"About","mn":"Pseudo/250958-0006","ix":6,"v":0},{"ty":6,"nm":"Plague of null layers.","mn":"Pseudo/250958-0007","ix":7,"v":0},{"ty":6,"nm":"Void","mn":"Pseudo/250958-0008","ix":8,"v":0},{"ty":6,"nm":"Following projects","mn":"Pseudo/250958-0009","ix":9,"v":0},{"ty":6,"nm":"Void","mn":"Pseudo/250958-0010","ix":10,"v":0},{"ty":6,"nm":"through time.","mn":"Pseudo/250958-0011","ix":11,"v":0},{"ty":6,"nm":"Void","mn":"Pseudo/250958-0012","ix":12,"v":0},{"ty":6,"nm":"Be free of the past.","mn":"Pseudo/250958-0013","ix":13,"v":0},{"ty":6,"nm":"Void","mn":"Pseudo/250958-0014","ix":14,"v":0},{"ty":6,"nm":"Copyright 2023 Battle Axe Inc","mn":"Pseudo/250958-0015","ix":15,"v":0},{"ty":6,"nm":"Void","mn":"Pseudo/250958-0016","ix":16,"v":0},{"ty":6,"nm":"Void","mn":"Pseudo/250958-0017","ix":17,"v":0}]}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":".primaryFixedDim","cl":"primaryFixedDim","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[252,157.5,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[504,315]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":28},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.925490196078,0.752941176471,0.423529411765,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"illustrations: action key","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"Home_Dismiss","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[277,282,0]},"a":{"a":0,"k":[277,282,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"w":554,"h":564,"ip":0,"op":426,"st":-25,"ct":1,"bm":0}],"markers":[],"props":{}}
\ No newline at end of file diff --git a/packages/SystemUI/res/raw/trackpad_home_success.json b/packages/SystemUI/res/raw/trackpad_home_success.json new file mode 100644 index 000000000000..f14fde5a397e --- /dev/null +++ b/packages/SystemUI/res/raw/trackpad_home_success.json @@ -0,0 +1 @@ +{"v":"5.12.1","fr":60,"ip":0,"op":50,"w":554,"h":564,"nm":"Trackpad-JSON_HomeGesture-Success","ddd":0,"assets":[{"id":"comp_0","nm":"TrackpadHome_Success_Checkmark","fr":60,"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"Check Rotate","parent":2,"sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"i":{"x":[0.12],"y":[1]},"o":{"x":[0.44],"y":[0]},"t":2,"s":[-16]},{"t":20,"s":[6]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[95.049,95.049,100]}},"ao":0,"ip":0,"op":228,"st":-72,"bm":0},{"ddd":0,"ind":2,"ty":3,"nm":"Bounce","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"i":{"x":[0.12],"y":[1]},"o":{"x":[0.44],"y":[0]},"t":12,"s":[0]},{"t":36,"s":[-6]}]},"p":{"a":0,"k":[81,127,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"i":{"x":[0.263,0.263,0.833],"y":[1.126,1.126,1]},"o":{"x":[0.05,0.05,0.05],"y":[0.958,0.958,0]},"t":1,"s":[80,80,100]},{"i":{"x":[0.1,0.1,0.1],"y":[1,1,1]},"o":{"x":[0.45,0.45,0.167],"y":[0.325,0.325,0]},"t":20,"s":[105,105,100]},{"t":36,"s":[100,100,100]}]}},"ao":0,"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".primaryFixedDim","cl":"primaryFixedDim","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":-0.289},"p":{"a":0,"k":[14.364,-33.591,0]},"a":{"a":0,"k":[-0.125,0,0]},"s":{"a":0,"k":[104.744,104.744,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[-1.401,-0.007],[-10.033,11.235]],"o":[[5.954,7.288],[1.401,0.007],[0,0]],"v":[[-28.591,4.149],[-10.73,26.013],[31.482,-21.255]],"c":false}},"nm":"Path 1","hd":false},{"ty":"tm","s":{"a":0,"k":0},"e":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.4],"y":[0]},"t":3,"s":[0]},{"i":{"x":[0.22],"y":[1]},"o":{"x":[0.001],"y":[0.149]},"t":10,"s":[29]},{"t":27,"s":[100]}]},"o":{"a":0,"k":0},"m":1,"nm":"Trim Paths 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.925490196078,0.752941176471,0.423529411765,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":11},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":5,"op":44,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".onPrimaryFixedVariant","cl":"onPrimaryFixedVariant","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[95,95,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"i":{"x":[0.275,0.275,0.21],"y":[1.102,1.102,1]},"o":{"x":[0.037,0.037,0.05],"y":[0.476,0.476,0]},"t":0,"s":[0,0,100]},{"i":{"x":[0.1,0.1,0.1],"y":[1,1,1]},"o":{"x":[0.252,0.252,0.47],"y":[0.159,0.159,0]},"t":16,"s":[120,120,100]},{"t":28,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.1,0.1],"y":[1,1]},"o":{"x":[0.32,0.32],"y":[0.11,0.11]},"t":16,"s":[148,148]},{"t":28,"s":[136,136]}]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":88},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.364705882353,0.258823529412,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Checkbox - Widget","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0}]},{"id":"comp_1","nm":"Home_LofiApp","fr":60,"pfr":1,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".onPrimaryFixedVariant","cl":"onPrimaryFixedVariant","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[339.937,151.75,0]},"a":{"a":0,"k":[339.937,151.75,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[1.021,-1.766],[0,0],[-2.043,0],[0,0],[1.022,1.767]],"o":[[-1.021,-1.766],[0,0],[-1.022,1.767],[0,0],[2.043,0],[0,0]],"v":[[2.297,-7.675],[-2.297,-7.675],[-9.64,5.025],[-7.343,9],[7.343,9],[9.64,5.025]],"c":true}},"nm":"Path 1","hd":false},{"ty":"rd","nm":"Round Corners 1","r":{"a":0,"k":9},"hd":false},{"ty":"fl","c":{"a":0,"k":[0.364705882353,0.258823529412,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Triangle","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[481.874,21]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Triangle","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[18,18]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":200},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.364705882353,0.258823529412,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Rectangle","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[457.874,21]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Rectangle","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[292,25]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":200},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.364705882353,0.258823529412,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Text field","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[334,279]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Text field","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[109,28]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":12},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.364705882353,0.258823529412,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Sent","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[425.5,208.5]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Sent","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[160,56]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":14},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.364705882353,0.258823529412,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Sent","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[400,158.5]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Sent","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[126,40]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":14},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.364705882353,0.258823529412,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Received","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[251,78.5]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Received","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".onPrimaryFixed","cl":"onPrimaryFixed","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[334,157.5,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[340,315]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":16},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.149019607843,0.098039215686,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Message","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".onPrimaryFixedVariant","cl":"onPrimaryFixedVariant","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[82,171.125,0]},"a":{"a":0,"k":[82,171.125,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[64,8]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":39.375},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.364705882353,0.258823529412,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Line 2","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[80,177.125]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Line 4","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[92,8]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":39.375},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.364705882353,0.258823529412,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Line 1","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[94,165.125]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Line 3","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[20,20]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":39.375},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.364705882353,0.258823529412,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Avatar","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[34,171.125]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"circle 2","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".onPrimaryFixed","cl":"onPrimaryFixed","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[82.5,140.5,0]},"a":{"a":0,"k":[82,140.938,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[132,22]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":39.375},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.149019607843,0.098039215686,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Search","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[82,31.5]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"header","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[64,8]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":200},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.149019607843,0.098039215686,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Line 2","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[80,257.375]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Line 6","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[92,8]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":200},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.149019607843,0.098039215686,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Line 1","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[94,245.375]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Line 5","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[20,20]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":200},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.149019607843,0.098039215686,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Avatar","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[34,251.375]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"circle 3","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[132,64]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":12},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.149019607843,0.098039215686,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Message","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[82,171]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"block","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[64,8]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":200},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.149019607843,0.098039215686,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Line 2","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[80,96.875]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Line 2","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[92,8]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":200},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.149019607843,0.098039215686,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Line 1","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[94,84.875]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Line 1","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[20,20]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":200},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.149019607843,0.098039215686,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Avatar","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[34,90.875]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"circle 1","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":".onPrimaryFixedVariant","cl":"onPrimaryFixedVariant","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[252,157.5,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[504,315]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":28},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.364705882353,0.258823529412,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"app only","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".onPrimaryFixedVariant","cl":"onPrimaryFixedVariant","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[277,459,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[200,128]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":18},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.364705882353,0.258823529412,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Frame 1321317559","bm":0,"hd":false}],"ip":0,"op":50,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"TrackpadHome_Success_Checkmark","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[277,198.5,0]},"a":{"a":0,"k":[95,95,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"w":190,"h":190,"ip":6,"op":50,"st":6,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".onPrimaryFixed","cl":"onPrimaryFixed","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":1,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":7,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":389,"s":[100]},{"t":392,"s":[0]}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[277,197.5,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"ef":[{"ty":5,"nm":"Global Position","np":4,"mn":"Pseudo/88900","ix":1,"en":1,"ef":[{"ty":10,"nm":"Master Parent","mn":"Pseudo/88900-0001","ix":1,"v":{"a":0,"k":3}},{"ty":3,"nm":"Global Position","mn":"Pseudo/88900-0002","ix":2,"v":{"k":[{"s":[277,197.5],"t":0,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,197.5],"t":49,"i":{"x":1,"y":1},"o":{"x":0,"y":0}}]}}]}],"shapes":[{"ty":"rc","d":1,"s":{"a":0,"k":[504,315]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":28},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.149019607843,0.098039215686,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false}],"ip":0,"op":50,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"matte","td":1,"sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[277,197.5,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"ef":[{"ty":5,"nm":"Global Position","np":4,"mn":"Pseudo/88900","ix":1,"en":1,"ef":[{"ty":10,"nm":"Master Parent","mn":"Pseudo/88900-0001","ix":1,"v":{"a":0,"k":3}},{"ty":3,"nm":"Global Position","mn":"Pseudo/88900-0002","ix":2,"v":{"k":[{"s":[277,197.5],"t":0,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,197.5],"t":49,"i":{"x":1,"y":1},"o":{"x":0,"y":0}}]}}]}],"shapes":[{"ty":"rc","d":1,"s":{"a":0,"k":[504,315]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":28},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false}],"ip":0,"op":50,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":5,"ty":0,"nm":"Home_LofiApp","tt":1,"tp":4,"refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[277,197.5,0]},"a":{"a":0,"k":[252,157.5,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"w":504,"h":315,"ip":0,"op":50,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":".primaryFixedDim","cl":"primaryFixedDim","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[277,197.5,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[504,315]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":28},"nm":"Rectangle Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.925490196078,0.752941176471,0.423529411765,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":14},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"op","nm":"Stroke align: Outside","a":{"k":[{"s":[7],"t":0,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[7],"t":49,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}]},"lj":1,"ml":{"a":0,"k":4},"hd":false},{"ty":"fl","c":{"a":0,"k":[0.925490196078,0.752941176471,0.423529411765,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"frame","bm":0,"hd":false}],"ip":0,"op":50,"st":0,"ct":1,"bm":0}],"markers":[],"props":{}}
\ No newline at end of file diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml index 0350cd7dab98..8cf0fb2537cc 100644 --- a/packages/SystemUI/res/values/colors.xml +++ b/packages/SystemUI/res/values/colors.xml @@ -160,8 +160,8 @@ <color name="GM2_red_300">#F28B82</color> <color name="GM2_red_500">#EA4335</color> - <color name="GM2_red_600">#B3261E</color> <color name="GM2_red_700">#C5221F</color> + <color name="GM2_red_800">#B3261E</color> <color name="GM2_blue_300">#8AB4F8</color> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 4bc46927571c..2ad6b6a4853c 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -280,13 +280,17 @@ <!-- For updated Screen Recording permission dialog (i.e. with PSS)--> <!-- Title for the screen prompting the user to begin recording their screen [CHAR LIMIT=NONE]--> - <string name="screenrecord_permission_dialog_title">Start Recording?</string> + <string name="screenrecord_permission_dialog_title">Record your screen?</string> + <!-- Screen recording permission option for recording just a single app [CHAR LIMIT=50] --> + <string name="screenrecord_permission_dialog_option_text_single_app">Record one app</string> + <!-- Screen recording permission option for recording the whole screen [CHAR LIMIT=50] --> + <string name="screenrecord_permission_dialog_option_text_entire_screen">Record entire screen</string> <!-- Message reminding the user that sensitive information may be captured during a full screen recording for the updated dialog that includes partial screen sharing option [CHAR_LIMIT=350]--> - <string name="screenrecord_permission_dialog_warning_entire_screen">While you’re recording, Android 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="screenrecord_permission_dialog_warning_entire_screen">When you’re recording your entire screen, anything shown on your screen is recorded. So be careful with things like passwords, payment details, messages, photos, and audio and video.</string> <!-- Message reminding the user that sensitive information may be captured during a single app screen recording for the updated dialog that includes partial screen sharing option [CHAR_LIMIT=350]--> - <string name="screenrecord_permission_dialog_warning_single_app">While you’re recording an app, Android 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> - <!-- Button to start a screen recording in the updated screen record dialog that allows to select an app to record [CHAR LIMIT=50]--> - <string name="screenrecord_permission_dialog_continue">Start recording</string> + <string name="screenrecord_permission_dialog_warning_single_app">When you’re recording an app, anything shown or played in that app is recorded. So be careful with things like passwords, payment details, messages, photos, and audio and video.</string> + <!-- Button to start a screen recording of the entire screen in the updated screen record dialog that allows to select an app to record [CHAR LIMIT=50]--> + <string name="screenrecord_permission_dialog_continue_entire_screen">Record screen</string> <!-- Label for a switch to enable recording audio [CHAR LIMIT=NONE]--> <string name="screenrecord_audio_label">Record audio</string> @@ -971,8 +975,8 @@ <string name="hearing_devices_presets_error">Couldn\'t update preset</string> <!-- QuickSettings: Title for hearing aids presets. Preset is a set of hearing aid settings. User can apply different settings in different environments (e.g. Outdoor, Restaurant, Home) [CHAR LIMIT=40]--> <string name="hearing_devices_preset_label">Preset</string> - <!-- QuickSettings: Tool name for hearing devices dialog related tools [CHAR LIMIT=40]--> - <string name="live_caption_title">Live Caption</string> + <!-- QuickSettings: Tool name for hearing devices dialog related tools [CHAR LIMIT=40] [BACKUP_MESSAGE_ID=8916875614623730005]--> + <string name="quick_settings_hearing_devices_live_caption_title">Live Caption</string> <!--- Title of dialog triggered if the microphone is disabled but an app tried to access it. [CHAR LIMIT=150] --> <string name="sensor_privacy_start_use_mic_dialog_title">Unblock device microphone?</string> @@ -1358,22 +1362,23 @@ <!-- Media projection permission dialog warning text for system services. [CHAR LIMIT=NONE] --> <string name="media_projection_sys_service_dialog_warning">The service providing this function will have access to all of the information that is visible on your screen or played from your device while recording or casting. This includes information such as passwords, payment details, photos, messages, and audio that you play.</string> - <!-- Permission dropdown option for sharing or recording the whole screen. [CHAR LIMIT=30] --> - <string name="screen_share_permission_dialog_option_entire_screen">Entire screen</string> - <!-- Permission dropdown option for sharing or recording single app. [CHAR LIMIT=30] --> - <string name="screen_share_permission_dialog_option_single_app">A single app</string> <!-- Title of the dialog that allows to select an app to share or record [CHAR LIMIT=NONE] --> <string name="screen_share_permission_app_selector_title">Share or record an app</string> <!-- 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] --> @@ -3682,17 +3688,35 @@ <string name="touchpad_tutorial_action_key_button">Action key</string> <!-- Label for button finishing touchpad tutorial [CHAR LIMIT=NONE] --> <string name="touchpad_tutorial_done_button">Done</string> - <!-- Screen title after gesture was done successfully [CHAR LIMIT=NONE] --> - <string name="touchpad_tutorial_gesture_done">Great job!</string> + <!-- BACK GESTURE --> <!-- Touchpad back gesture action name in tutorial [CHAR LIMIT=NONE] --> <string name="touchpad_back_gesture_action_title">Go back</string> <!-- 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 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_animation_content_description">Touchpad showing three fingers moving right and left</string> - <string name="touchpad_back_gesture_screen_animation_content_description">Device screen showing animation for 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_success_title">Nice!</string> + <!-- Text shown to the user after they complete home gesture tutorial [CHAR LIMIT=NONE] --> + <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/shared/biometrics/src/com/android/systemui/biometrics/Utils.kt b/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/Utils.kt index 12d881b20ca1..c0b6acfb2acd 100644 --- a/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/Utils.kt +++ b/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/Utils.kt @@ -16,6 +16,7 @@ package com.android.systemui.biometrics import android.Manifest +import android.app.ActivityTaskManager import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_COMPLEX @@ -35,6 +36,7 @@ import android.hardware.biometrics.PromptInfo import android.hardware.biometrics.SensorPropertiesInternal import android.os.UserManager import android.util.DisplayMetrics +import android.util.Log import android.view.ViewGroup import android.view.WindowInsets import android.view.WindowManager @@ -45,6 +47,8 @@ import com.android.internal.widget.LockPatternUtils import com.android.systemui.biometrics.shared.model.PromptKind object Utils { + private const val TAG = "SysUIBiometricUtils" + /** Base set of layout flags for fingerprint overlay widgets. */ const val FINGERPRINT_OVERLAY_LAYOUT_PARAM_FLAGS = (WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or @@ -148,4 +152,39 @@ object Utils { draw(canvas) return bitmap } + + // LINT.IfChange + @JvmStatic + /** + * Checks if a client package is running in the background or it's a system app. + * + * @param clientPackage The name of the package to be checked. + * @param clientClassNameIfItIsConfirmDeviceCredentialActivity The class name of + * ConfirmDeviceCredentialActivity. + * @return Whether the client package is running in background + */ + fun ActivityTaskManager.isSystemAppOrInBackground( + context: Context, + clientPackage: String, + clientClassNameIfItIsConfirmDeviceCredentialActivity: String? + ): Boolean { + Log.v(TAG, "Checking if the authenticating is in background, clientPackage:$clientPackage") + val tasks = getTasks(Int.MAX_VALUE) + if (tasks == null || tasks.isEmpty()) { + Log.w(TAG, "No running tasks reported") + return false + } + + val topActivity = tasks[0].topActivity + val isSystemApp = isSystem(context, clientPackage) + val topPackageEqualsToClient = topActivity!!.packageName == clientPackage + val isClientConfirmDeviceCredentialActivity = + clientClassNameIfItIsConfirmDeviceCredentialActivity != null + // b/339532378: If it's ConfirmDeviceCredentialActivity, we need to check further on + // class name. + return !(isSystemApp || topPackageEqualsToClient) || + (isClientConfirmDeviceCredentialActivity && + topActivity.className != clientClassNameIfItIsConfirmDeviceCredentialActivity) + } + // LINT.ThenChange(frameworks/base/services/core/java/com/android/server/biometrics/Utils.java) } diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButton.java b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButton.java index 317201d2c2d9..f358ba2d3ccd 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButton.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButton.java @@ -125,6 +125,7 @@ public class FloatingRotationButton implements RotationButton { taskbarMarginLeft, taskbarMarginBottom, floatingRotationButtonPositionLeft); final int diameter = res.getDimensionPixelSize(mButtonDiameterResource); + mKeyButtonView.setDiameter(diameter); mContainerSize = diameter + Math.max(defaultMargin, Math.max(taskbarMarginLeft, taskbarMarginBottom)); } @@ -195,6 +196,7 @@ public class FloatingRotationButton implements RotationButton { public void updateIcon(int lightIconColor, int darkIconColor) { mAnimatedDrawable = (AnimatedVectorDrawable) mKeyButtonView.getContext() .getDrawable(mRotationButtonController.getIconResId()); + mAnimatedDrawable.setBounds(0, 0, mKeyButtonView.getWidth(), mKeyButtonView.getHeight()); mKeyButtonView.setImageDrawable(mAnimatedDrawable); mKeyButtonView.setColors(lightIconColor, darkIconColor); } @@ -248,8 +250,14 @@ public class FloatingRotationButton implements RotationButton { updateDimensionResources(); if (mIsShowing) { + updateIcon(mRotationButtonController.getLightIconColor(), + mRotationButtonController.getDarkIconColor()); final LayoutParams layoutParams = adjustViewPositionAndCreateLayoutParams(); mWindowManager.updateViewLayout(mKeyButtonContainer, layoutParams); + if (mAnimatedDrawable != null) { + mAnimatedDrawable.reset(); + mAnimatedDrawable.start(); + } } } diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButtonView.java b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButtonView.java index 2145166e9bc5..75412f94ccb1 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButtonView.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButtonView.java @@ -37,6 +37,7 @@ public class FloatingRotationButtonView extends ImageView { private static final float BACKGROUND_ALPHA = 0.92f; private KeyButtonRipple mRipple; + private int mDiameter; private final Paint mOvalBgPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); private final Configuration mLastConfiguration; @@ -93,10 +94,25 @@ public class FloatingRotationButtonView extends ImageView { mRipple.setDarkIntensity(darkIntensity); } + /** + * Sets the view's diameter. + * + * @param diameter the diameter value for the view + */ + void setDiameter(int diameter) { + mDiameter = diameter; + } + @Override public void draw(Canvas canvas) { int d = Math.min(getWidth(), getHeight()); canvas.drawOval(0, 0, d, d, mOvalBgPaint); super.draw(canvas); } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + setMeasuredDimension(mDiameter, mDiameter); + } } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationImpl.java b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationImpl.java index 93c4630cd0cd..d81a6862c1c1 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationImpl.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationImpl.java @@ -46,6 +46,7 @@ import android.window.InputTransferToken; import androidx.annotation.NonNull; +import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.graphics.SfVsyncFrameCallbackProvider; import com.android.systemui.Flags; @@ -193,15 +194,18 @@ public class MagnificationImpl implements Magnification, CommandQueue.Callbacks private final Context mContext; private final MagnificationSettingsController.Callback mSettingsControllerCallback; private final SecureSettings mSecureSettings; + private final ViewCaptureAwareWindowManager mViewCaptureAwareWindowManager; SettingsSupplier(Context context, MagnificationSettingsController.Callback settingsControllerCallback, DisplayManager displayManager, - SecureSettings secureSettings) { + SecureSettings secureSettings, + ViewCaptureAwareWindowManager viewCaptureAwareWindowManager) { super(displayManager); mContext = context; mSettingsControllerCallback = settingsControllerCallback; mSecureSettings = secureSettings; + mViewCaptureAwareWindowManager = viewCaptureAwareWindowManager; } @Override @@ -213,7 +217,8 @@ public class MagnificationImpl implements Magnification, CommandQueue.Callbacks windowContext, new SfVsyncFrameCallbackProvider(), mSettingsControllerCallback, - mSecureSettings); + mSecureSettings, + mViewCaptureAwareWindowManager); } } @@ -227,10 +232,12 @@ public class MagnificationImpl implements Magnification, CommandQueue.Callbacks SysUiState sysUiState, OverviewProxyService overviewProxyService, SecureSettings secureSettings, DisplayTracker displayTracker, DisplayManager displayManager, AccessibilityLogger a11yLogger, - IWindowManager iWindowManager, AccessibilityManager accessibilityManager) { + IWindowManager iWindowManager, AccessibilityManager accessibilityManager, + ViewCaptureAwareWindowManager viewCaptureAwareWindowManager) { this(context, mainHandler.getLooper(), executor, commandQueue, modeSwitchesController, sysUiState, overviewProxyService, secureSettings, - displayTracker, displayManager, a11yLogger, iWindowManager, accessibilityManager); + displayTracker, displayManager, a11yLogger, iWindowManager, accessibilityManager, + viewCaptureAwareWindowManager); } @VisibleForTesting @@ -240,7 +247,8 @@ public class MagnificationImpl implements Magnification, CommandQueue.Callbacks SecureSettings secureSettings, DisplayTracker displayTracker, DisplayManager displayManager, AccessibilityLogger a11yLogger, IWindowManager iWindowManager, - AccessibilityManager accessibilityManager) { + AccessibilityManager accessibilityManager, + ViewCaptureAwareWindowManager viewCaptureAwareWindowManager) { mHandler = new Handler(looper) { @Override public void handleMessage(@NonNull Message msg) { @@ -263,7 +271,8 @@ public class MagnificationImpl implements Magnification, CommandQueue.Callbacks mFullscreenMagnificationControllerSupplier = new FullscreenMagnificationControllerSupplier( context, displayManager, mHandler, mExecutor, iWindowManager); mMagnificationSettingsSupplier = new SettingsSupplier(context, - mMagnificationSettingsControllerCallback, displayManager, secureSettings); + mMagnificationSettingsControllerCallback, displayManager, secureSettings, + viewCaptureAwareWindowManager); mModeSwitchesController.setClickListenerDelegate( displayId -> mHandler.post(() -> { 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/MagnificationSettingsController.java b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java index caf55174b6b5..fc7535a712e3 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java @@ -26,6 +26,7 @@ import android.content.res.Configuration; import android.util.Range; import android.view.WindowManager; +import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.internal.accessibility.common.MagnificationConstants; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.graphics.SfVsyncFrameCallbackProvider; @@ -60,8 +61,10 @@ public class MagnificationSettingsController implements ComponentCallbacks { @UiContext Context context, SfVsyncFrameCallbackProvider sfVsyncFrameProvider, @NonNull Callback settingsControllerCallback, - SecureSettings secureSettings) { - this(context, sfVsyncFrameProvider, settingsControllerCallback, secureSettings, null); + SecureSettings secureSettings, + ViewCaptureAwareWindowManager viewCaptureAwareWindowManager) { + this(context, sfVsyncFrameProvider, settingsControllerCallback, secureSettings, null, + viewCaptureAwareWindowManager); } @VisibleForTesting @@ -70,7 +73,8 @@ public class MagnificationSettingsController implements ComponentCallbacks { SfVsyncFrameCallbackProvider sfVsyncFrameProvider, @NonNull Callback settingsControllerCallback, SecureSettings secureSettings, - WindowMagnificationSettings windowMagnificationSettings) { + WindowMagnificationSettings windowMagnificationSettings, + ViewCaptureAwareWindowManager viewCaptureAwareWindowManager) { mContext = context.createWindowContext( context.getDisplay(), WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL, @@ -84,7 +88,7 @@ public class MagnificationSettingsController implements ComponentCallbacks { } else { mWindowMagnificationSettings = new WindowMagnificationSettings(mContext, mWindowMagnificationSettingsCallback, - sfVsyncFrameProvider, secureSettings); + sfVsyncFrameProvider, secureSettings, viewCaptureAwareWindowManager); } } 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/accessibility/WindowMagnificationSettings.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java index 99d966dfd9aa..9b6501eb1e57 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java @@ -56,6 +56,7 @@ import android.widget.LinearLayout; import android.widget.SeekBar; import android.widget.Switch; +import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.graphics.SfVsyncFrameCallbackProvider; import com.android.systemui.Flags; @@ -75,6 +76,7 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest private final Context mContext; private final AccessibilityManager mAccessibilityManager; private final WindowManager mWindowManager; + private final ViewCaptureAwareWindowManager mViewCaptureAwareWindowManager; private final SecureSettings mSecureSettings; private final Runnable mWindowInsetChangeRunnable; @@ -135,10 +137,12 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest @VisibleForTesting WindowMagnificationSettings(Context context, WindowMagnificationSettingsCallback callback, - SfVsyncFrameCallbackProvider sfVsyncFrameProvider, SecureSettings secureSettings) { + SfVsyncFrameCallbackProvider sfVsyncFrameProvider, SecureSettings secureSettings, + ViewCaptureAwareWindowManager viewCaptureAwareWindowManager) { mContext = context; mAccessibilityManager = mContext.getSystemService(AccessibilityManager.class); mWindowManager = mContext.getSystemService(WindowManager.class); + mViewCaptureAwareWindowManager = viewCaptureAwareWindowManager; mSfVsyncFrameProvider = sfVsyncFrameProvider; mCallback = callback; mSecureSettings = secureSettings; @@ -320,7 +324,7 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest // Unregister observer before removing view mSecureSettings.unregisterContentObserverSync(mMagnificationCapabilityObserver); - mWindowManager.removeView(mSettingView); + mViewCaptureAwareWindowManager.removeView(mSettingView); mIsVisible = false; if (resetPosition) { mParams.x = 0; @@ -378,7 +382,7 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest mParams.y = mDraggableWindowBounds.bottom; } - mWindowManager.addView(mSettingView, mParams); + mViewCaptureAwareWindowManager.addView(mSettingView, mParams); mSecureSettings.registerContentObserverForUserSync( Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CAPABILITY, diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java index 083f1db07886..d08653c3cf1b 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java @@ -228,7 +228,7 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, mHearingDeviceItemList = getHearingDevicesList(); if (mPresetsController != null) { activeHearingDevice = getActiveHearingDevice(mHearingDeviceItemList); - mPresetsController.setActiveHearingDevice(activeHearingDevice); + mPresetsController.setHearingDeviceIfSupportHap(activeHearingDevice); } else { activeHearingDevice = null; } @@ -336,7 +336,7 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, } final CachedBluetoothDevice activeHearingDevice = getActiveHearingDevice( mHearingDeviceItemList); - mPresetsController.setActiveHearingDevice(activeHearingDevice); + mPresetsController.setHearingDeviceIfSupportHap(activeHearingDevice); mPresetInfoAdapter = new ArrayAdapter<>(dialog.getContext(), R.layout.hearing_devices_preset_spinner_selected, @@ -499,7 +499,8 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, final List<ResolveInfo> resolved = packageManager.queryIntentActivities(LIVE_CAPTION_INTENT, /* flags= */ 0); if (!resolved.isEmpty()) { - return new ToolItem(context.getString(R.string.live_caption_title), + return new ToolItem( + context.getString(R.string.quick_settings_hearing_devices_live_caption_title), context.getDrawable(R.drawable.ic_volume_odi_captions), LIVE_CAPTION_INTENT); } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesListAdapter.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesListAdapter.java index b46b8fe4f6c8..664f3f834f86 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesListAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesListAdapter.java @@ -27,6 +27,7 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; +import com.android.settingslib.Utils; import com.android.systemui.bluetooth.qsdialog.DeviceItem; import com.android.systemui.res.R; @@ -105,6 +106,7 @@ public class HearingDevicesListAdapter extends RecyclerView.Adapter<RecyclerView private final TextView mNameView; private final TextView mSummaryView; private final ImageView mIconView; + private final ImageView mGearIcon; private final View mGearView; DeviceItemViewHolder(@NonNull View itemView, Context context) { @@ -114,6 +116,7 @@ public class HearingDevicesListAdapter extends RecyclerView.Adapter<RecyclerView mNameView = itemView.requireViewById(R.id.bluetooth_device_name); mSummaryView = itemView.requireViewById(R.id.bluetooth_device_summary); mIconView = itemView.requireViewById(R.id.bluetooth_device_icon); + mGearIcon = itemView.requireViewById(R.id.gear_icon_image); mGearView = itemView.requireViewById(R.id.gear_icon); } @@ -124,13 +127,31 @@ public class HearingDevicesListAdapter extends RecyclerView.Adapter<RecyclerView if (backgroundResId != null) { mContainer.setBackground(mContext.getDrawable(item.getBackground())); } - mNameView.setText(item.getDeviceName()); - mSummaryView.setText(item.getConnectionSummary()); + + // tint different color in different state for bad color contrast problem + int tintColor = item.isActive() ? Utils.getColorAttr(mContext, + com.android.internal.R.attr.materialColorOnPrimaryContainer).getDefaultColor() + : Utils.getColorAttr(mContext, + com.android.internal.R.attr.materialColorOnSurface).getDefaultColor(); + Pair<Drawable, String> iconPair = item.getIconWithDescription(); if (iconPair != null) { - mIconView.setImageDrawable(iconPair.getFirst()); + Drawable drawable = iconPair.getFirst().mutate(); + drawable.setTint(tintColor); + mIconView.setImageDrawable(drawable); mIconView.setContentDescription(iconPair.getSecond()); } + + mNameView.setTextAppearance( + item.isActive() ? R.style.BluetoothTileDialog_DeviceName_Active + : R.style.BluetoothTileDialog_DeviceName); + mNameView.setText(item.getDeviceName()); + mSummaryView.setTextAppearance( + item.isActive() ? R.style.BluetoothTileDialog_DeviceSummary_Active + : R.style.BluetoothTileDialog_DeviceSummary); + mSummaryView.setText(item.getConnectionSummary()); + + mGearIcon.getDrawable().mutate().setTint(tintColor); mGearView.setOnClickListener(view -> callback.onDeviceItemGearClicked(item, view)); } } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesPresetsController.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesPresetsController.java index f81124eeeb7f..aa95fd038260 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesPresetsController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesPresetsController.java @@ -113,7 +113,7 @@ public class HearingDevicesPresetsController implements @Override public void onPresetSelectionForGroupFailed(int hapGroupId, int reason) { - if (mActiveHearingDevice == null) { + if (mActiveHearingDevice == null || mHapClientProfile == null) { return; } if (hapGroupId == mHapClientProfile.getHapGroup(mActiveHearingDevice.getDevice())) { @@ -137,7 +137,7 @@ public class HearingDevicesPresetsController implements @Override public void onSetPresetNameForGroupFailed(int hapGroupId, int reason) { - if (mActiveHearingDevice == null) { + if (mActiveHearingDevice == null || mHapClientProfile == null) { return; } if (hapGroupId == mHapClientProfile.getHapGroup(mActiveHearingDevice.getDevice())) { @@ -177,22 +177,33 @@ public class HearingDevicesPresetsController implements } /** - * Sets the hearing device for this controller to control the preset. + * Sets the hearing device for this controller to control the preset if it supports + * {@link HapClientProfile}. * * @param activeHearingDevice the {@link CachedBluetoothDevice} need to be hearing aid device + * and support {@link HapClientProfile}. */ - public void setActiveHearingDevice(CachedBluetoothDevice activeHearingDevice) { - mActiveHearingDevice = activeHearingDevice; + public void setHearingDeviceIfSupportHap(CachedBluetoothDevice activeHearingDevice) { + if (mHapClientProfile == null || activeHearingDevice == null) { + mActiveHearingDevice = null; + return; + } + if (activeHearingDevice.getProfiles().stream().anyMatch( + profile -> profile instanceof HapClientProfile)) { + mActiveHearingDevice = activeHearingDevice; + } else { + mActiveHearingDevice = null; + } } /** * Selects the currently active preset for {@code mActiveHearingDevice} individual device or - * the device group accoridng to whether it supports synchronized presets or not. + * the device group according to whether it supports synchronized presets or not. * * @param presetIndex an index of one of the available presets */ public void selectPreset(int presetIndex) { - if (mActiveHearingDevice == null) { + if (mActiveHearingDevice == null || mHapClientProfile == null) { return; } mSelectedPresetIndex = presetIndex; @@ -217,7 +228,7 @@ public class HearingDevicesPresetsController implements * @return a list of all known preset info */ public List<BluetoothHapPresetInfo> getAllPresetInfo() { - if (mActiveHearingDevice == null) { + if (mActiveHearingDevice == null || mHapClientProfile == null) { return emptyList(); } return mHapClientProfile.getAllPresetInfo(mActiveHearingDevice.getDevice()).stream().filter( @@ -230,14 +241,14 @@ public class HearingDevicesPresetsController implements * @return active preset index */ public int getActivePresetIndex() { - if (mActiveHearingDevice == null) { + if (mActiveHearingDevice == null || mHapClientProfile == null) { return BluetoothHapClient.PRESET_INDEX_UNAVAILABLE; } return mHapClientProfile.getActivePresetIndex(mActiveHearingDevice.getDevice()); } private void selectPresetSynchronously(int groupId, int presetIndex) { - if (mActiveHearingDevice == null) { + if (mActiveHearingDevice == null || mHapClientProfile == null) { return; } if (DEBUG) { @@ -250,7 +261,7 @@ public class HearingDevicesPresetsController implements } private void selectPresetIndependently(int presetIndex) { - if (mActiveHearingDevice == null) { + if (mActiveHearingDevice == null || mHapClientProfile == null) { return; } if (DEBUG) { 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/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java index 9521be1f11a7..723587e60df1 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java @@ -17,11 +17,9 @@ package com.android.systemui.biometrics; import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE; -import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_POWER_BUTTON; import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; import static com.android.internal.jank.InteractionJankMonitor.CUJ_BIOMETRIC_PROMPT_TRANSITION; -import static com.android.systemui.Flags.constraintBp; import android.animation.Animator; import android.annotation.IntDef; @@ -30,8 +28,6 @@ import android.annotation.Nullable; import android.app.AlertDialog; import android.content.Context; import android.content.res.Configuration; -import android.content.res.TypedArray; -import android.graphics.Color; import android.graphics.PixelFormat; import android.hardware.biometrics.BiometricAuthenticator.Modality; import android.hardware.biometrics.BiometricConstants; @@ -41,17 +37,11 @@ import android.hardware.biometrics.PromptInfo; import android.hardware.face.FaceSensorPropertiesInternal; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.os.Binder; -import android.os.Handler; import android.os.IBinder; -import android.os.Looper; import android.os.UserManager; import android.util.Log; -import android.view.Display; -import android.view.DisplayInfo; -import android.view.Gravity; import android.view.KeyEvent; import android.view.LayoutInflater; -import android.view.Surface; import android.view.View; import android.view.ViewGroup; import android.view.WindowInsets; @@ -60,7 +50,6 @@ import android.view.animation.Interpolator; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.LinearLayout; -import android.widget.ScrollView; import android.window.OnBackInvokedCallback; import android.window.OnBackInvokedDispatcher; @@ -74,7 +63,6 @@ import com.android.systemui.biometrics.AuthController.ScaleFactorProvider; import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor; import com.android.systemui.biometrics.shared.model.BiometricModalities; import com.android.systemui.biometrics.shared.model.PromptKind; -import com.android.systemui.biometrics.ui.BiometricPromptLayout; import com.android.systemui.biometrics.ui.CredentialView; import com.android.systemui.biometrics.ui.binder.BiometricViewBinder; import com.android.systemui.biometrics.ui.binder.BiometricViewSizeBinder; @@ -111,7 +99,6 @@ public class AuthContainerView extends LinearLayout private static final int ANIMATION_DURATION_SHOW_MS = 250; private static final int ANIMATION_DURATION_AWAY_MS = 350; - private static final int ANIMATE_CREDENTIAL_START_DELAY_MS = 300; private static final int STATE_UNKNOWN = 0; private static final int STATE_ANIMATING_IN = 1; @@ -136,13 +123,11 @@ public class AuthContainerView extends LinearLayout private final Config mConfig; private final int mEffectiveUserId; - private final Handler mHandler; private final IBinder mWindowToken = new Binder(); private final WindowManager mWindowManager; private final Interpolator mLinearOutSlowIn; private final LockPatternUtils mLockPatternUtils; private final WakefulnessLifecycle mWakefulnessLifecycle; - private final AuthDialogPanelInteractionDetector mPanelInteractionDetector; private final InteractionJankMonitor mInteractionJankMonitor; private final CoroutineScope mApplicationCoroutineScope; @@ -159,10 +144,7 @@ public class AuthContainerView extends LinearLayout private final AuthPanelController mPanelController; private final ViewGroup mLayout; private final ImageView mBackgroundView; - private final ScrollView mBiometricScrollView; private final View mPanelView; - private final List<FingerprintSensorPropertiesInternal> mFpProps; - private final List<FaceSensorPropertiesInternal> mFaceProps; private final float mTranslationY; @VisibleForTesting @ContainerState int mContainerState = STATE_UNKNOWN; private final Set<Integer> mFailedModalities = new HashSet<Integer>(); @@ -229,13 +211,7 @@ public class AuthContainerView extends LinearLayout @Override public void onUseDeviceCredential() { mConfig.mCallback.onDeviceCredentialPressed(getRequestId()); - if (constraintBp()) { - addCredentialView(false /* animatePanel */, true /* animateContents */); - } else { - mHandler.postDelayed(() -> { - addCredentialView(false /* animatePanel */, true /* animateContents */); - }, mConfig.mSkipAnimation ? 0 : ANIMATE_CREDENTIAL_START_DELAY_MS); - } + addCredentialView(false /* animatePanel */, true /* animateContents */); // TODO(b/313469218): Remove Config mConfig.mPromptInfo.setAuthenticators(Authenticators.DEVICE_CREDENTIAL); @@ -303,36 +279,12 @@ public class AuthContainerView extends LinearLayout @Nullable List<FingerprintSensorPropertiesInternal> fpProps, @Nullable List<FaceSensorPropertiesInternal> faceProps, @NonNull WakefulnessLifecycle wakefulnessLifecycle, - @NonNull AuthDialogPanelInteractionDetector panelInteractionDetector, - @NonNull UserManager userManager, - @NonNull LockPatternUtils lockPatternUtils, - @NonNull InteractionJankMonitor jankMonitor, - @NonNull Provider<PromptSelectorInteractor> promptSelectorInteractor, - @NonNull PromptViewModel promptViewModel, - @NonNull Provider<CredentialViewModel> credentialViewModelProvider, - @NonNull @Background DelayableExecutor bgExecutor, - @NonNull VibratorHelper vibratorHelper) { - this(config, applicationCoroutineScope, fpProps, faceProps, - wakefulnessLifecycle, panelInteractionDetector, userManager, lockPatternUtils, - jankMonitor, promptSelectorInteractor, promptViewModel, - credentialViewModelProvider, new Handler(Looper.getMainLooper()), bgExecutor, - vibratorHelper); - } - - @VisibleForTesting - AuthContainerView(@NonNull Config config, - @NonNull CoroutineScope applicationCoroutineScope, - @Nullable List<FingerprintSensorPropertiesInternal> fpProps, - @Nullable List<FaceSensorPropertiesInternal> faceProps, - @NonNull WakefulnessLifecycle wakefulnessLifecycle, - @NonNull AuthDialogPanelInteractionDetector panelInteractionDetector, @NonNull UserManager userManager, @NonNull LockPatternUtils lockPatternUtils, @NonNull InteractionJankMonitor jankMonitor, @NonNull Provider<PromptSelectorInteractor> promptSelectorInteractorProvider, @NonNull PromptViewModel promptViewModel, @NonNull Provider<CredentialViewModel> credentialViewModelProvider, - @NonNull Handler mainHandler, @NonNull @Background DelayableExecutor bgExecutor, @NonNull VibratorHelper vibratorHelper) { super(config.mContext); @@ -340,10 +292,8 @@ public class AuthContainerView extends LinearLayout mConfig = config; mLockPatternUtils = lockPatternUtils; mEffectiveUserId = userManager.getCredentialOwnerProfile(mConfig.mUserId); - mHandler = mainHandler; mWindowManager = mContext.getSystemService(WindowManager.class); mWakefulnessLifecycle = wakefulnessLifecycle; - mPanelInteractionDetector = panelInteractionDetector; mApplicationCoroutineScope = applicationCoroutineScope; mPromptViewModel = promptViewModel; @@ -352,8 +302,6 @@ public class AuthContainerView extends LinearLayout mLinearOutSlowIn = Interpolators.LINEAR_OUT_SLOW_IN; mBiometricCallback = new BiometricCallback(); - mFpProps = fpProps; - mFaceProps = faceProps; final BiometricModalities biometricModalities = new BiometricModalities( Utils.findFirstSensorProperties(fpProps, mConfig.mSensorIds), Utils.findFirstSensorProperties(faceProps, mConfig.mSensorIds)); @@ -367,7 +315,7 @@ public class AuthContainerView extends LinearLayout final LayoutInflater layoutInflater = LayoutInflater.from(mContext); final PromptKind kind = mPromptViewModel.getPromptKind().getValue(); - if (constraintBp() && kind.isBiometric()) { + if (kind.isBiometric()) { if (kind.isTwoPaneLandscapeBiometric()) { mLayout = (ConstraintLayout) layoutInflater.inflate( R.layout.biometric_prompt_two_pane_layout, this, false /* attachToRoot */); @@ -379,26 +327,16 @@ public class AuthContainerView extends LinearLayout mLayout = (FrameLayout) layoutInflater.inflate( R.layout.auth_container_view, this, false /* attachToRoot */); } - mBiometricScrollView = mLayout.findViewById(R.id.biometric_scrollview); addView(mLayout); mBackgroundView = mLayout.findViewById(R.id.background); mPanelView = mLayout.findViewById(R.id.panel); - if (!constraintBp()) { - final TypedArray ta = mContext.obtainStyledAttributes(new int[]{ - android.R.attr.colorBackgroundFloating}); - mPanelView.setBackgroundColor(ta.getColor(0, Color.WHITE)); - ta.recycle(); - } mPanelController = new AuthPanelController(mContext, mPanelView); mBackgroundExecutor = bgExecutor; mInteractionJankMonitor = jankMonitor; mCredentialViewModelProvider = credentialViewModelProvider; - showPrompt(config, layoutInflater, promptViewModel, - Utils.findFirstSensorProperties(fpProps, mConfig.mSensorIds), - Utils.findFirstSensorProperties(faceProps, mConfig.mSensorIds), - vibratorHelper); + showPrompt(promptViewModel, vibratorHelper); // TODO: De-dupe the logic with AuthCredentialPasswordView setOnKeyListener((v, keyCode, event) -> { @@ -415,52 +353,25 @@ public class AuthContainerView extends LinearLayout requestFocus(); } - private void showPrompt(@NonNull Config config, @NonNull LayoutInflater layoutInflater, - @NonNull PromptViewModel viewModel, - @Nullable FingerprintSensorPropertiesInternal fpProps, - @Nullable FaceSensorPropertiesInternal faceProps, - @NonNull VibratorHelper vibratorHelper - ) { + private void showPrompt(@NonNull PromptViewModel viewModel, + @NonNull VibratorHelper vibratorHelper) { if (mPromptViewModel.getPromptKind().getValue().isBiometric()) { - addBiometricView(config, layoutInflater, viewModel, fpProps, faceProps, vibratorHelper); + addBiometricView(viewModel, vibratorHelper); } else if (mPromptViewModel.getPromptKind().getValue().isCredential()) { - if (constraintBp()) { - addCredentialView(true, false); - } + addCredentialView(true, false); } else { mPromptSelectorInteractorProvider.get().resetPrompt(getRequestId()); } } - private void addBiometricView(@NonNull Config config, @NonNull LayoutInflater layoutInflater, - @NonNull PromptViewModel viewModel, - @Nullable FingerprintSensorPropertiesInternal fpProps, - @Nullable FaceSensorPropertiesInternal faceProps, + private void addBiometricView(@NonNull PromptViewModel viewModel, @NonNull VibratorHelper vibratorHelper) { - - if (constraintBp()) { - mBiometricView = BiometricViewBinder.bind(mLayout, viewModel, null, - // TODO(b/201510778): This uses the wrong timeout in some cases - getJankListener(mLayout, TRANSIT, - BiometricViewSizeBinder.ANIMATE_MEDIUM_TO_LARGE_DURATION_MS), - mBackgroundView, mBiometricCallback, mApplicationCoroutineScope, - vibratorHelper); - } else { - final BiometricPromptLayout view = (BiometricPromptLayout) layoutInflater.inflate( - R.layout.biometric_prompt_layout, null, false); - mBiometricView = BiometricViewBinder.bind(view, viewModel, mPanelController, - // TODO(b/201510778): This uses the wrong timeout in some cases - getJankListener(view, TRANSIT, - BiometricViewSizeBinder.ANIMATE_MEDIUM_TO_LARGE_DURATION_MS), - mBackgroundView, mBiometricCallback, mApplicationCoroutineScope, - vibratorHelper); - - // TODO(b/251476085): migrate these dependencies - if (fpProps != null && fpProps.isAnyUdfpsType()) { - view.setUdfpsAdapter(new UdfpsDialogMeasureAdapter(view, fpProps), - config.mScaleProvider); - } - } + mBiometricView = BiometricViewBinder.bind(mLayout, viewModel, + // TODO(b/201510778): This uses the wrong timeout in some cases + getJankListener(mLayout, TRANSIT, + BiometricViewSizeBinder.ANIMATE_MEDIUM_TO_LARGE_DURATION_MS), + mBackgroundView, mBiometricCallback, mApplicationCoroutineScope, + vibratorHelper); } @VisibleForTesting @@ -524,9 +435,6 @@ public class AuthContainerView extends LinearLayout @Override public void onOrientationChanged() { - if (!constraintBp()) { - updatePositionByCapability(true /* invalidate */); - } } @Override @@ -538,23 +446,6 @@ public class AuthContainerView extends LinearLayout } mWakefulnessLifecycle.addObserver(this); - if (constraintBp()) { - // Do nothing on attachment with constraintLayout - } else if (mPromptViewModel.getPromptKind().getValue().isBiometric()) { - mBiometricScrollView.addView(mBiometricView.asView()); - } else if (mPromptViewModel.getPromptKind().getValue().isCredential()) { - addCredentialView(true /* animatePanel */, false /* animateContents */); - } else { - throw new IllegalStateException("Unknown configuration: " - + mConfig.mPromptInfo.getAuthenticators()); - } - - if (!constraintBp()) { - mPanelInteractionDetector.enable( - () -> animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED)); - updatePositionByCapability(false /* invalidate */); - } - if (mConfig.mSkipIntro) { mContainerState = STATE_SHOWING; } else { @@ -618,120 +509,8 @@ public class AuthContainerView extends LinearLayout }; } - private void updatePositionByCapability(boolean forceInvalidate) { - final FingerprintSensorPropertiesInternal fpProp = Utils.findFirstSensorProperties( - mFpProps, mConfig.mSensorIds); - final FaceSensorPropertiesInternal faceProp = Utils.findFirstSensorProperties( - mFaceProps, mConfig.mSensorIds); - if (fpProp != null && fpProp.isAnyUdfpsType()) { - maybeUpdatePositionForUdfps(forceInvalidate /* invalidate */); - } - if (faceProp != null && mBiometricView != null && mBiometricView.isFaceOnly()) { - alwaysUpdatePositionAtScreenBottom(forceInvalidate /* invalidate */); - } - if (fpProp != null && fpProp.sensorType == TYPE_POWER_BUTTON) { - alwaysUpdatePositionAtScreenBottom(forceInvalidate /* invalidate */); - } - } - - private static boolean shouldUpdatePositionForUdfps(@NonNull View view) { - if (view instanceof BiometricPromptLayout) { - // this will force the prompt to align itself on the edge of the screen - // instead of centering (temporary workaround to prevent small implicit view - // from breaking due to the way gravity / margins are set in the legacy - // AuthPanelController - return true; - } - - return false; - } - - private boolean maybeUpdatePositionForUdfps(boolean invalidate) { - final Display display = getDisplay(); - if (display == null) { - return false; - } - - final DisplayInfo cachedDisplayInfo = new DisplayInfo(); - display.getDisplayInfo(cachedDisplayInfo); - if (mBiometricView == null || !shouldUpdatePositionForUdfps(mBiometricView.asView())) { - return false; - } - - final int displayRotation = cachedDisplayInfo.rotation; - switch (displayRotation) { - case Surface.ROTATION_0: - mPanelController.setPosition(AuthPanelController.POSITION_BOTTOM); - setScrollViewGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM); - break; - - case Surface.ROTATION_90: - mPanelController.setPosition(AuthPanelController.POSITION_RIGHT); - setScrollViewGravity(Gravity.CENTER_VERTICAL | Gravity.RIGHT); - break; - - case Surface.ROTATION_270: - mPanelController.setPosition(AuthPanelController.POSITION_LEFT); - setScrollViewGravity(Gravity.CENTER_VERTICAL | Gravity.LEFT); - break; - - case Surface.ROTATION_180: - default: - Log.e(TAG, "Unsupported display rotation: " + displayRotation); - mPanelController.setPosition(AuthPanelController.POSITION_BOTTOM); - setScrollViewGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM); - break; - } - - if (invalidate) { - mPanelView.invalidateOutline(); - } - - return true; - } - - private boolean alwaysUpdatePositionAtScreenBottom(boolean invalidate) { - final Display display = getDisplay(); - if (display == null) { - return false; - } - if (mBiometricView == null || !shouldUpdatePositionForUdfps(mBiometricView.asView())) { - return false; - } - - final int displayRotation = display.getRotation(); - switch (displayRotation) { - case Surface.ROTATION_0: - case Surface.ROTATION_90: - case Surface.ROTATION_270: - case Surface.ROTATION_180: - mPanelController.setPosition(AuthPanelController.POSITION_BOTTOM); - setScrollViewGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM); - break; - default: - Log.e(TAG, "Unsupported display rotation: " + displayRotation); - mPanelController.setPosition(AuthPanelController.POSITION_BOTTOM); - setScrollViewGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM); - break; - } - - if (invalidate) { - mPanelView.invalidateOutline(); - } - - return true; - } - - private void setScrollViewGravity(int gravity) { - final FrameLayout.LayoutParams params = - (FrameLayout.LayoutParams) mBiometricScrollView.getLayoutParams(); - params.gravity = gravity; - mBiometricScrollView.setLayoutParams(params); - } - @Override public void onDetachedFromWindow() { - mPanelInteractionDetector.disable(); OnBackInvokedDispatcher dispatcher = findOnBackInvokedDispatcher(); if (dispatcher != null) { findOnBackInvokedDispatcher().unregisterOnBackInvokedCallback(mBackCallback); @@ -834,6 +613,11 @@ public class AuthContainerView extends LinearLayout } @Override + public String getClassNameIfItIsConfirmDeviceCredentialActivity() { + return mConfig.mPromptInfo.getClassNameIfItIsConfirmDeviceCredentialActivity(); + } + + @Override public long getRequestId() { return mConfig.mRequestId; } @@ -878,7 +662,7 @@ public class AuthContainerView extends LinearLayout final Runnable endActionRunnable = () -> { setVisibility(View.INVISIBLE); - if (Flags.customBiometricPrompt() && constraintBp()) { + if (Flags.customBiometricPrompt()) { // TODO(b/288175645): resetPrompt calls should be lifecycle aware mPromptSelectorInteractorProvider.get().resetPrompt(getRequestId()); } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java index b466f31cc509..037f5b72aff1 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java @@ -22,7 +22,6 @@ import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_REAR import android.annotation.NonNull; import android.annotation.Nullable; -import android.app.ActivityManager; import android.app.ActivityTaskManager; import android.app.TaskStackListener; import android.content.BroadcastReceiver; @@ -173,7 +172,6 @@ public class AuthController implements @NonNull private final SparseBooleanArray mSfpsEnrolledForUser; @NonNull private final SensorPrivacyManager mSensorPrivacyManager; private final WakefulnessLifecycle mWakefulnessLifecycle; - private final AuthDialogPanelInteractionDetector mPanelInteractionDetector; private boolean mAllFingerprintAuthenticatorsRegistered; @NonNull private final UserManager mUserManager; @NonNull private final LockPatternUtils mLockPatternUtils; @@ -187,7 +185,7 @@ public class AuthController implements final TaskStackListener mTaskStackListener = new TaskStackListener() { @Override public void onTaskStackChanged() { - if (!isOwnerInForeground()) { + if (isOwnerInBackground()) { mHandler.post(AuthController.this::cancelIfOwnerIsNotInForeground); } } @@ -227,21 +225,20 @@ public class AuthController implements } } - private boolean isOwnerInForeground() { + private boolean isOwnerInBackground() { if (mCurrentDialog != null) { final String clientPackage = mCurrentDialog.getOpPackageName(); - final List<ActivityManager.RunningTaskInfo> runningTasks = - mActivityTaskManager.getTasks(1); - if (!runningTasks.isEmpty()) { - final String topPackage = runningTasks.get(0).topActivity.getPackageName(); - if (!topPackage.contentEquals(clientPackage) - && !Utils.isSystem(mContext, clientPackage)) { - Log.w(TAG, "Evicting client due to: " + topPackage); - return false; - } + final String clientClassNameIfItIsConfirmDeviceCredentialActivity = + mCurrentDialog.getClassNameIfItIsConfirmDeviceCredentialActivity(); + final boolean isInBackground = Utils.isSystemAppOrInBackground(mActivityTaskManager, + mContext, clientPackage, + clientClassNameIfItIsConfirmDeviceCredentialActivity); + if (isInBackground) { + Log.w(TAG, "Evicting client due to top activity is not : " + clientPackage); } + return isInBackground; } - return true; + return false; } private void cancelIfOwnerIsNotInForeground() { @@ -728,7 +725,6 @@ public class AuthController implements Provider<UdfpsController> udfpsControllerFactory, @NonNull DisplayManager displayManager, @NonNull WakefulnessLifecycle wakefulnessLifecycle, - @NonNull AuthDialogPanelInteractionDetector panelInteractionDetector, @NonNull UserManager userManager, @NonNull LockPatternUtils lockPatternUtils, @NonNull Lazy<UdfpsLogger> udfpsLogger, @@ -779,7 +775,6 @@ public class AuthController implements }); mWakefulnessLifecycle = wakefulnessLifecycle; - mPanelInteractionDetector = panelInteractionDetector; mFaceProps = mFaceManager != null ? mFaceManager.getSensorPropertiesInternal() : null; @@ -1229,7 +1224,6 @@ public class AuthController implements operationId, requestId, mWakefulnessLifecycle, - mPanelInteractionDetector, mUserManager, mLockPatternUtils, viewModel); @@ -1259,9 +1253,9 @@ public class AuthController implements } mCurrentDialog = newDialog; - // TODO(b/339532378): We should check whether |allowBackgroundAuthentication| should be + // TODO(b/353597496): We should check whether |allowBackgroundAuthentication| should be // removed. - if (!promptInfo.isAllowBackgroundAuthentication() && !isOwnerInForeground()) { + if (!promptInfo.isAllowBackgroundAuthentication() && isOwnerInBackground()) { cancelIfOwnerIsNotInForeground(); } else { mCurrentDialog.show(mWindowManager); @@ -1306,7 +1300,6 @@ public class AuthController implements PromptInfo promptInfo, boolean requireConfirmation, int userId, int[] sensorIds, String opPackageName, boolean skipIntro, long operationId, long requestId, @NonNull WakefulnessLifecycle wakefulnessLifecycle, - @NonNull AuthDialogPanelInteractionDetector panelInteractionDetector, @NonNull UserManager userManager, @NonNull LockPatternUtils lockPatternUtils, @NonNull PromptViewModel viewModel) { @@ -1323,7 +1316,7 @@ public class AuthController implements config.mSensorIds = sensorIds; config.mScaleProvider = this::getScaleFactor; return new AuthContainerView(config, mApplicationCoroutineScope, mFpProps, mFaceProps, - wakefulnessLifecycle, panelInteractionDetector, userManager, lockPatternUtils, + wakefulnessLifecycle, userManager, lockPatternUtils, mInteractionJankMonitor, mPromptSelectorInteractor, viewModel, mCredentialViewModelProvider, bgExecutor, mVibratorHelper); } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java index 3fd488c34121..861191671ba9 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java @@ -94,6 +94,12 @@ public interface AuthDialog extends Dumpable { */ String getOpPackageName(); + /** + * Get the class name of ConfirmDeviceCredentialActivity. Returns null if the direct caller is + * not ConfirmDeviceCredentialActivity. + */ + String getClassNameIfItIsConfirmDeviceCredentialActivity(); + /** The requestId of the underlying operation within the framework. */ long getRequestId(); diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetector.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetector.kt deleted file mode 100644 index 04c2351c1a3e..000000000000 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetector.kt +++ /dev/null @@ -1,68 +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.biometrics - -import android.annotation.MainThread -import android.util.Log -import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.shade.domain.interactor.ShadeInteractor -import dagger.Lazy -import javax.inject.Inject -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch - -class AuthDialogPanelInteractionDetector -@Inject -constructor( - @Application private val scope: CoroutineScope, - private val shadeInteractorLazy: Lazy<ShadeInteractor>, -) { - private var shadeExpansionCollectorJob: Job? = null - - @MainThread - fun enable(onShadeInteraction: Runnable) { - if (shadeExpansionCollectorJob != null) { - Log.e(TAG, "Already enabled") - return - } - //TODO(b/313957306) delete this check - if (shadeInteractorLazy.get().isUserInteracting.value) { - // Workaround for b/311266890. This flow is in an error state that breaks this. - Log.e(TAG, "isUserInteracting already true, skipping enable") - return - } - shadeExpansionCollectorJob = - scope.launch { - Log.i(TAG, "Enable detector") - // wait for it to emit true once - shadeInteractorLazy.get().isUserInteracting.first { it } - Log.i(TAG, "Detector detected shade interaction") - onShadeInteraction.run() - } - shadeExpansionCollectorJob?.invokeOnCompletion { shadeExpansionCollectorJob = null } - } - - @MainThread - fun disable() { - Log.i(TAG, "Disable detector") - shadeExpansionCollectorJob?.cancel() - } -} - -private const val TAG = "AuthDialogPanelInteractionDetector" diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java deleted file mode 100644 index 02eae9cedf74..000000000000 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java +++ /dev/null @@ -1,386 +0,0 @@ -/* - * Copyright (C) 2021 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.biometrics; - -import android.annotation.IdRes; -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.graphics.Insets; -import android.graphics.Rect; -import android.hardware.biometrics.SensorLocationInternal; -import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; -import android.os.Build; -import android.util.Log; -import android.view.Surface; -import android.view.View; -import android.view.View.MeasureSpec; -import android.view.ViewGroup; -import android.view.WindowInsets; -import android.view.WindowManager; -import android.view.WindowMetrics; -import android.widget.FrameLayout; - -import com.android.internal.annotations.VisibleForTesting; -import com.android.systemui.res.R; - -/** - * Adapter that remeasures an auth dialog view to ensure that it matches the location of a physical - * under-display fingerprint sensor (UDFPS). - */ -public class UdfpsDialogMeasureAdapter { - private static final String TAG = "UdfpsDialogMeasurementAdapter"; - private static final boolean DEBUG = Build.IS_USERDEBUG || Build.IS_ENG; - - @NonNull private final ViewGroup mView; - @NonNull private final FingerprintSensorPropertiesInternal mSensorProps; - @Nullable private WindowManager mWindowManager; - private int mBottomSpacerHeight; - - public UdfpsDialogMeasureAdapter( - @NonNull ViewGroup view, @NonNull FingerprintSensorPropertiesInternal sensorProps) { - mView = view; - mSensorProps = sensorProps; - mWindowManager = mView.getContext().getSystemService(WindowManager.class); - } - - @NonNull - FingerprintSensorPropertiesInternal getSensorProps() { - return mSensorProps; - } - - @NonNull - public AuthDialog.LayoutParams onMeasureInternal( - int width, int height, @NonNull AuthDialog.LayoutParams layoutParams, - float scaleFactor) { - - final int displayRotation = mView.getDisplay().getRotation(); - switch (displayRotation) { - case Surface.ROTATION_0: - return onMeasureInternalPortrait(width, height, scaleFactor); - case Surface.ROTATION_90: - case Surface.ROTATION_270: - return onMeasureInternalLandscape(width, height, scaleFactor); - default: - Log.e(TAG, "Unsupported display rotation: " + displayRotation); - return layoutParams; - } - } - - /** - * @return the actual (and possibly negative) bottom spacer height. If negative, this indicates - * that the UDFPS sensor is too low. Our current xml and custom measurement logic is very hard - * too cleanly support this case. So, let's have the onLayout code translate the sensor location - * instead. - */ - public int getBottomSpacerHeight() { - return mBottomSpacerHeight; - } - - /** - * @return sensor diameter size as scaleFactor - */ - public int getSensorDiameter(float scaleFactor) { - return (int) (scaleFactor * mSensorProps.getLocation().sensorRadius * 2); - } - - @NonNull - private AuthDialog.LayoutParams onMeasureInternalPortrait(int width, int height, - float scaleFactor) { - final WindowMetrics windowMetrics = mWindowManager.getMaximumWindowMetrics(); - - // Figure out where the bottom of the sensor anim should be. - final int textIndicatorHeight = getViewHeightPx(R.id.indicator); - final int buttonBarHeight = getViewHeightPx(R.id.button_bar); - final int dialogMargin = getDialogMarginPx(); - final int displayHeight = getMaximumWindowBounds(windowMetrics).height(); - final Insets navbarInsets = getNavbarInsets(windowMetrics); - mBottomSpacerHeight = calculateBottomSpacerHeightForPortrait( - mSensorProps, displayHeight, textIndicatorHeight, buttonBarHeight, - dialogMargin, navbarInsets.bottom, scaleFactor); - - // Go through each of the children and do the custom measurement. - int totalHeight = 0; - final int numChildren = mView.getChildCount(); - final int sensorDiameter = getSensorDiameter(scaleFactor); - for (int i = 0; i < numChildren; i++) { - final View child = mView.getChildAt(i); - if (child.getId() == R.id.biometric_icon_frame) { - final FrameLayout iconFrame = (FrameLayout) child; - final View icon = iconFrame.getChildAt(0); - // Create a frame that's exactly the height of the sensor circle. - iconFrame.measure( - MeasureSpec.makeMeasureSpec( - child.getLayoutParams().width, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.EXACTLY)); - - // Ensure that the icon is never larger than the sensor. - icon.measure( - MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.AT_MOST), - MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.AT_MOST)); - } else if (child.getId() == R.id.space_above_icon - || child.getId() == R.id.space_above_content - || child.getId() == R.id.button_bar) { - child.measure( - MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec( - child.getLayoutParams().height, MeasureSpec.EXACTLY)); - } else if (child.getId() == R.id.space_below_icon) { - // Set the spacer height so the fingerprint icon is on the physical sensor area - final int clampedSpacerHeight = Math.max(mBottomSpacerHeight, 0); - child.measure( - MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(clampedSpacerHeight, MeasureSpec.EXACTLY)); - } else if (child.getId() == R.id.description - || child.getId() == R.id.customized_view_container) { - //skip description view and compute later - continue; - } else if (child.getId() == R.id.logo) { - child.measure( - MeasureSpec.makeMeasureSpec(child.getLayoutParams().width, - MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(child.getLayoutParams().height, - MeasureSpec.EXACTLY)); - } else { - child.measure( - MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST)); - } - - if (child.getVisibility() != View.GONE) { - totalHeight += child.getMeasuredHeight(); - } - } - - //re-calculate the height of body content - View description = mView.findViewById(R.id.description); - View contentView = mView.findViewById(R.id.customized_view_container); - if (description != null && description.getVisibility() != View.GONE) { - totalHeight += measureDescription(description, displayHeight, width, totalHeight); - } else if (contentView != null && contentView.getVisibility() != View.GONE) { - totalHeight += measureDescription(contentView, displayHeight, width, totalHeight); - } - - return new AuthDialog.LayoutParams(width, totalHeight); - } - - private int measureDescription(View bodyContent, int displayHeight, int currWidth, - int currHeight) { - int newHeight = bodyContent.getMeasuredHeight() + currHeight; - int limit = (int) (displayHeight * 0.75); - if (newHeight > limit) { - bodyContent.measure( - MeasureSpec.makeMeasureSpec(currWidth, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(limit - currHeight, MeasureSpec.EXACTLY)); - } - return bodyContent.getMeasuredHeight(); - } - - @NonNull - private AuthDialog.LayoutParams onMeasureInternalLandscape(int width, int height, - float scaleFactor) { - final WindowMetrics windowMetrics = mWindowManager.getMaximumWindowMetrics(); - - // Find the spacer height needed to vertically align the icon with the sensor. - final int titleHeight = getViewHeightPx(R.id.title); - final int subtitleHeight = getViewHeightPx(R.id.subtitle); - final int descriptionHeight = getViewHeightPx(R.id.description); - final int topSpacerHeight = getViewHeightPx(R.id.space_above_icon); - final int textIndicatorHeight = getViewHeightPx(R.id.indicator); - final int buttonBarHeight = getViewHeightPx(R.id.button_bar); - - final Insets navbarInsets = getNavbarInsets(windowMetrics); - final int bottomSpacerHeight = calculateBottomSpacerHeightForLandscape(titleHeight, - subtitleHeight, descriptionHeight, topSpacerHeight, textIndicatorHeight, - buttonBarHeight, navbarInsets.bottom); - - // Find the spacer width needed to horizontally align the icon with the sensor. - final int displayWidth = getMaximumWindowBounds(windowMetrics).width(); - final int dialogMargin = getDialogMarginPx(); - final int horizontalInset = navbarInsets.left + navbarInsets.right; - final int horizontalSpacerWidth = calculateHorizontalSpacerWidthForLandscape( - mSensorProps, displayWidth, dialogMargin, horizontalInset, scaleFactor); - - final int sensorDiameter = getSensorDiameter(scaleFactor); - final int remeasuredWidth = sensorDiameter + 2 * horizontalSpacerWidth; - - int remeasuredHeight = 0; - final int numChildren = mView.getChildCount(); - for (int i = 0; i < numChildren; i++) { - final View child = mView.getChildAt(i); - if (child.getId() == R.id.biometric_icon_frame) { - final FrameLayout iconFrame = (FrameLayout) child; - final View icon = iconFrame.getChildAt(0); - // Create a frame that's exactly the height of the sensor circle. - iconFrame.measure( - MeasureSpec.makeMeasureSpec(remeasuredWidth, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.EXACTLY)); - - // Ensure that the icon is never larger than the sensor. - icon.measure( - MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.AT_MOST), - MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.AT_MOST)); - } else if (child.getId() == R.id.space_above_icon) { - // Adjust the width and height of the top spacer if necessary. - final int newTopSpacerHeight = child.getLayoutParams().height - - Math.min(bottomSpacerHeight, 0); - child.measure( - MeasureSpec.makeMeasureSpec(remeasuredWidth, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(newTopSpacerHeight, MeasureSpec.EXACTLY)); - } else if (child.getId() == R.id.button_bar) { - // Adjust the width of the button bar while preserving its height. - child.measure( - MeasureSpec.makeMeasureSpec(remeasuredWidth, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec( - child.getLayoutParams().height, MeasureSpec.EXACTLY)); - } else if (child.getId() == R.id.space_below_icon) { - // Adjust the bottom spacer height to align the fingerprint icon with the sensor. - final int newBottomSpacerHeight = Math.max(bottomSpacerHeight, 0); - child.measure( - MeasureSpec.makeMeasureSpec(remeasuredWidth, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(newBottomSpacerHeight, MeasureSpec.EXACTLY)); - } else { - // Use the remeasured width for all other child views. - child.measure( - MeasureSpec.makeMeasureSpec(remeasuredWidth, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST)); - } - - if (child.getVisibility() != View.GONE) { - remeasuredHeight += child.getMeasuredHeight(); - } - } - - return new AuthDialog.LayoutParams(remeasuredWidth, remeasuredHeight); - } - - private int getViewHeightPx(@IdRes int viewId) { - final View view = mView.findViewById(viewId); - return view != null && view.getVisibility() != View.GONE ? view.getMeasuredHeight() : 0; - } - - private int getDialogMarginPx() { - return mView.getResources().getDimensionPixelSize(R.dimen.biometric_dialog_border_padding); - } - - @NonNull - private static Insets getNavbarInsets(@Nullable WindowMetrics windowMetrics) { - return windowMetrics != null - ? windowMetrics.getWindowInsets().getInsets(WindowInsets.Type.navigationBars()) - : Insets.NONE; - } - - @NonNull - private static Rect getMaximumWindowBounds(@Nullable WindowMetrics windowMetrics) { - return windowMetrics != null ? windowMetrics.getBounds() : new Rect(); - } - - /** - * For devices in portrait orientation where the sensor is too high up, calculates the amount of - * padding necessary to center the biometric icon within the sensor's physical location. - */ - @VisibleForTesting - static int calculateBottomSpacerHeightForPortrait( - @NonNull FingerprintSensorPropertiesInternal sensorProperties, int displayHeightPx, - int textIndicatorHeightPx, int buttonBarHeightPx, int dialogMarginPx, - int navbarBottomInsetPx, float scaleFactor) { - final SensorLocationInternal location = sensorProperties.getLocation(); - final int sensorDistanceFromBottom = displayHeightPx - - (int) (scaleFactor * location.sensorLocationY) - - (int) (scaleFactor * location.sensorRadius); - - final int spacerHeight = sensorDistanceFromBottom - - textIndicatorHeightPx - - buttonBarHeightPx - - dialogMarginPx - - navbarBottomInsetPx; - - if (DEBUG) { - Log.d(TAG, "Display height: " + displayHeightPx - + ", Distance from bottom: " + sensorDistanceFromBottom - + ", Bottom margin: " + dialogMarginPx - + ", Navbar bottom inset: " + navbarBottomInsetPx - + ", Bottom spacer height (portrait): " + spacerHeight - + ", Scale Factor: " + scaleFactor); - } - - return spacerHeight; - } - - /** - * For devices in landscape orientation where the sensor is too high up, calculates the amount - * of padding necessary to center the biometric icon within the sensor's physical location. - */ - @VisibleForTesting - static int calculateBottomSpacerHeightForLandscape(int titleHeightPx, int subtitleHeightPx, - int descriptionHeightPx, int topSpacerHeightPx, int textIndicatorHeightPx, - int buttonBarHeightPx, int navbarBottomInsetPx) { - - final int dialogHeightAboveIcon = titleHeightPx - + subtitleHeightPx - + descriptionHeightPx - + topSpacerHeightPx; - - final int dialogHeightBelowIcon = textIndicatorHeightPx + buttonBarHeightPx; - - final int bottomSpacerHeight = dialogHeightAboveIcon - - dialogHeightBelowIcon - - navbarBottomInsetPx; - - if (DEBUG) { - Log.d(TAG, "Title height: " + titleHeightPx - + ", Subtitle height: " + subtitleHeightPx - + ", Description height: " + descriptionHeightPx - + ", Top spacer height: " + topSpacerHeightPx - + ", Text indicator height: " + textIndicatorHeightPx - + ", Button bar height: " + buttonBarHeightPx - + ", Navbar bottom inset: " + navbarBottomInsetPx - + ", Bottom spacer height (landscape): " + bottomSpacerHeight); - } - - return bottomSpacerHeight; - } - - /** - * For devices in landscape orientation where the sensor is too left/right, calculates the - * amount of padding necessary to center the biometric icon within the sensor's physical - * location. - */ - @VisibleForTesting - static int calculateHorizontalSpacerWidthForLandscape( - @NonNull FingerprintSensorPropertiesInternal sensorProperties, int displayWidthPx, - int dialogMarginPx, int navbarHorizontalInsetPx, float scaleFactor) { - final SensorLocationInternal location = sensorProperties.getLocation(); - final int sensorDistanceFromEdge = displayWidthPx - - (int) (scaleFactor * location.sensorLocationY) - - (int) (scaleFactor * location.sensorRadius); - - final int horizontalPadding = sensorDistanceFromEdge - - dialogMarginPx - - navbarHorizontalInsetPx; - - if (DEBUG) { - Log.d(TAG, "Display width: " + displayWidthPx - + ", Distance from edge: " + sensorDistanceFromEdge - + ", Dialog margin: " + dialogMarginPx - + ", Navbar horizontal inset: " + navbarHorizontalInsetPx - + ", Horizontal spacer width (landscape): " + horizontalPadding - + ", Scale Factor: " + scaleFactor); - } - - return horizontalPadding; - } -} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt index 5e2b5ff5c1ac..6da5e42c12d9 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt @@ -188,7 +188,6 @@ constructor( val hasCredentialViewShown = promptKind.value.isCredential() val showBpForCredential = Flags.customBiometricPrompt() && - com.android.systemui.Flags.constraintBp() && !Utils.isBiometricAllowed(promptInfo) && isDeviceCredentialAllowed(promptInfo) && promptInfo.contentView != null && diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt index 348b4234a430..695707d782b7 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt @@ -44,7 +44,7 @@ sealed class BiometricPromptRequest( val logoDescription: String? = info.logoDescription val negativeButtonText: String = info.negativeButtonText?.toString() ?: "" val componentNameForConfirmDeviceCredentialActivity: ComponentName? = - info.componentNameForConfirmDeviceCredentialActivity + info.realCallerForConfirmDeviceCredentialActivity val allowBackgroundAuthentication = info.isAllowBackgroundAuthentication } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java b/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java deleted file mode 100644 index b450896729b7..000000000000 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java +++ /dev/null @@ -1,188 +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.biometrics.ui; - -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.content.Context; -import android.graphics.Insets; -import android.util.AttributeSet; -import android.util.Log; -import android.view.View; -import android.view.WindowInsets; -import android.view.WindowManager; -import android.widget.FrameLayout; -import android.widget.LinearLayout; -import android.widget.TextView; - -import com.android.systemui.biometrics.AuthController; -import com.android.systemui.biometrics.AuthDialog; -import com.android.systemui.biometrics.UdfpsDialogMeasureAdapter; -import com.android.systemui.res.R; - -import kotlin.Pair; - -/** - * Contains the Biometric views (title, subtitle, icon, buttons, etc.). - * - * TODO(b/251476085): get the udfps junk out of here, at a minimum. Likely can be replaced with a - * normal LinearLayout. - */ -public class BiometricPromptLayout extends LinearLayout { - - private static final String TAG = "BiometricPromptLayout"; - - @NonNull - private final WindowManager mWindowManager; - @Nullable - private AuthController.ScaleFactorProvider mScaleFactorProvider; - @Nullable - private UdfpsDialogMeasureAdapter mUdfpsAdapter; - - private final boolean mUseCustomBpSize; - private final int mCustomBpWidth; - private final int mCustomBpHeight; - - public BiometricPromptLayout(Context context) { - this(context, null); - } - - public BiometricPromptLayout(Context context, AttributeSet attrs) { - super(context, attrs); - - mWindowManager = context.getSystemService(WindowManager.class); - - mUseCustomBpSize = getResources().getBoolean(R.bool.use_custom_bp_size); - mCustomBpWidth = getResources().getDimensionPixelSize(R.dimen.biometric_dialog_width); - mCustomBpHeight = getResources().getDimensionPixelSize(R.dimen.biometric_dialog_height); - } - - @Deprecated - public void setUdfpsAdapter(@NonNull UdfpsDialogMeasureAdapter adapter, - @NonNull AuthController.ScaleFactorProvider scaleProvider) { - mUdfpsAdapter = adapter; - mScaleFactorProvider = scaleProvider != null ? scaleProvider : () -> 1.0f; - } - - @Deprecated - public boolean isUdfps() { - return mUdfpsAdapter != null; - } - - @Deprecated - public Pair<Integer, Integer> getUpdatedFingerprintAffordanceSize() { - if (mUdfpsAdapter != null) { - final int sensorDiameter = mUdfpsAdapter.getSensorDiameter( - mScaleFactorProvider.provide()); - return new Pair(sensorDiameter, sensorDiameter); - } - return null; - } - - @NonNull - private AuthDialog.LayoutParams onMeasureInternal(int width, int height) { - int totalHeight = 0; - final int numChildren = getChildCount(); - for (int i = 0; i < numChildren; i++) { - final View child = getChildAt(i); - - if (child.getId() == R.id.space_above_icon - || child.getId() == R.id.space_above_content - || child.getId() == R.id.space_below_icon - || child.getId() == R.id.button_bar) { - child.measure( - MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(child.getLayoutParams().height, - MeasureSpec.EXACTLY)); - } else if (child.getId() == R.id.biometric_icon_frame) { - final View iconView = findViewById(R.id.biometric_icon); - child.measure( - MeasureSpec.makeMeasureSpec(iconView.getLayoutParams().width, - MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(iconView.getLayoutParams().height, - MeasureSpec.EXACTLY)); - } else if (child.getId() == R.id.logo) { - child.measure( - MeasureSpec.makeMeasureSpec(child.getLayoutParams().width, - MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(child.getLayoutParams().height, - MeasureSpec.EXACTLY)); - } else if (child.getId() == R.id.biometric_icon) { - child.measure( - MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST), - MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST)); - } else { - child.measure( - MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST)); - } - - if (child.getVisibility() != View.GONE) { - totalHeight += child.getMeasuredHeight(); - } - } - - final AuthDialog.LayoutParams params = new AuthDialog.LayoutParams(width, totalHeight); - if (mUdfpsAdapter != null) { - return mUdfpsAdapter.onMeasureInternal(width, height, params, - (mScaleFactorProvider != null) ? mScaleFactorProvider.provide() : 1.0f); - } else { - return params; - } - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - int width = MeasureSpec.getSize(widthMeasureSpec); - int height = MeasureSpec.getSize(heightMeasureSpec); - - if (mUseCustomBpSize) { - width = mCustomBpWidth; - height = mCustomBpHeight; - } else { - width = Math.min(width, height); - } - - // add nav bar insets since the parent AuthContainerView - // uses LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS - final Insets insets = mWindowManager.getMaximumWindowMetrics().getWindowInsets() - .getInsets(WindowInsets.Type.navigationBars()); - final AuthDialog.LayoutParams params = onMeasureInternal(width, height); - setMeasuredDimension(params.mMediumWidth + insets.left + insets.right, - params.mMediumHeight + insets.bottom); - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - - if (mUdfpsAdapter != null) { - // Move the UDFPS icon and indicator text if necessary. This probably only needs to - // happen for devices where the UDFPS sensor is too low. - // TODO(b/201510778): Update this logic to support cases where the sensor or text - // overlap the button bar area. - final float bottomSpacerHeight = mUdfpsAdapter.getBottomSpacerHeight(); - Log.w(TAG, "bottomSpacerHeight: " + bottomSpacerHeight); - if (bottomSpacerHeight < 0) { - final FrameLayout iconFrame = findViewById(R.id.biometric_icon_frame); - iconFrame.setTranslationY(-bottomSpacerHeight); - final TextView indicator = findViewById(R.id.indicator); - indicator.setTranslationY(-bottomSpacerHeight); - } - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricCustomizedViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricCustomizedViewBinder.kt index 7ccac03bcac6..0b474f8092a4 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricCustomizedViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricCustomizedViewBinder.kt @@ -38,14 +38,13 @@ import android.widget.LinearLayout import android.widget.Space import android.widget.TextView import com.android.settingslib.Utils -import com.android.systemui.biometrics.ui.BiometricPromptLayout import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.res.R import kotlin.math.ceil private const val TAG = "BiometricCustomizedViewBinder" -/** Sub-binder for [BiometricPromptLayout.customized_view_container]. */ +/** Sub-binder for Biometric Prompt Customized View */ object BiometricCustomizedViewBinder { fun bind( customizedViewContainer: LinearLayout, diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt index 43ba097684d6..a20a17f13a42 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt @@ -45,13 +45,10 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.airbnb.lottie.LottieAnimationView import com.airbnb.lottie.LottieCompositionFactory -import com.android.systemui.Flags.constraintBp -import com.android.systemui.biometrics.AuthPanelController import com.android.systemui.biometrics.shared.model.BiometricModalities import com.android.systemui.biometrics.shared.model.BiometricModality import com.android.systemui.biometrics.shared.model.PromptKind import com.android.systemui.biometrics.shared.model.asBiometricModality -import com.android.systemui.biometrics.ui.BiometricPromptLayout import com.android.systemui.biometrics.ui.viewmodel.FingerprintStartMode import com.android.systemui.biometrics.ui.viewmodel.PromptMessage import com.android.systemui.biometrics.ui.viewmodel.PromptSize @@ -72,28 +69,18 @@ private const val MAX_LOGO_DESCRIPTION_CHARACTER_NUMBER = 30 /** Top-most view binder for BiometricPrompt views. */ object BiometricViewBinder { - /** Binds a [BiometricPromptLayout] to a [PromptViewModel]. */ + /** Binds a Biometric Prompt View to a [PromptViewModel]. */ @SuppressLint("ClickableViewAccessibility") @JvmStatic fun bind( view: View, viewModel: PromptViewModel, - panelViewController: AuthPanelController?, jankListener: BiometricJankListener, backgroundView: View, legacyCallback: Spaghetti.Callback, applicationScope: CoroutineScope, vibratorHelper: VibratorHelper, ): Spaghetti { - /** - * View is only set visible in BiometricViewSizeBinder once PromptSize is determined that - * accounts for iconView size, to prevent prompt resizing being visible to the user. - * - * TODO(b/288175072): May be able to remove this once constraint layout is implemented - */ - if (!constraintBp()) { - view.visibility = View.INVISIBLE - } val accessibilityManager = view.context.getSystemService(AccessibilityManager::class.java)!! val textColorError = @@ -104,9 +91,7 @@ object BiometricViewBinder { R.style.TextAppearance_AuthCredential_Indicator, intArrayOf(android.R.attr.textColor) ) - val textColorHint = - if (constraintBp()) attributes.getColor(0, 0) - else view.resources.getColor(R.color.biometric_dialog_gray, view.context.theme) + val textColorHint = attributes.getColor(0, 0) attributes.recycle() val logoView = view.requireViewById<ImageView>(R.id.logo) @@ -116,12 +101,7 @@ object BiometricViewBinder { val descriptionView = view.requireViewById<TextView>(R.id.description) val customizedViewContainer = view.requireViewById<LinearLayout>(R.id.customized_view_container) - val udfpsGuidanceView = - if (constraintBp()) { - view.requireViewById<View>(R.id.panel) - } else { - backgroundView - } + val udfpsGuidanceView = view.requireViewById<View>(R.id.panel) // set selected to enable marquee unless a screen reader is enabled titleView.isSelected = @@ -130,14 +110,6 @@ object BiometricViewBinder { !accessibilityManager.isEnabled || !accessibilityManager.isTouchExplorationEnabled val iconView = view.requireViewById<LottieAnimationView>(R.id.biometric_icon) - - val iconSizeOverride = - if (constraintBp()) { - null - } else { - (view as BiometricPromptLayout).updatedFingerprintAffordanceSize - } - val indicatorMessageView = view.requireViewById<TextView>(R.id.indicator) // Negative-side (left) buttons @@ -213,7 +185,7 @@ object BiometricViewBinder { subtitleView.text = viewModel.subtitle.first() descriptionView.text = viewModel.description.first() - if (Flags.customBiometricPrompt() && constraintBp()) { + if (Flags.customBiometricPrompt()) { BiometricCustomizedViewBinder.bind( customizedViewContainer, viewModel.contentView.first(), @@ -250,22 +222,6 @@ object BiometricViewBinder { descriptionView, customizedViewContainer, ), - viewsToFadeInOnSizeChange = - listOf( - logoView, - logoDescriptionView, - titleView, - subtitleView, - descriptionView, - customizedViewContainer, - indicatorMessageView, - negativeButton, - cancelButton, - retryButton, - confirmationButton, - credentialFallbackButton, - ), - panelViewController = panelViewController, jankListener = jankListener, ) } @@ -275,7 +231,6 @@ object BiometricViewBinder { if (!showWithoutIcon) { PromptIconViewBinder.bind( iconView, - iconSizeOverride, viewModel, ) } @@ -329,20 +284,6 @@ object BiometricViewBinder { } } - // set padding - launch { - viewModel.promptPadding.collect { promptPadding -> - if (!constraintBp()) { - view.setPadding( - promptPadding.left, - promptPadding.top, - promptPadding.right, - promptPadding.bottom - ) - } - } - } - // configure & hide/disable buttons launch { viewModel.credentialKind @@ -546,24 +487,6 @@ class Spaghetti( fun onAuthenticatedAndConfirmed() } - @Deprecated("TODO(b/330788871): remove after replacing AuthContainerView") - enum class BiometricState { - /** Authentication hardware idle. */ - STATE_IDLE, - /** UI animating in, authentication hardware active. */ - STATE_AUTHENTICATING_ANIMATING_IN, - /** UI animated in, authentication hardware active. */ - STATE_AUTHENTICATING, - /** UI animated in, authentication hardware active. */ - STATE_HELP, - /** Hard error, e.g. ERROR_TIMEOUT. Authentication hardware idle. */ - STATE_ERROR, - /** Authenticated, waiting for user confirmation. Authentication hardware idle. */ - STATE_PENDING_CONFIRMATION, - /** Authenticated, dialog animating away soon. */ - STATE_AUTHENTICATED, - } - private var lifecycleScope: CoroutineScope? = null private var modalities: BiometricModalities = BiometricModalities() private var legacyCallback: Callback? = null @@ -699,15 +622,8 @@ class Spaghetti( } fun startTransitionToCredentialUI(isError: Boolean) { - if (!constraintBp()) { - applicationScope.launch { - viewModel.onSwitchToCredential() - legacyCallback?.onUseDeviceCredential() - } - } else { - viewModel.onSwitchToCredential() - legacyCallback?.onUseDeviceCredential() - } + viewModel.onSwitchToCredential() + legacyCallback?.onUseDeviceCredential() } fun cancelAnimation() { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt index b9ec2de63269..85c3ae3f214e 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt @@ -38,10 +38,7 @@ import androidx.constraintlayout.widget.ConstraintSet import androidx.constraintlayout.widget.Guideline import androidx.core.animation.addListener import androidx.core.view.doOnLayout -import androidx.core.view.isGone import androidx.lifecycle.lifecycleScope -import com.android.systemui.Flags.constraintBp -import com.android.systemui.biometrics.AuthPanelController import com.android.systemui.biometrics.Utils import com.android.systemui.biometrics.ui.viewmodel.PromptPosition import com.android.systemui.biometrics.ui.viewmodel.PromptSize @@ -49,7 +46,6 @@ import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel import com.android.systemui.biometrics.ui.viewmodel.isLarge import com.android.systemui.biometrics.ui.viewmodel.isLeft import com.android.systemui.biometrics.ui.viewmodel.isMedium -import com.android.systemui.biometrics.ui.viewmodel.isNullOrNotSmall import com.android.systemui.biometrics.ui.viewmodel.isSmall import com.android.systemui.biometrics.ui.viewmodel.isTop import com.android.systemui.lifecycle.repeatWhenAttached @@ -71,8 +67,6 @@ object BiometricViewSizeBinder { view: View, viewModel: PromptViewModel, viewsToHideWhenSmall: List<View>, - viewsToFadeInOnSizeChange: List<View>, - panelViewController: AuthPanelController?, jankListener: BiometricJankListener, ) { val windowManager = requireNotNull(view.context.getSystemService(WindowManager::class.java)) @@ -92,553 +86,366 @@ object BiometricViewSizeBinder { } } - if (constraintBp()) { - val leftGuideline = view.requireViewById<Guideline>(R.id.leftGuideline) - val topGuideline = view.requireViewById<Guideline>(R.id.topGuideline) - val rightGuideline = view.requireViewById<Guideline>(R.id.rightGuideline) - val midGuideline = view.findViewById<Guideline>(R.id.midGuideline) - - val iconHolderView = view.requireViewById<View>(R.id.biometric_icon) - val panelView = view.requireViewById<View>(R.id.panel) - val cornerRadius = view.resources.getDimension(R.dimen.biometric_dialog_corner_size) - val pxToDp = - TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, - 1f, - view.resources.displayMetrics - ) - val cornerRadiusPx = (pxToDp * cornerRadius).toInt() - - var currentSize: PromptSize? = null - var currentPosition: PromptPosition = PromptPosition.Bottom - panelView.outlineProvider = - object : ViewOutlineProvider() { - override fun getOutline(view: View, outline: Outline) { - when (currentPosition) { - PromptPosition.Right -> { - outline.setRoundRect( - 0, - 0, - view.width + cornerRadiusPx, - view.height, - cornerRadiusPx.toFloat() - ) - } - PromptPosition.Left -> { - outline.setRoundRect( - -cornerRadiusPx, - 0, - view.width, - view.height, - cornerRadiusPx.toFloat() - ) - } - PromptPosition.Bottom, - PromptPosition.Top -> { - outline.setRoundRect( - 0, - 0, - view.width, - view.height + cornerRadiusPx, - cornerRadiusPx.toFloat() - ) - } + val leftGuideline = view.requireViewById<Guideline>(R.id.leftGuideline) + val topGuideline = view.requireViewById<Guideline>(R.id.topGuideline) + val rightGuideline = view.requireViewById<Guideline>(R.id.rightGuideline) + val midGuideline = view.findViewById<Guideline>(R.id.midGuideline) + + val iconHolderView = view.requireViewById<View>(R.id.biometric_icon) + val panelView = view.requireViewById<View>(R.id.panel) + val cornerRadius = view.resources.getDimension(R.dimen.biometric_dialog_corner_size) + val pxToDp = + TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + 1f, + view.resources.displayMetrics + ) + val cornerRadiusPx = (pxToDp * cornerRadius).toInt() + + var currentSize: PromptSize? = null + var currentPosition: PromptPosition = PromptPosition.Bottom + panelView.outlineProvider = + object : ViewOutlineProvider() { + override fun getOutline(view: View, outline: Outline) { + when (currentPosition) { + PromptPosition.Right -> { + outline.setRoundRect( + 0, + 0, + view.width + cornerRadiusPx, + view.height, + cornerRadiusPx.toFloat() + ) } - } - } - - // ConstraintSets for animating between prompt sizes - val mediumConstraintSet = ConstraintSet() - mediumConstraintSet.clone(view as ConstraintLayout) - - val smallConstraintSet = ConstraintSet() - smallConstraintSet.clone(mediumConstraintSet) - - val largeConstraintSet = ConstraintSet() - largeConstraintSet.clone(mediumConstraintSet) - largeConstraintSet.constrainMaxWidth(R.id.panel, 0) - largeConstraintSet.setGuidelineBegin(R.id.leftGuideline, 0) - largeConstraintSet.setGuidelineEnd(R.id.rightGuideline, 0) - - // TODO: Investigate better way to handle 180 rotations - val flipConstraintSet = ConstraintSet() - - view.doOnLayout { - fun setVisibilities(hideSensorIcon: Boolean, size: PromptSize) { - viewsToHideWhenSmall.forEach { it.showContentOrHide(forceHide = size.isSmall) } - largeConstraintSet.setVisibility(iconHolderView.id, View.GONE) - largeConstraintSet.setVisibility(R.id.indicator, View.GONE) - largeConstraintSet.setVisibility(R.id.scrollView, View.GONE) - - if (hideSensorIcon) { - smallConstraintSet.setVisibility(iconHolderView.id, View.GONE) - smallConstraintSet.setVisibility(R.id.indicator, View.GONE) - mediumConstraintSet.setVisibility(iconHolderView.id, View.GONE) - mediumConstraintSet.setVisibility(R.id.indicator, View.GONE) - } - } - - view.repeatWhenAttached { - lifecycleScope.launch { - viewModel.iconPosition.collect { position -> - if (position != Rect()) { - val iconParams = - iconHolderView.layoutParams as ConstraintLayout.LayoutParams - - if (position.left != 0) { - iconParams.endToEnd = ConstraintSet.UNSET - iconParams.leftMargin = position.left - mediumConstraintSet.clear( - R.id.biometric_icon, - ConstraintSet.RIGHT - ) - mediumConstraintSet.connect( - R.id.biometric_icon, - ConstraintSet.LEFT, - ConstraintSet.PARENT_ID, - ConstraintSet.LEFT - ) - mediumConstraintSet.setMargin( - R.id.biometric_icon, - ConstraintSet.LEFT, - position.left - ) - smallConstraintSet.clear( - R.id.biometric_icon, - ConstraintSet.RIGHT - ) - smallConstraintSet.connect( - R.id.biometric_icon, - ConstraintSet.LEFT, - ConstraintSet.PARENT_ID, - ConstraintSet.LEFT - ) - smallConstraintSet.setMargin( - R.id.biometric_icon, - ConstraintSet.LEFT, - position.left - ) - } - if (position.top != 0) { - iconParams.bottomToBottom = ConstraintSet.UNSET - iconParams.topMargin = position.top - mediumConstraintSet.clear( - R.id.biometric_icon, - ConstraintSet.BOTTOM - ) - mediumConstraintSet.setMargin( - R.id.biometric_icon, - ConstraintSet.TOP, - position.top - ) - smallConstraintSet.clear( - R.id.biometric_icon, - ConstraintSet.BOTTOM - ) - smallConstraintSet.setMargin( - R.id.biometric_icon, - ConstraintSet.TOP, - position.top - ) - } - if (position.right != 0) { - iconParams.startToStart = ConstraintSet.UNSET - iconParams.rightMargin = position.right - mediumConstraintSet.clear( - R.id.biometric_icon, - ConstraintSet.LEFT - ) - mediumConstraintSet.connect( - R.id.biometric_icon, - ConstraintSet.RIGHT, - ConstraintSet.PARENT_ID, - ConstraintSet.RIGHT - ) - mediumConstraintSet.setMargin( - R.id.biometric_icon, - ConstraintSet.RIGHT, - position.right - ) - smallConstraintSet.clear( - R.id.biometric_icon, - ConstraintSet.LEFT - ) - smallConstraintSet.connect( - R.id.biometric_icon, - ConstraintSet.RIGHT, - ConstraintSet.PARENT_ID, - ConstraintSet.RIGHT - ) - smallConstraintSet.setMargin( - R.id.biometric_icon, - ConstraintSet.RIGHT, - position.right - ) - } - if (position.bottom != 0) { - iconParams.topToTop = ConstraintSet.UNSET - iconParams.bottomMargin = position.bottom - mediumConstraintSet.clear( - R.id.biometric_icon, - ConstraintSet.TOP - ) - mediumConstraintSet.setMargin( - R.id.biometric_icon, - ConstraintSet.BOTTOM, - position.bottom - ) - smallConstraintSet.clear(R.id.biometric_icon, ConstraintSet.TOP) - smallConstraintSet.setMargin( - R.id.biometric_icon, - ConstraintSet.BOTTOM, - position.bottom - ) - } - iconHolderView.layoutParams = iconParams - } + PromptPosition.Left -> { + outline.setRoundRect( + -cornerRadiusPx, + 0, + view.width, + view.height, + cornerRadiusPx.toFloat() + ) } - } - - lifecycleScope.launch { - viewModel.iconSize.collect { iconSize -> - iconHolderView.layoutParams.width = iconSize.first - iconHolderView.layoutParams.height = iconSize.second - mediumConstraintSet.constrainWidth(R.id.biometric_icon, iconSize.first) - mediumConstraintSet.constrainHeight( - R.id.biometric_icon, - iconSize.second + PromptPosition.Bottom, + PromptPosition.Top -> { + outline.setRoundRect( + 0, + 0, + view.width, + view.height + cornerRadiusPx, + cornerRadiusPx.toFloat() ) } } + } + } - lifecycleScope.launch { - viewModel.guidelineBounds.collect { bounds -> - val bottomInset = - windowManager.maximumWindowMetrics.windowInsets - .getInsets(WindowInsets.Type.navigationBars()) - .bottom - mediumConstraintSet.setGuidelineEnd(R.id.bottomGuideline, bottomInset) - - if (bounds.left >= 0) { - mediumConstraintSet.setGuidelineBegin(leftGuideline.id, bounds.left) - smallConstraintSet.setGuidelineBegin(leftGuideline.id, bounds.left) - } else if (bounds.left < 0) { - mediumConstraintSet.setGuidelineEnd( - leftGuideline.id, - abs(bounds.left) + // ConstraintSets for animating between prompt sizes + val mediumConstraintSet = ConstraintSet() + mediumConstraintSet.clone(view as ConstraintLayout) + + val smallConstraintSet = ConstraintSet() + smallConstraintSet.clone(mediumConstraintSet) + + val largeConstraintSet = ConstraintSet() + largeConstraintSet.clone(mediumConstraintSet) + largeConstraintSet.constrainMaxWidth(R.id.panel, 0) + largeConstraintSet.setGuidelineBegin(R.id.leftGuideline, 0) + largeConstraintSet.setGuidelineEnd(R.id.rightGuideline, 0) + + // TODO: Investigate better way to handle 180 rotations + val flipConstraintSet = ConstraintSet() + + view.doOnLayout { + fun setVisibilities(hideSensorIcon: Boolean, size: PromptSize) { + viewsToHideWhenSmall.forEach { it.showContentOrHide(forceHide = size.isSmall) } + largeConstraintSet.setVisibility(iconHolderView.id, View.GONE) + largeConstraintSet.setVisibility(R.id.biometric_icon_overlay, View.GONE) + largeConstraintSet.setVisibility(R.id.indicator, View.GONE) + largeConstraintSet.setVisibility(R.id.scrollView, View.GONE) + + if (hideSensorIcon) { + smallConstraintSet.setVisibility(iconHolderView.id, View.GONE) + smallConstraintSet.setVisibility(R.id.biometric_icon_overlay, View.GONE) + smallConstraintSet.setVisibility(R.id.indicator, View.GONE) + mediumConstraintSet.setVisibility(iconHolderView.id, View.GONE) + mediumConstraintSet.setVisibility(R.id.biometric_icon_overlay, View.GONE) + mediumConstraintSet.setVisibility(R.id.indicator, View.GONE) + } + } + + view.repeatWhenAttached { + lifecycleScope.launch { + viewModel.iconPosition.collect { position -> + if (position != Rect()) { + val iconParams = + iconHolderView.layoutParams as ConstraintLayout.LayoutParams + + if (position.left != 0) { + iconParams.endToEnd = ConstraintSet.UNSET + iconParams.leftMargin = position.left + mediumConstraintSet.clear(R.id.biometric_icon, ConstraintSet.RIGHT) + mediumConstraintSet.connect( + R.id.biometric_icon, + ConstraintSet.LEFT, + ConstraintSet.PARENT_ID, + ConstraintSet.LEFT ) - smallConstraintSet.setGuidelineEnd( - leftGuideline.id, - abs(bounds.left) + mediumConstraintSet.setMargin( + R.id.biometric_icon, + ConstraintSet.LEFT, + position.left ) - } - - if (bounds.right >= 0) { - mediumConstraintSet.setGuidelineEnd(rightGuideline.id, bounds.right) - smallConstraintSet.setGuidelineEnd(rightGuideline.id, bounds.right) - } else if (bounds.right < 0) { - mediumConstraintSet.setGuidelineBegin( - rightGuideline.id, - abs(bounds.right) + smallConstraintSet.clear(R.id.biometric_icon, ConstraintSet.RIGHT) + smallConstraintSet.connect( + R.id.biometric_icon, + ConstraintSet.LEFT, + ConstraintSet.PARENT_ID, + ConstraintSet.LEFT ) - smallConstraintSet.setGuidelineBegin( - rightGuideline.id, - abs(bounds.right) + smallConstraintSet.setMargin( + R.id.biometric_icon, + ConstraintSet.LEFT, + position.left ) } - - if (bounds.top >= 0) { - mediumConstraintSet.setGuidelineBegin(topGuideline.id, bounds.top) - smallConstraintSet.setGuidelineBegin(topGuideline.id, bounds.top) - } else if (bounds.top < 0) { - mediumConstraintSet.setGuidelineEnd( - topGuideline.id, - abs(bounds.top) + if (position.top != 0) { + iconParams.bottomToBottom = ConstraintSet.UNSET + iconParams.topMargin = position.top + mediumConstraintSet.clear(R.id.biometric_icon, ConstraintSet.BOTTOM) + mediumConstraintSet.setMargin( + R.id.biometric_icon, + ConstraintSet.TOP, + position.top ) - smallConstraintSet.setGuidelineEnd(topGuideline.id, abs(bounds.top)) - } - - if (midGuideline != null) { - val left = - if (bounds.left >= 0) { - abs(bounds.left) - } else { - view.width - abs(bounds.left) - } - val right = - if (bounds.right >= 0) { - view.width - abs(bounds.right) - } else { - abs(bounds.right) - } - val mid = (left + right) / 2 - mediumConstraintSet.setGuidelineBegin(midGuideline.id, mid) - } - } - } - - lifecycleScope.launch { - combine(viewModel.hideSensorIcon, viewModel.size, ::Pair).collect { - (hideSensorIcon, size) -> - setVisibilities(hideSensorIcon, size) - } - } - - lifecycleScope.launch { - combine(viewModel.position, viewModel.size, ::Pair).collect { - (position, size) -> - if (position.isLeft) { - if (size.isSmall) { - flipConstraintSet.clone(smallConstraintSet) - } else { - flipConstraintSet.clone(mediumConstraintSet) - } - - // Move all content to other panel - flipConstraintSet.connect( - R.id.scrollView, - ConstraintSet.LEFT, - R.id.midGuideline, - ConstraintSet.LEFT + smallConstraintSet.clear(R.id.biometric_icon, ConstraintSet.BOTTOM) + smallConstraintSet.setMargin( + R.id.biometric_icon, + ConstraintSet.TOP, + position.top ) - flipConstraintSet.connect( - R.id.scrollView, + } + if (position.right != 0) { + iconParams.startToStart = ConstraintSet.UNSET + iconParams.rightMargin = position.right + mediumConstraintSet.clear(R.id.biometric_icon, ConstraintSet.LEFT) + mediumConstraintSet.connect( + R.id.biometric_icon, ConstraintSet.RIGHT, - R.id.rightGuideline, + ConstraintSet.PARENT_ID, ConstraintSet.RIGHT ) - } else if (position.isTop) { - // Top position is only used for 180 rotation Udfps - // Requires repositioning due to sensor location at top of screen - mediumConstraintSet.connect( - R.id.scrollView, - ConstraintSet.TOP, - R.id.indicator, - ConstraintSet.BOTTOM + mediumConstraintSet.setMargin( + R.id.biometric_icon, + ConstraintSet.RIGHT, + position.right ) - mediumConstraintSet.connect( - R.id.scrollView, - ConstraintSet.BOTTOM, - R.id.button_bar, - ConstraintSet.TOP + smallConstraintSet.clear(R.id.biometric_icon, ConstraintSet.LEFT) + smallConstraintSet.connect( + R.id.biometric_icon, + ConstraintSet.RIGHT, + ConstraintSet.PARENT_ID, + ConstraintSet.RIGHT ) - mediumConstraintSet.connect( - R.id.panel, - ConstraintSet.TOP, + smallConstraintSet.setMargin( R.id.biometric_icon, - ConstraintSet.TOP + ConstraintSet.RIGHT, + position.right ) + } + if (position.bottom != 0) { + iconParams.topToTop = ConstraintSet.UNSET + iconParams.bottomMargin = position.bottom + mediumConstraintSet.clear(R.id.biometric_icon, ConstraintSet.TOP) mediumConstraintSet.setMargin( - R.id.panel, - ConstraintSet.TOP, - (-24 * pxToDp).toInt() + R.id.biometric_icon, + ConstraintSet.BOTTOM, + position.bottom + ) + smallConstraintSet.clear(R.id.biometric_icon, ConstraintSet.TOP) + smallConstraintSet.setMargin( + R.id.biometric_icon, + ConstraintSet.BOTTOM, + position.bottom ) - mediumConstraintSet.setVerticalBias(R.id.scrollView, 0f) } + iconHolderView.layoutParams = iconParams + } + } + } - when { - size.isSmall -> { - if (position.isLeft) { - flipConstraintSet.applyTo(view) - } else { - smallConstraintSet.applyTo(view) - } - } - size.isMedium && currentSize.isSmall -> { - val autoTransition = AutoTransition() - autoTransition.setDuration( - ANIMATE_SMALL_TO_MEDIUM_DURATION_MS.toLong() - ) - - TransitionManager.beginDelayedTransition(view, autoTransition) - - if (position.isLeft) { - flipConstraintSet.applyTo(view) - } else { - mediumConstraintSet.applyTo(view) - } - } - size.isMedium -> { - if (position.isLeft) { - flipConstraintSet.applyTo(view) - } else { - mediumConstraintSet.applyTo(view) - } - } - size.isLarge && currentSize.isMedium -> { - val autoTransition = AutoTransition() - autoTransition.setDuration( - ANIMATE_MEDIUM_TO_LARGE_DURATION_MS.toLong() - ) - - TransitionManager.beginDelayedTransition(view, autoTransition) - largeConstraintSet.applyTo(view) - } - } + lifecycleScope.launch { + viewModel.iconSize.collect { iconSize -> + iconHolderView.layoutParams.width = iconSize.first + iconHolderView.layoutParams.height = iconSize.second + mediumConstraintSet.constrainWidth(R.id.biometric_icon, iconSize.first) + mediumConstraintSet.constrainHeight(R.id.biometric_icon, iconSize.second) + } + } + + lifecycleScope.launch { + viewModel.guidelineBounds.collect { bounds -> + val bottomInset = + windowManager.maximumWindowMetrics.windowInsets + .getInsets(WindowInsets.Type.navigationBars()) + .bottom + mediumConstraintSet.setGuidelineEnd(R.id.bottomGuideline, bottomInset) + + if (bounds.left >= 0) { + mediumConstraintSet.setGuidelineBegin(leftGuideline.id, bounds.left) + smallConstraintSet.setGuidelineBegin(leftGuideline.id, bounds.left) + } else if (bounds.left < 0) { + mediumConstraintSet.setGuidelineEnd(leftGuideline.id, abs(bounds.left)) + smallConstraintSet.setGuidelineEnd(leftGuideline.id, abs(bounds.left)) + } + + if (bounds.right >= 0) { + mediumConstraintSet.setGuidelineEnd(rightGuideline.id, bounds.right) + smallConstraintSet.setGuidelineEnd(rightGuideline.id, bounds.right) + } else if (bounds.right < 0) { + mediumConstraintSet.setGuidelineBegin( + rightGuideline.id, + abs(bounds.right) + ) + smallConstraintSet.setGuidelineBegin( + rightGuideline.id, + abs(bounds.right) + ) + } - currentSize = size - currentPosition = position - notifyAccessibilityChanged() + if (bounds.top >= 0) { + mediumConstraintSet.setGuidelineBegin(topGuideline.id, bounds.top) + smallConstraintSet.setGuidelineBegin(topGuideline.id, bounds.top) + } else if (bounds.top < 0) { + mediumConstraintSet.setGuidelineEnd(topGuideline.id, abs(bounds.top)) + smallConstraintSet.setGuidelineEnd(topGuideline.id, abs(bounds.top)) + } - panelView.invalidateOutline() - view.invalidate() - view.requestLayout() + if (midGuideline != null) { + val left = + if (bounds.left >= 0) { + abs(bounds.left) + } else { + view.width - abs(bounds.left) + } + val right = + if (bounds.right >= 0) { + view.width - abs(bounds.right) + } else { + abs(bounds.right) + } + val mid = (left + right) / 2 + mediumConstraintSet.setGuidelineBegin(midGuideline.id, mid) } } } - } - } else if (panelViewController != null) { - val iconHolderView = view.requireViewById<View>(R.id.biometric_icon_frame) - val iconPadding = view.resources.getDimension(R.dimen.biometric_dialog_icon_padding) - val fullSizeYOffset = - view.resources.getDimension( - R.dimen.biometric_dialog_medium_to_large_translation_offset - ) - - // cache the original position of the icon view (as done in legacy view) - // this must happen before any size changes can be made - view.doOnLayout { - // TODO(b/251476085): this old way of positioning has proven itself unreliable - // remove this and associated thing like (UdfpsDialogMeasureAdapter) and - // pin to the physical sensor - val iconHolderOriginalY = iconHolderView.y - - // bind to prompt - // TODO(b/251476085): migrate the legacy panel controller and simplify this - view.repeatWhenAttached { - var currentSize: PromptSize? = null - lifecycleScope.launch { - /** - * View is only set visible in BiometricViewSizeBinder once PromptSize is - * determined that accounts for iconView size, to prevent prompt resizing - * being visible to the user. - * - * TODO(b/288175072): May be able to remove isIconViewLoaded once constraint - * layout is implemented - */ - combine(viewModel.isIconViewLoaded, viewModel.size, ::Pair).collect { - (isIconViewLoaded, size) -> - if (!isIconViewLoaded) { - return@collect - } - // prepare for animated size transitions - for (v in viewsToHideWhenSmall) { - v.showContentOrHide(forceHide = size.isSmall) - } + lifecycleScope.launch { + combine(viewModel.hideSensorIcon, viewModel.size, ::Pair).collect { + (hideSensorIcon, size) -> + setVisibilities(hideSensorIcon, size) + } + } - if (viewModel.hideSensorIcon.first()) { - iconHolderView.visibility = View.GONE + lifecycleScope.launch { + combine(viewModel.position, viewModel.size, ::Pair).collect { (position, size) + -> + if (position.isLeft) { + if (size.isSmall) { + flipConstraintSet.clone(smallConstraintSet) + } else { + flipConstraintSet.clone(mediumConstraintSet) } - if (currentSize == null && size.isSmall) { - iconHolderView.alpha = 0f - } - if ((currentSize.isSmall && size.isMedium) || size.isSmall) { - viewsToFadeInOnSizeChange.forEach { it.alpha = 0f } + // Move all content to other panel + flipConstraintSet.connect( + R.id.scrollView, + ConstraintSet.LEFT, + R.id.midGuideline, + ConstraintSet.LEFT + ) + flipConstraintSet.connect( + R.id.scrollView, + ConstraintSet.RIGHT, + R.id.rightGuideline, + ConstraintSet.RIGHT + ) + } else if (position.isTop) { + // Top position is only used for 180 rotation Udfps + // Requires repositioning due to sensor location at top of screen + mediumConstraintSet.connect( + R.id.scrollView, + ConstraintSet.TOP, + R.id.indicator, + ConstraintSet.BOTTOM + ) + mediumConstraintSet.connect( + R.id.scrollView, + ConstraintSet.BOTTOM, + R.id.button_bar, + ConstraintSet.TOP + ) + mediumConstraintSet.connect( + R.id.panel, + ConstraintSet.TOP, + R.id.biometric_icon, + ConstraintSet.TOP + ) + mediumConstraintSet.setMargin( + R.id.panel, + ConstraintSet.TOP, + (-24 * pxToDp).toInt() + ) + mediumConstraintSet.setVerticalBias(R.id.scrollView, 0f) + } + + when { + size.isSmall -> { + if (position.isLeft) { + flipConstraintSet.applyTo(view) + } else { + smallConstraintSet.applyTo(view) + } } + size.isMedium && currentSize.isSmall -> { + val autoTransition = AutoTransition() + autoTransition.setDuration( + ANIMATE_SMALL_TO_MEDIUM_DURATION_MS.toLong() + ) - // propagate size changes to legacy panel controller and animate - // transitions - view.doOnLayout { - val width = view.measuredWidth - val height = view.measuredHeight - - when { - size.isSmall -> { - iconHolderView.alpha = 1f - val bottomInset = - windowManager.maximumWindowMetrics.windowInsets - .getInsets(WindowInsets.Type.navigationBars()) - .bottom - iconHolderView.y = - if (view.isLandscape()) { - (view.height - - iconHolderView.height - - bottomInset) / 2f - } else { - view.height - - iconHolderView.height - - iconPadding - - bottomInset - } - val newHeight = - iconHolderView.height + (2 * iconPadding.toInt()) - - iconHolderView.paddingTop - - iconHolderView.paddingBottom - panelViewController.updateForContentDimensions( - width, - newHeight + bottomInset, - 0, /* animateDurationMs */ - ) - } - size.isMedium && currentSize.isSmall -> { - val duration = ANIMATE_SMALL_TO_MEDIUM_DURATION_MS - panelViewController.updateForContentDimensions( - width, - height, - duration, - ) - startMonitoredAnimation( - listOf( - iconHolderView.asVerticalAnimator( - duration = duration.toLong(), - toY = - iconHolderOriginalY - - viewsToHideWhenSmall - .filter { it.isGone } - .sumOf { it.height }, - ), - viewsToFadeInOnSizeChange.asFadeInAnimator( - duration = duration.toLong(), - delay = duration.toLong(), - ), - ) - ) - } - size.isMedium && currentSize.isNullOrNotSmall -> { - panelViewController.updateForContentDimensions( - width, - height, - 0, /* animateDurationMs */ - ) - } - size.isLarge -> { - val duration = ANIMATE_MEDIUM_TO_LARGE_DURATION_MS - panelViewController.setUseFullScreen(true) - panelViewController.updateForContentDimensions( - panelViewController.containerWidth, - panelViewController.containerHeight, - duration, - ) - - startMonitoredAnimation( - listOf( - view.asVerticalAnimator( - duration.toLong() * 2 / 3, - toY = view.y - fullSizeYOffset - ), - listOf(view) - .asFadeInAnimator( - duration = duration.toLong() / 2, - delay = duration.toLong(), - ), - ) - ) - // TODO(b/251476085): clean up (copied from legacy) - if (view.isAttachedToWindow) { - val parent = view.parent as? ViewGroup - parent?.removeView(view) - } - } + TransitionManager.beginDelayedTransition(view, autoTransition) + + if (position.isLeft) { + flipConstraintSet.applyTo(view) + } else { + mediumConstraintSet.applyTo(view) + } + } + size.isMedium -> { + if (position.isLeft) { + flipConstraintSet.applyTo(view) + } else { + mediumConstraintSet.applyTo(view) } + } + size.isLarge && currentSize.isMedium -> { + val autoTransition = AutoTransition() + autoTransition.setDuration( + ANIMATE_MEDIUM_TO_LARGE_DURATION_MS.toLong() + ) - currentSize = size - view.visibility = View.VISIBLE - viewModel.setIsIconViewLoaded(false) - notifyAccessibilityChanged() + TransitionManager.beginDelayedTransition(view, autoTransition) + largeConstraintSet.applyTo(view) } } + + currentSize = size + currentPosition = position + notifyAccessibilityChanged() + + panelView.invalidateOutline() + view.invalidate() + view.requestLayout() } } } @@ -646,17 +453,6 @@ object BiometricViewSizeBinder { } } -private fun View.isLandscape(): Boolean { - val r = context.display.rotation - return if ( - context.resources.getBoolean(com.android.internal.R.bool.config_reverseDefaultRotation) - ) { - r == Surface.ROTATION_0 || r == Surface.ROTATION_180 - } else { - r == Surface.ROTATION_90 || r == Surface.ROTATION_270 - } -} - private fun View.showContentOrHide(forceHide: Boolean = false) { val isTextViewWithBlankText = this is TextView && this.text.isBlank() val isImageViewWithoutImage = this is ImageView && this.drawable == null @@ -667,26 +463,3 @@ private fun View.showContentOrHide(forceHide: Boolean = false) { View.VISIBLE } } - -private fun View.asVerticalAnimator( - duration: Long, - toY: Float, - fromY: Float = this.y -): ValueAnimator { - val animator = ValueAnimator.ofFloat(fromY, toY) - animator.duration = duration - animator.addUpdateListener { y = it.animatedValue as Float } - return animator -} - -private fun List<View>.asFadeInAnimator(duration: Long, delay: Long): ValueAnimator { - forEach { it.alpha = 0f } - val animator = ValueAnimator.ofFloat(0f, 1f) - animator.duration = duration - animator.startDelay = delay - animator.addUpdateListener { - val alpha = it.animatedValue as Float - forEach { view -> view.alpha = alpha } - } - return animator -} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt index 18e2a56e5e78..49f4b05e2cd9 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt @@ -10,7 +10,6 @@ import android.widget.TextView import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.app.animation.Interpolators -import com.android.systemui.Flags.constraintBp import com.android.systemui.biometrics.AuthPanelController import com.android.systemui.biometrics.ui.CredentialPasswordView import com.android.systemui.biometrics.ui.CredentialPatternView @@ -82,7 +81,7 @@ object CredentialViewBinder { subtitleView.textOrHide = header.subtitle descriptionView.textOrHide = header.description - if (Flags.customBiometricPrompt() && constraintBp()) { + if (Flags.customBiometricPrompt()) { BiometricCustomizedViewBinder.bind( customizedViewContainer, header.contentView, diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt index 9e4aaaa44085..eab3b26e9b68 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt @@ -22,9 +22,7 @@ import android.util.Log import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.airbnb.lottie.LottieAnimationView -import com.airbnb.lottie.LottieOnCompositionLoadedListener import com.android.settingslib.widget.LottieColorUtils -import com.android.systemui.Flags.constraintBp import com.android.systemui.biometrics.ui.viewmodel.PromptIconViewModel import com.android.systemui.biometrics.ui.viewmodel.PromptIconViewModel.AuthType import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel @@ -44,55 +42,12 @@ object PromptIconViewBinder { @JvmStatic fun bind( iconView: LottieAnimationView, - iconViewLayoutParamSizeOverride: Pair<Int, Int>?, promptViewModel: PromptViewModel ) { val viewModel = promptViewModel.iconViewModel iconView.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.onConfigurationChanged(iconView.context.resources.configuration) - if (iconViewLayoutParamSizeOverride != null) { - iconView.layoutParams.width = iconViewLayoutParamSizeOverride.first - iconView.layoutParams.height = iconViewLayoutParamSizeOverride.second - } - - if (!constraintBp()) { - launch { - var lottieOnCompositionLoadedListener: LottieOnCompositionLoadedListener? = - null - - viewModel.iconSize.collect { iconSize -> - /** - * When we bind the BiometricPrompt View and ViewModel in - * [BiometricViewBinder], the view is set invisible and - * [isIconViewLoaded] is set to false. We configure the iconView with a - * LottieOnCompositionLoadedListener that sets [isIconViewLoaded] to - * true, in order to wait for the iconView to load before determining - * the prompt size, and prevent any prompt resizing from being visible - * to the user. - * - * TODO(b/288175072): May be able to remove this once constraint layout - * is unflagged - */ - if (lottieOnCompositionLoadedListener != null) { - iconView.removeLottieOnCompositionLoadedListener( - lottieOnCompositionLoadedListener!! - ) - } - lottieOnCompositionLoadedListener = LottieOnCompositionLoadedListener { - promptViewModel.setIsIconViewLoaded(true) - } - iconView.addLottieOnCompositionLoadedListener( - lottieOnCompositionLoadedListener!! - ) - - if (iconViewLayoutParamSizeOverride == null) { - iconView.layoutParams.width = iconSize.first - iconView.layoutParams.height = iconSize.second - } - } - } - } launch { viewModel.iconAsset @@ -154,7 +109,7 @@ fun LottieAnimationView.updateAsset( setAnimation(asset) if (animatingFromSfpsAuthenticating(asset)) { // Skipping to error / success / unlock segment of animation - setMinFrame(151) + setMinFrame(158) } else { frame = 0 } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt index 31af126eb3f0..761c3da77a4d 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt @@ -6,7 +6,6 @@ import android.hardware.biometrics.Flags.customBiometricPrompt import android.hardware.biometrics.PromptContentView import android.text.InputType import com.android.internal.widget.LockPatternView -import com.android.systemui.Flags.constraintBp import com.android.systemui.biometrics.Utils import com.android.systemui.biometrics.domain.interactor.CredentialStatus import com.android.systemui.biometrics.domain.interactor.PromptCredentialInteractor @@ -39,7 +38,7 @@ constructor( credentialInteractor.prompt.filterIsInstance<BiometricPromptRequest.Credential>(), credentialInteractor.showTitleOnly ) { request, showTitleOnly -> - val flagEnabled = customBiometricPrompt() && constraintBp() + val flagEnabled = customBiometricPrompt() val showTitleOnlyForCredential = showTitleOnly && flagEnabled BiometricPromptHeaderViewModelImpl( request, @@ -82,8 +81,8 @@ constructor( val errorMessage: Flow<String> = combine(credentialInteractor.verificationError, credentialInteractor.prompt) { error, p -> when (error) { - is CredentialStatus.Fail.Error -> error.error - ?: applicationContext.asBadCredentialErrorMessage(p) + is CredentialStatus.Fail.Error -> + error.error ?: applicationContext.asBadCredentialErrorMessage(p) is CredentialStatus.Fail.Throttled -> error.error null -> "" } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt index 214420d45560..25d43d972fe2 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt @@ -36,7 +36,6 @@ import android.util.RotationUtils import android.view.HapticFeedbackConstants import android.view.MotionEvent import com.android.launcher3.icons.IconProvider -import com.android.systemui.Flags.constraintBp import com.android.systemui.biometrics.UdfpsUtils import com.android.systemui.biometrics.Utils import com.android.systemui.biometrics.Utils.isSystem @@ -470,7 +469,7 @@ constructor( promptSelectorInteractor.prompt .map { when { - !(customBiometricPrompt() && constraintBp()) || it == null -> Pair(null, "") + !(customBiometricPrompt()) || it == null -> Pair(null, "") else -> context.getUserBadgedLogoInfo(it, iconProvider, activityTaskManager) } } @@ -487,7 +486,7 @@ constructor( /** Custom content view for the prompt. */ val contentView: Flow<PromptContentView?> = promptSelectorInteractor.prompt - .map { if (customBiometricPrompt() && constraintBp()) it?.contentView else null } + .map { if (customBiometricPrompt()) it?.contentView else null } .distinctUntilChanged() private val originalDescription = @@ -1045,7 +1044,7 @@ private fun BiometricPromptRequest.Biometric.getApplicationInfo( val packageName = when { componentNameForLogo != null -> componentNameForLogo.packageName - // TODO(b/339532378): We should check whether |allowBackgroundAuthentication| should be + // TODO(b/353597496): We should check whether |allowBackgroundAuthentication| should be // removed. // This is being consistent with the check in [AuthController.showDialog()]. allowBackgroundAuthentication || isSystem(context, opPackageName) -> opPackageName diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt index 19ea007fc60f..c2a4ee36dec6 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt @@ -28,7 +28,6 @@ import android.view.WindowManager import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY import com.airbnb.lottie.model.KeyPath -import com.android.systemui.Flags.constraintBp import com.android.systemui.biometrics.Utils import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor import com.android.systemui.biometrics.domain.interactor.SideFpsSensorInteractor @@ -152,21 +151,6 @@ constructor( -> val topLeft = Point(sensorLocation.left, sensorLocation.top) - if (!constraintBp()) { - if (sensorLocation.isSensorVerticalInDefaultOrientation) { - if (displayRotation == DisplayRotation.ROTATION_0) { - topLeft.x -= bounds!!.width() - } else if (displayRotation == DisplayRotation.ROTATION_270) { - topLeft.y -= bounds!!.height() - } - } else { - if (displayRotation == DisplayRotation.ROTATION_180) { - topLeft.y -= bounds!!.height() - } else if (displayRotation == DisplayRotation.ROTATION_270) { - topLeft.x -= bounds!!.width() - } - } - } defaultOverlayViewParams.apply { x = topLeft.x y = topLeft.y 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/communal/widgets/EditWidgetsActivity.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt index 03ef17b6ec5b..2bcbc9aa74ac 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt @@ -16,7 +16,10 @@ package com.android.systemui.communal.widgets +import android.app.Activity +import android.app.Application.ActivityLifecycleCallbacks import android.content.Intent +import android.content.IntentSender import android.os.Bundle import android.os.RemoteException import android.util.Log @@ -66,12 +69,78 @@ constructor( const val EXTRA_OPEN_WIDGET_PICKER_ON_START = "open_widget_picker_on_start" } + /** + * [ActivityController] handles closing the activity in the case it is backgrounded without + * waiting for an activity result + */ + class ActivityController(activity: Activity) { + companion object { + private const val STATE_EXTRA_IS_WAITING_FOR_RESULT = "extra_is_waiting_for_result" + } + + private var waitingForResult: Boolean = false + + init { + activity.registerActivityLifecycleCallbacks( + object : ActivityLifecycleCallbacks { + override fun onActivityCreated( + activity: Activity, + savedInstanceState: Bundle? + ) { + waitingForResult = + savedInstanceState?.getBoolean(STATE_EXTRA_IS_WAITING_FOR_RESULT) + ?: false + } + + override fun onActivityStarted(activity: Activity) { + // Nothing to implement. + } + + override fun onActivityResumed(activity: Activity) { + // Nothing to implement. + } + + override fun onActivityPaused(activity: Activity) { + // Nothing to implement. + } + + override fun onActivityStopped(activity: Activity) { + // If we're not backgrounded due to waiting for a resul (either widget + // selection + // or configuration), finish activity. + if (!waitingForResult) { + activity.finish() + } + } + + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) { + outState.putBoolean(STATE_EXTRA_IS_WAITING_FOR_RESULT, waitingForResult) + } + + override fun onActivityDestroyed(activity: Activity) { + // Nothing to implement. + } + } + ) + } + + /** + * Invoked when waiting for an activity result changes, either initiating such wait or + * finishing due to the return of a result. + */ + fun onWaitingForResult(waitingForResult: Boolean) { + this.waitingForResult = waitingForResult + } + } + private val logger = Logger(logBuffer, "EditWidgetsActivity") private val widgetConfigurator by lazy { widgetConfiguratorFactory.create(this) } private var shouldOpenWidgetPickerOnStart = false + private val activityController: ActivityController = ActivityController(this) + private val addWidgetActivityLauncher: ActivityResultLauncher<Intent> = registerForActivityResult(StartActivityForResult()) { result -> when (result.resultCode) { @@ -154,6 +223,13 @@ constructor( // edit mode communalViewModel.currentScene.first { it == CommunalScenes.Blank } communalViewModel.setEditModeState(EditModeState.SHOWING) + + // Show the widget picker, if necessary, after the edit activity has animated in. + // Waiting until after the activity has appeared avoids transitions issues. + if (shouldOpenWidgetPickerOnStart) { + onOpenWidgetPicker() + shouldOpenWidgetPickerOnStart = false + } } } } @@ -186,7 +262,34 @@ constructor( } } + override fun startActivityForResult(intent: Intent, requestCode: Int, options: Bundle?) { + activityController.onWaitingForResult(true) + super.startActivityForResult(intent, requestCode, options) + } + + override fun startIntentSenderForResult( + intent: IntentSender, + requestCode: Int, + fillInIntent: Intent?, + flagsMask: Int, + flagsValues: Int, + extraFlags: Int, + options: Bundle? + ) { + activityController.onWaitingForResult(true) + super.startIntentSenderForResult( + intent, + requestCode, + fillInIntent, + flagsMask, + flagsValues, + extraFlags, + options + ) + } + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + activityController.onWaitingForResult(false) super.onActivityResult(requestCode, resultCode, data) if (requestCode == WidgetConfigurationController.REQUEST_CODE) { widgetConfigurator.setConfigurationResult(resultCode) @@ -198,11 +301,6 @@ constructor( communalViewModel.setEditActivityShowing(true) - if (shouldOpenWidgetPickerOnStart) { - onOpenWidgetPicker() - shouldOpenWidgetPickerOnStart = false - } - logger.i("Starting the communal widget editor activity") uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_EDIT_MODE_SHOWN) } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java index 32731117a8f6..79f4568d73be 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java @@ -33,6 +33,7 @@ import com.android.systemui.display.ui.viewmodel.ConnectingDisplayViewModel; import com.android.systemui.dock.DockManager; import com.android.systemui.dock.DockManagerImpl; import com.android.systemui.doze.DozeHost; +import com.android.systemui.inputdevice.tutorial.KeyboardTouchpadTutorialModule; import com.android.systemui.keyboard.shortcut.ShortcutHelperModule; import com.android.systemui.keyguard.ui.composable.blueprint.DefaultBlueprintModule; import com.android.systemui.keyguard.ui.view.layout.blueprints.KeyguardBlueprintModule; @@ -77,7 +78,7 @@ import com.android.systemui.statusbar.policy.IndividualSensorPrivacyControllerIm import com.android.systemui.statusbar.policy.SensorPrivacyController; import com.android.systemui.statusbar.policy.SensorPrivacyControllerImpl; import com.android.systemui.toast.ToastModule; -import com.android.systemui.touchpad.tutorial.TouchpadKeyboardTutorialModule; +import com.android.systemui.touchpad.tutorial.TouchpadTutorialModule; import com.android.systemui.unfold.SysUIUnfoldStartableModule; import com.android.systemui.unfold.UnfoldTransitionModule; import com.android.systemui.util.kotlin.SysUICoroutinesModule; @@ -122,6 +123,7 @@ import javax.inject.Named; KeyboardShortcutsModule.class, KeyguardBlueprintModule.class, KeyguardSectionsModule.class, + KeyboardTouchpadTutorialModule.class, MediaModule.class, MediaMuteAwaitConnectionCli.StartableModule.class, MultiUserUtilsModule.class, @@ -142,7 +144,7 @@ import javax.inject.Named; SysUIUnfoldStartableModule.class, UnfoldTransitionModule.Startables.class, ToastModule.class, - TouchpadKeyboardTutorialModule.class, + TouchpadTutorialModule.class, VolumeModule.class, WallpaperModule.class, ShortcutHelperModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt index b0f2c18db565..cbea87676d3a 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt @@ -258,13 +258,6 @@ abstract class SystemUICoreStartableModule { @Binds @IntoMap - @ClassKey(KeyboardTouchpadTutorialCoreStartable::class) - abstract fun bindKeyboardTouchpadTutorialCoreStartable( - listener: KeyboardTouchpadTutorialCoreStartable - ): CoreStartable - - @Binds - @IntoMap @ClassKey(PhysicalKeyboardCoreStartable::class) abstract fun bindKeyboardCoreStartable(listener: PhysicalKeyboardCoreStartable): CoreStartable 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/dagger/ContextualEducationModule.kt b/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt index 532b123663ad..7e2c9f89fa67 100644 --- a/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt +++ b/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt @@ -18,14 +18,15 @@ package com.android.systemui.education.dagger import com.android.systemui.CoreStartable import com.android.systemui.Flags -import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.contextualeducation.GestureType +import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.education.data.repository.ContextualEducationRepository -import com.android.systemui.education.data.repository.ContextualEducationRepositoryImpl +import com.android.systemui.education.data.repository.UserContextualEducationRepository import com.android.systemui.education.domain.interactor.ContextualEducationInteractor import com.android.systemui.education.domain.interactor.KeyboardTouchpadEduInteractor import com.android.systemui.education.domain.interactor.KeyboardTouchpadEduStatsInteractor import com.android.systemui.education.domain.interactor.KeyboardTouchpadEduStatsInteractorImpl +import com.android.systemui.education.ui.view.ContextualEduUiCoordinator import dagger.Binds import dagger.Lazy import dagger.Module @@ -42,7 +43,7 @@ import kotlinx.coroutines.SupervisorJob interface ContextualEducationModule { @Binds fun bindContextualEducationRepository( - impl: ContextualEducationRepositoryImpl + impl: UserContextualEducationRepository ): ContextualEducationRepository @Qualifier annotation class EduDataStoreScope @@ -74,7 +75,7 @@ interface ContextualEducationModule { implLazy.get() } else { // No-op implementation when the flag is disabled. - return NoOpContextualEducationInteractor + return NoOpCoreStartable } } @@ -91,6 +92,8 @@ interface ContextualEducationModule { } @Provides + @IntoMap + @ClassKey(KeyboardTouchpadEduInteractor::class) fun provideKeyboardTouchpadEduInteractor( implLazy: Lazy<KeyboardTouchpadEduInteractor> ): CoreStartable { @@ -98,22 +101,32 @@ interface ContextualEducationModule { implLazy.get() } else { // No-op implementation when the flag is disabled. - return NoOpKeyboardTouchpadEduInteractor + return NoOpCoreStartable } } - } - - private object NoOpKeyboardTouchpadEduStatsInteractor : KeyboardTouchpadEduStatsInteractor { - override fun incrementSignalCount(gestureType: GestureType) {} - override fun updateShortcutTriggerTime(gestureType: GestureType) {} + @Provides + @IntoMap + @ClassKey(ContextualEduUiCoordinator::class) + fun provideContextualEduUiCoordinator( + implLazy: Lazy<ContextualEduUiCoordinator> + ): CoreStartable { + return if (Flags.keyboardTouchpadContextualEducation()) { + implLazy.get() + } else { + // No-op implementation when the flag is disabled. + return NoOpCoreStartable + } + } } +} - private object NoOpContextualEducationInteractor : CoreStartable { - override fun start() {} - } +private object NoOpKeyboardTouchpadEduStatsInteractor : KeyboardTouchpadEduStatsInteractor { + override fun incrementSignalCount(gestureType: GestureType) {} - private object NoOpKeyboardTouchpadEduInteractor : CoreStartable { - override fun start() {} - } + override fun updateShortcutTriggerTime(gestureType: GestureType) {} +} + +private object NoOpCoreStartable : CoreStartable { + override fun start() {} } 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/ContextualEducationRepository.kt b/packages/SystemUI/src/com/android/systemui/education/data/repository/ContextualEducationRepository.kt deleted file mode 100644 index 52ccba4b65c7..000000000000 --- a/packages/SystemUI/src/com/android/systemui/education/data/repository/ContextualEducationRepository.kt +++ /dev/null @@ -1,66 +0,0 @@ -/* - * 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.systemui.education.data.repository - -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.contextualeducation.GestureType -import com.android.systemui.education.dagger.ContextualEducationModule.EduClock -import com.android.systemui.education.data.model.GestureEduModel -import java.time.Clock -import javax.inject.Inject -import kotlinx.coroutines.flow.Flow - -/** Encapsulates the functions of ContextualEducationRepository. */ -interface ContextualEducationRepository { - fun setUser(userId: Int) - - fun readGestureEduModelFlow(gestureType: GestureType): Flow<GestureEduModel> - - suspend fun incrementSignalCount(gestureType: GestureType) - - suspend fun updateShortcutTriggerTime(gestureType: GestureType) -} - -/** - * Provide methods to read and update on field level and allow setting datastore when user is - * changed - */ -@SysUISingleton -class ContextualEducationRepositoryImpl -@Inject -constructor( - @EduClock private val clock: Clock, - private val userEduRepository: UserContextualEducationRepository -) : ContextualEducationRepository { - /** To change data store when user is changed */ - override fun setUser(userId: Int) = userEduRepository.setUser(userId) - - override fun readGestureEduModelFlow(gestureType: GestureType) = - userEduRepository.readGestureEduModelFlow(gestureType) - - override suspend fun incrementSignalCount(gestureType: GestureType) { - userEduRepository.updateGestureEduModel(gestureType) { - it.copy(signalCount = it.signalCount + 1) - } - } - - override suspend fun updateShortcutTriggerTime(gestureType: GestureType) { - userEduRepository.updateGestureEduModel(gestureType) { - it.copy(lastShortcutTriggeredTime = clock.instant()) - } - } -} 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 4b37b29e88a5..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 @@ -25,9 +25,9 @@ import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.longPreferencesKey import androidx.datastore.preferences.preferencesDataStoreFile +import com.android.systemui.contextualeducation.GestureType import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.contextualeducation.GestureType import com.android.systemui.education.dagger.ContextualEducationModule.EduDataStoreScope import com.android.systemui.education.data.model.GestureEduModel import java.time.Instant @@ -43,10 +43,24 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map /** - * A contextual education repository to: - * 1) store education data per user - * 2) provide methods to read and update data on model-level - * 3) provide method to enable changing datastore when user is changed + * Allows to: + * 1) read and update data on model-level + * 2) change data store when user is changed + */ +interface ContextualEducationRepository { + fun setUser(userId: Int) + + fun readGestureEduModelFlow(gestureType: GestureType): Flow<GestureEduModel> + + suspend fun updateGestureEduModel( + gestureType: GestureType, + transform: (GestureEduModel) -> GestureEduModel + ) +} + +/** + * Implementation of [ContextualEducationRepository] that uses [androidx.datastore.preferences.core] + * for storage. Data is stored per user. */ @SysUISingleton class UserContextualEducationRepository @@ -54,11 +68,13 @@ class UserContextualEducationRepository constructor( @Application private val applicationContext: Context, @EduDataStoreScope private val dataStoreScopeProvider: Provider<CoroutineScope> -) { +) : ContextualEducationRepository { companion object { 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" } @@ -70,7 +86,7 @@ constructor( @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) private val prefData: Flow<Preferences> = datastore.filterNotNull().flatMapLatest { it.data } - internal fun setUser(userId: Int) { + override fun setUser(userId: Int) { dataStoreScope?.cancel() val newDsScope = dataStoreScopeProvider.get() datastore.value = @@ -85,7 +101,7 @@ constructor( dataStoreScope = newDsScope } - internal fun readGestureEduModelFlow(gestureType: GestureType): Flow<GestureEduModel> = + override fun readGestureEduModelFlow(gestureType: GestureType): Flow<GestureEduModel> = prefData.map { preferences -> getGestureEduModel(gestureType, preferences) } private fun getGestureEduModel( @@ -97,12 +113,20 @@ constructor( educationShownCount = preferences[getEducationShownCountKey(gestureType)] ?: 0, lastShortcutTriggeredTime = preferences[getLastShortcutTriggeredTimeKey(gestureType)]?.let { - Instant.ofEpochMilli(it) + Instant.ofEpochSecond(it) + }, + usageSessionStartTime = + preferences[getUsageSessionStartTimeKey(gestureType)]?.let { + Instant.ofEpochSecond(it) + }, + lastEducationTime = + preferences[getLastEducationTimeKey(gestureType)]?.let { + Instant.ofEpochSecond(it) }, ) } - internal suspend fun updateGestureEduModel( + override suspend fun updateGestureEduModel( gestureType: GestureType, transform: (GestureEduModel) -> GestureEduModel ) { @@ -111,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) + ) } } @@ -128,13 +162,22 @@ 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> ) { if (instant != null) { - preferences[key] = instant.toEpochMilli() + // Use epochSecond because an instant is defined as a signed long (64bit number) of + // seconds. Using toEpochMilli() on Instant.MIN or Instant.MAX will throw exception + // when converting to a long. So we use second instead of milliseconds for storage. + preferences[key] = instant.epochSecond } else { preferences.remove(key) } 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 bee289d4b63a..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 @@ -17,13 +17,15 @@ package com.android.systemui.education.domain.interactor import com.android.systemui.CoreStartable -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.contextualeducation.GestureType import com.android.systemui.contextualeducation.GestureType.BACK +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.education.dagger.ContextualEducationModule.EduClock import com.android.systemui.education.data.model.GestureEduModel import com.android.systemui.education.data.repository.ContextualEducationRepository import com.android.systemui.user.domain.interactor.SelectedUserInteractor +import java.time.Clock import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope @@ -43,6 +45,7 @@ class ContextualEducationInteractor constructor( @Background private val backgroundScope: CoroutineScope, @Background private val backgroundDispatcher: CoroutineDispatcher, + @EduClock private val clock: Clock, private val selectedUserInteractor: SelectedUserInteractor, private val repository: ContextualEducationRepository, ) : CoreStartable { @@ -64,9 +67,37 @@ constructor( .flowOn(backgroundDispatcher) } - suspend fun incrementSignalCount(gestureType: GestureType) = - repository.incrementSignalCount(gestureType) + suspend fun incrementSignalCount(gestureType: GestureType) { + repository.updateGestureEduModel(gestureType) { + it.copy( + signalCount = it.signalCount + 1, + usageSessionStartTime = + if (it.signalCount == 0) clock.instant() else it.usageSessionStartTime + ) + } + } + + suspend fun updateShortcutTriggerTime(gestureType: GestureType) { + repository.updateGestureEduModel(gestureType) { + 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 updateShortcutTriggerTime(gestureType: GestureType) = - repository.updateShortcutTriggerTime(gestureType) + 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/education/ui/view/ContextualEduUiCoordinator.kt b/packages/SystemUI/src/com/android/systemui/education/ui/view/ContextualEduUiCoordinator.kt new file mode 100644 index 000000000000..b446ea2bcb38 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/education/ui/view/ContextualEduUiCoordinator.kt @@ -0,0 +1,68 @@ +/* + * 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.systemui.education.ui.view + +import android.content.Context +import android.widget.Toast +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.education.shared.model.EducationUiType +import com.android.systemui.education.ui.viewmodel.ContextualEduContentViewModel +import com.android.systemui.education.ui.viewmodel.ContextualEduViewModel +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +/** + * A class to show contextual education on UI based on the edu produced from + * [ContextualEduViewModel] + */ +@SysUISingleton +class ContextualEduUiCoordinator +constructor( + @Application private val applicationScope: CoroutineScope, + private val viewModel: ContextualEduViewModel, + private val createToast: (String) -> Toast +) : CoreStartable { + + @Inject + constructor( + @Application applicationScope: CoroutineScope, + context: Context, + viewModel: ContextualEduViewModel, + ) : this( + applicationScope, + viewModel, + createToast = { message -> Toast.makeText(context, message, Toast.LENGTH_LONG) } + ) + + override fun start() { + applicationScope.launch { + viewModel.eduContent.collect { contentModel -> + if (contentModel.type == EducationUiType.Toast) { + showToast(contentModel) + } + } + } + } + + private fun showToast(model: ContextualEduContentViewModel) { + val toast = createToast(model.message) + toast.show() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/education/ui/viewmodel/ContextualEduContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/education/ui/viewmodel/ContextualEduContentViewModel.kt new file mode 100644 index 000000000000..3cba4c8fb110 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/education/ui/viewmodel/ContextualEduContentViewModel.kt @@ -0,0 +1,21 @@ +/* + * 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.systemui.education.ui.viewmodel + +import com.android.systemui.education.shared.model.EducationUiType + +data class ContextualEduContentViewModel(val message: String, val type: EducationUiType) diff --git a/packages/SystemUI/src/com/android/systemui/education/ui/viewmodel/ContextualEduViewModel.kt b/packages/SystemUI/src/com/android/systemui/education/ui/viewmodel/ContextualEduViewModel.kt new file mode 100644 index 000000000000..58276e0759f6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/education/ui/viewmodel/ContextualEduViewModel.kt @@ -0,0 +1,51 @@ +/* + * 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.systemui.education.ui.viewmodel + +import android.content.res.Resources +import com.android.systemui.contextualeducation.GestureType +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.education.domain.interactor.KeyboardTouchpadEduInteractor +import com.android.systemui.education.shared.model.EducationInfo +import com.android.systemui.res.R +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.map + +@SysUISingleton +class ContextualEduViewModel +@Inject +constructor(@Main private val resources: Resources, interactor: KeyboardTouchpadEduInteractor) { + val eduContent: Flow<ContextualEduContentViewModel> = + interactor.educationTriggered.filterNotNull().map { + ContextualEduContentViewModel(getEduContent(it), it.educationUiType) + } + + private fun getEduContent(educationInfo: EducationInfo): String { + // Todo: also check UiType in educationInfo to determine the string + val resourceId = + when (educationInfo.gestureType) { + GestureType.BACK -> R.string.back_edu_toast_content + GestureType.HOME -> R.string.home_edu_toast_content + GestureType.OVERVIEW -> R.string.overview_edu_toast_content + GestureType.ALL_APPS -> R.string.all_apps_edu_toast_content + } + return resources.getString(resourceId) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt index 1d1ac5ab21a3..cd0b3f9b6693 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt @@ -34,8 +34,6 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.KeyguardBottomAreaRefactor import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.shared.ComposeLockscreen -import com.android.systemui.qs.flags.NewQsUI -import com.android.systemui.qs.flags.QSComposeFragment import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.shared.flag.DualShade import com.android.systemui.statusbar.notification.collection.SortBySectionTimeFlag @@ -60,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 @@ -76,9 +74,6 @@ class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, ha // DualShade dependencies DualShade.token dependsOn SceneContainerFlag.getMainAconfigFlag() - // QS Fragment using Compose dependencies - QSComposeFragment.token dependsOn NewQsUI.token - // Status bar chip dependencies statusBarCallChipNotificationIconToken dependsOn statusBarUseReposForCallChipToken statusBarCallChipNotificationIconToken dependsOn statusBarScreenSharingChipsToken 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/inputdevice/oobe/KeyboardTouchpadTutorialModule.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/KeyboardTouchpadTutorialModule.kt new file mode 100644 index 000000000000..8e6cb077a25e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/KeyboardTouchpadTutorialModule.kt @@ -0,0 +1,50 @@ +/* + * 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.inputdevice.tutorial + +import android.app.Activity +import com.android.systemui.CoreStartable +import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity +import com.android.systemui.touchpad.tutorial.domain.interactor.TouchpadGesturesInteractor +import dagger.Binds +import dagger.BindsOptionalOf +import dagger.Module +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +interface KeyboardTouchpadTutorialModule { + + @Binds + @IntoMap + @ClassKey(KeyboardTouchpadTutorialCoreStartable::class) + fun bindKeyboardTouchpadTutorialCoreStartable( + listener: KeyboardTouchpadTutorialCoreStartable + ): CoreStartable + + @Binds + @IntoMap + @ClassKey(KeyboardTouchpadTutorialActivity::class) + fun activity(impl: KeyboardTouchpadTutorialActivity): Activity + + // TouchpadModule dependencies below + // all should be optional to not introduce touchpad dependency in all sysui variants + + @BindsOptionalOf fun touchpadScreensProvider(): TouchpadTutorialScreensProvider + + @BindsOptionalOf fun touchpadGesturesInteractor(): TouchpadGesturesInteractor +} diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/TouchpadKeyboardTutorialModule.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/TouchpadTutorialScreensProvider.kt index 8ba8db498a36..bd3e771f40bc 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/TouchpadKeyboardTutorialModule.kt +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/TouchpadTutorialScreensProvider.kt @@ -14,20 +14,13 @@ * limitations under the License. */ -package com.android.systemui.touchpad.tutorial +package com.android.systemui.inputdevice.tutorial -import android.app.Activity -import com.android.systemui.touchpad.tutorial.ui.view.TouchpadTutorialActivity -import dagger.Binds -import dagger.Module -import dagger.multibindings.ClassKey -import dagger.multibindings.IntoMap +import androidx.compose.runtime.Composable -@Module -interface TouchpadKeyboardTutorialModule { +interface TouchpadTutorialScreensProvider { - @Binds - @IntoMap - @ClassKey(TouchpadTutorialActivity::class) - fun activity(impl: TouchpadTutorialActivity): Activity + @Composable fun BackGesture(onDoneButtonClicked: () -> Unit, onBack: () -> Unit) + + @Composable fun HomeGesture(onDoneButtonClicked: () -> Unit, onBack: () -> Unit) } diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/ui/composable/ActionKeyTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/ui/composable/ActionKeyTutorialScreen.kt new file mode 100644 index 000000000000..c5b0ca78d65a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/ui/composable/ActionKeyTutorialScreen.kt @@ -0,0 +1,105 @@ +/* + * 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.inputdevice.tutorial.ui.composable + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyEvent +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onKeyEvent +import androidx.compose.ui.input.key.type +import com.airbnb.lottie.compose.rememberLottieDynamicProperties +import com.android.compose.theme.LocalAndroidColorScheme +import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.FINISHED +import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.NOT_STARTED +import com.android.systemui.res.R + +@Composable +fun ActionKeyTutorialScreen( + onDoneButtonClicked: () -> Unit, + onBack: () -> Unit, +) { + BackHandler(onBack = onBack) + val screenConfig = buildScreenConfig() + var actionState by remember { mutableStateOf(NOT_STARTED) } + Box( + modifier = + Modifier.fillMaxSize().onKeyEvent { keyEvent: KeyEvent -> + // temporary before we can access Action/Meta key + if (keyEvent.key == Key.AltLeft && keyEvent.type == KeyEventType.KeyUp) { + actionState = FINISHED + } + true + } + ) { + ActionTutorialContent(actionState, onDoneButtonClicked, screenConfig) + } +} + +@Composable +private fun buildScreenConfig() = + TutorialScreenConfig( + colors = rememberScreenColors(), + strings = + TutorialScreenConfig.Strings( + titleResId = R.string.tutorial_action_key_title, + bodyResId = R.string.tutorial_action_key_guidance, + titleSuccessResId = R.string.tutorial_action_key_success_title, + bodySuccessResId = R.string.tutorial_action_key_success_body + ), + animations = + TutorialScreenConfig.Animations( + educationResId = R.raw.action_key_edu, + successResId = R.raw.action_key_success + ) + ) + +@Composable +private fun rememberScreenColors(): TutorialScreenConfig.Colors { + val primaryFixedDim = LocalAndroidColorScheme.current.primaryFixedDim + val secondaryFixedDim = LocalAndroidColorScheme.current.secondaryFixedDim + val onSecondaryFixed = LocalAndroidColorScheme.current.onSecondaryFixed + val onSecondaryFixedVariant = LocalAndroidColorScheme.current.onSecondaryFixedVariant + val surfaceContainer = MaterialTheme.colorScheme.surfaceContainer + val dynamicProperties = + rememberLottieDynamicProperties( + rememberColorFilterProperty(".primaryFixedDim", primaryFixedDim), + rememberColorFilterProperty(".secondaryFixedDim", secondaryFixedDim), + rememberColorFilterProperty(".onSecondaryFixed", onSecondaryFixed), + rememberColorFilterProperty(".onSecondaryFixedVariant", onSecondaryFixedVariant) + ) + val screenColors = + remember(surfaceContainer, dynamicProperties) { + TutorialScreenConfig.Colors( + background = onSecondaryFixed, + successBackground = surfaceContainer, + title = primaryFixedDim, + animationColors = dynamicProperties, + ) + } + return screenColors +} diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/ui/composable/ActionTutorialContent.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/ui/composable/ActionTutorialContent.kt new file mode 100644 index 000000000000..c50b7dc06265 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/ui/composable/ActionTutorialContent.kt @@ -0,0 +1,237 @@ +/* + * 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.inputdevice.tutorial.ui.composable + +import android.graphics.ColorFilter +import android.graphics.PorterDuff +import android.graphics.PorterDuffColorFilter +import androidx.annotation.RawRes +import androidx.annotation.StringRes +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.snap +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.airbnb.lottie.LottieProperty +import com.airbnb.lottie.compose.LottieAnimation +import com.airbnb.lottie.compose.LottieCompositionSpec +import com.airbnb.lottie.compose.LottieConstants +import com.airbnb.lottie.compose.LottieDynamicProperties +import com.airbnb.lottie.compose.LottieDynamicProperty +import com.airbnb.lottie.compose.animateLottieCompositionAsState +import com.airbnb.lottie.compose.rememberLottieComposition +import com.airbnb.lottie.compose.rememberLottieDynamicProperty +import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.FINISHED +import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.IN_PROGRESS +import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.NOT_STARTED + +enum class TutorialActionState { + NOT_STARTED, + IN_PROGRESS, + FINISHED +} + +@Composable +fun ActionTutorialContent( + actionState: TutorialActionState, + onDoneButtonClicked: () -> Unit, + config: TutorialScreenConfig +) { + val animatedColor by + animateColorAsState( + targetValue = + if (actionState == FINISHED) config.colors.successBackground + else config.colors.background, + animationSpec = tween(durationMillis = 150, easing = LinearEasing), + label = "backgroundColor" + ) + Column( + verticalArrangement = Arrangement.Center, + modifier = + Modifier.fillMaxSize() + .drawBehind { drawRect(animatedColor) } + .padding(start = 48.dp, top = 124.dp, end = 48.dp, bottom = 48.dp) + ) { + Row(modifier = Modifier.fillMaxWidth().weight(1f)) { + TutorialDescription( + titleTextId = + if (actionState == FINISHED) config.strings.titleSuccessResId + else config.strings.titleResId, + titleColor = config.colors.title, + bodyTextId = + if (actionState == FINISHED) config.strings.bodySuccessResId + else config.strings.bodyResId, + modifier = Modifier.weight(1f) + ) + Spacer(modifier = Modifier.width(76.dp)) + TutorialAnimation( + actionState, + config, + modifier = Modifier.weight(1f).padding(top = 8.dp) + ) + } + DoneButton(onDoneButtonClicked = onDoneButtonClicked) + } +} + +@Composable +fun TutorialDescription( + @StringRes titleTextId: Int, + titleColor: Color, + @StringRes bodyTextId: Int, + modifier: Modifier = Modifier +) { + Column(verticalArrangement = Arrangement.Top, modifier = modifier) { + Text( + text = stringResource(id = titleTextId), + style = MaterialTheme.typography.displayLarge, + color = titleColor + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = stringResource(id = bodyTextId), + style = MaterialTheme.typography.bodyLarge, + color = Color.White + ) + } +} + +@Composable +fun TutorialAnimation( + actionState: TutorialActionState, + config: TutorialScreenConfig, + modifier: Modifier = Modifier +) { + Box(modifier = modifier.fillMaxWidth()) { + AnimatedContent( + targetState = actionState, + transitionSpec = { + if (initialState == NOT_STARTED) { + val transitionDurationMillis = 150 + 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 + EnterTransition.None togetherWith ExitTransition.None + } + } + ) { state -> + when (state) { + NOT_STARTED -> + EducationAnimation( + config.animations.educationResId, + config.colors.animationColors + ) + IN_PROGRESS -> + FrozenSuccessAnimation( + config.animations.successResId, + config.colors.animationColors + ) + FINISHED -> + SuccessAnimation(config.animations.successResId, config.colors.animationColors) + } + } + } +} + +@Composable +private fun FrozenSuccessAnimation( + @RawRes successAnimationId: Int, + animationProperties: LottieDynamicProperties +) { + val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(successAnimationId)) + LottieAnimation( + composition = composition, + progress = { 0f }, // animation should freeze on 1st frame + dynamicProperties = animationProperties, + ) +} + +@Composable +private fun EducationAnimation( + @RawRes educationAnimationId: Int, + animationProperties: LottieDynamicProperties +) { + val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(educationAnimationId)) + val progress by + animateLottieCompositionAsState(composition, iterations = LottieConstants.IterateForever) + LottieAnimation( + composition = composition, + progress = { progress }, + dynamicProperties = animationProperties, + ) +} + +@Composable +private fun SuccessAnimation( + @RawRes successAnimationId: Int, + animationProperties: LottieDynamicProperties +) { + val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(successAnimationId)) + val progress by animateLottieCompositionAsState(composition, iterations = 1) + LottieAnimation( + composition = composition, + progress = { progress }, + dynamicProperties = animationProperties, + ) +} + +@Composable +fun rememberColorFilterProperty( + layerName: String, + color: Color +): LottieDynamicProperty<ColorFilter> { + return rememberLottieDynamicProperty( + LottieProperty.COLOR_FILTER, + value = PorterDuffColorFilter(color.toArgb(), PorterDuff.Mode.SRC_ATOP), + // "**" below means match zero or more layers, so ** layerName ** means find layer with that + // name at any depth + keyPath = arrayOf("**", layerName, "**") + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialComponents.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/ui/composable/TutorialComponents.kt index f2276c8be71d..01ad585019d2 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialComponents.kt +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/ui/composable/TutorialComponents.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.touchpad.tutorial.ui.composable +package com.android.systemui.inputdevice.tutorial.ui.composable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialScreenConfig.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/ui/composable/TutorialScreenConfig.kt index d76ceb9380cd..0406bb9e6fef 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialScreenConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/ui/composable/TutorialScreenConfig.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.touchpad.tutorial.ui.composable +package com.android.systemui.inputdevice.tutorial.ui.composable import androidx.annotation.RawRes import androidx.annotation.StringRes diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/ui/view/KeyboardTouchpadTutorialActivity.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/ui/view/KeyboardTouchpadTutorialActivity.kt new file mode 100644 index 000000000000..3e382d669e5d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/ui/view/KeyboardTouchpadTutorialActivity.kt @@ -0,0 +1,74 @@ +/* + * 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.inputdevice.tutorial.ui.view + +import android.os.Bundle +import android.view.WindowManager +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.activity.viewModels +import androidx.compose.runtime.Composable +import com.android.compose.theme.PlatformTheme +import com.android.systemui.inputdevice.tutorial.TouchpadTutorialScreensProvider +import com.android.systemui.inputdevice.tutorial.ui.viewmodel.KeyboardTouchpadTutorialViewModel +import java.util.Optional +import javax.inject.Inject + +/** + * Activity for out of the box experience for keyboard and touchpad. Note that it's possible that + * either of them are actually not connected when this is launched + */ +class KeyboardTouchpadTutorialActivity +@Inject +constructor( + private val viewModelFactory: KeyboardTouchpadTutorialViewModel.Factory, + private val touchpadTutorialScreensProvider: Optional<TouchpadTutorialScreensProvider>, +) : ComponentActivity() { + + private val vm by + viewModels<KeyboardTouchpadTutorialViewModel>(factoryProducer = { viewModelFactory }) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + PlatformTheme { + KeyboardTouchpadTutorialContainer(vm, touchpadTutorialScreensProvider) { finish() } + } + } + // required to handle 3+ fingers on touchpad + window.addPrivateFlags(WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY) + } + + override fun onResume() { + super.onResume() + vm.onOpened() + } + + override fun onPause() { + super.onPause() + vm.onClosed() + } +} + +@Composable +fun KeyboardTouchpadTutorialContainer( + vm: KeyboardTouchpadTutorialViewModel, + touchpadTutorialScreensProvider: Optional<TouchpadTutorialScreensProvider>, + closeTutorial: () -> Unit +) {} diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/ui/viewmodel/KeyboardTouchpadTutorialViewModel.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/ui/viewmodel/KeyboardTouchpadTutorialViewModel.kt new file mode 100644 index 000000000000..39b1ec0f0390 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/ui/viewmodel/KeyboardTouchpadTutorialViewModel.kt @@ -0,0 +1,62 @@ +/* + * 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.inputdevice.tutorial.ui.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.android.systemui.touchpad.tutorial.domain.interactor.TouchpadGesturesInteractor +import java.util.Optional +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +class KeyboardTouchpadTutorialViewModel( + private val gesturesInteractor: Optional<TouchpadGesturesInteractor> +) : ViewModel() { + + private val _screen = MutableStateFlow(Screen.BACK_GESTURE) + val screen: StateFlow<Screen> = _screen + + fun goTo(screen: Screen) { + _screen.value = screen + } + + fun onOpened() { + gesturesInteractor.ifPresent { it.disableGestures() } + } + + fun onClosed() { + gesturesInteractor.ifPresent { it.enableGestures() } + } + + class Factory + @Inject + constructor(private val gesturesInteractor: Optional<TouchpadGesturesInteractor>) : + ViewModelProvider.Factory { + + @Suppress("UNCHECKED_CAST") + override fun <T : ViewModel> create(modelClass: Class<T>): T { + return KeyboardTouchpadTutorialViewModel(gesturesInteractor) as T + } + } +} + +enum class Screen { + BACK_GESTURE, + HOME_GESTURE, + ACTION_KEY +} diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/data/model/TutorialSchedulerInfo.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/data/model/TutorialSchedulerInfo.kt index cfe64e269c95..9f46846f0d91 100644 --- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/data/model/TutorialSchedulerInfo.kt +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/data/model/TutorialSchedulerInfo.kt @@ -16,11 +16,6 @@ package com.android.systemui.inputdevice.tutorial.data.model -data class TutorialSchedulerInfo( - val keyboard: DeviceSchedulerInfo = DeviceSchedulerInfo(), - val touchpad: DeviceSchedulerInfo = DeviceSchedulerInfo() -) - data class DeviceSchedulerInfo(var isLaunched: Boolean = false, var connectTime: Long? = null) { val wasEverConnected: Boolean get() = connectTime != null diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/data/repository/TutorialSchedulerRepository.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/data/repository/TutorialSchedulerRepository.kt index 31ff01836428..b9b38954784e 100644 --- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/data/repository/TutorialSchedulerRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/data/repository/TutorialSchedulerRepository.kt @@ -25,21 +25,32 @@ import androidx.datastore.preferences.core.longPreferencesKey import androidx.datastore.preferences.preferencesDataStore import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.inputdevice.tutorial.data.model.DeviceSchedulerInfo -import com.android.systemui.inputdevice.tutorial.data.model.TutorialSchedulerInfo import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map @SysUISingleton class TutorialSchedulerRepository @Inject -constructor(@Application private val applicationContext: Context) { +constructor( + @Application private val applicationContext: Context, + @Background private val backgroundScope: CoroutineScope +) { private val Context.dataStore: DataStore<Preferences> by - preferencesDataStore(name = DATASTORE_NAME) + preferencesDataStore(name = DATASTORE_NAME, scope = backgroundScope) - suspend fun loadData(): TutorialSchedulerInfo { + suspend fun isLaunched(deviceType: DeviceType): Boolean = loadData()[deviceType]!!.isLaunched + + suspend fun wasEverConnected(deviceType: DeviceType): Boolean = + loadData()[deviceType]!!.wasEverConnected + + suspend fun connectTime(deviceType: DeviceType): Long = loadData()[deviceType]!!.connectTime!! + + private suspend fun loadData(): Map<DeviceType, DeviceSchedulerInfo> { return applicationContext.dataStore.data.map { pref -> getSchedulerInfo(pref) }.first() } @@ -51,10 +62,10 @@ constructor(@Application private val applicationContext: Context) { applicationContext.dataStore.edit { pref -> pref[getLaunchedKey(device)] = true } } - private fun getSchedulerInfo(pref: Preferences): TutorialSchedulerInfo { - return TutorialSchedulerInfo( - keyboard = getDeviceSchedulerInfo(pref, DeviceType.KEYBOARD), - touchpad = getDeviceSchedulerInfo(pref, DeviceType.TOUCHPAD) + private fun getSchedulerInfo(pref: Preferences): Map<DeviceType, DeviceSchedulerInfo> { + return mapOf( + DeviceType.KEYBOARD to getDeviceSchedulerInfo(pref, DeviceType.KEYBOARD), + DeviceType.TOUCHPAD to getDeviceSchedulerInfo(pref, DeviceType.TOUCHPAD) ) } diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialSchedulerInteractor.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialSchedulerInteractor.kt index 05e104468f67..b3b8f21a4a4b 100644 --- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialSchedulerInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialSchedulerInteractor.kt @@ -16,23 +16,25 @@ package com.android.systemui.inputdevice.tutorial.domain.interactor -import android.content.Context -import android.content.Intent +import android.os.SystemProperties +import android.util.Log import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.inputdevice.tutorial.data.model.DeviceSchedulerInfo +import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType +import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType.KEYBOARD +import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType.TOUCHPAD import com.android.systemui.inputdevice.tutorial.data.repository.TutorialSchedulerRepository import com.android.systemui.keyboard.data.repository.KeyboardRepository import com.android.systemui.touchpad.data.repository.TouchpadRepository -import java.time.Duration import java.time.Instant import javax.inject.Inject +import kotlin.time.Duration.Companion.hours import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.merge import kotlinx.coroutines.launch /** @@ -43,62 +45,72 @@ import kotlinx.coroutines.launch class TutorialSchedulerInteractor @Inject constructor( - @Application private val context: Context, - @Application private val applicationScope: CoroutineScope, - private val keyboardRepository: KeyboardRepository, - private val touchpadRepository: TouchpadRepository, - private val tutorialSchedulerRepository: TutorialSchedulerRepository + @Background private val backgroundScope: CoroutineScope, + keyboardRepository: KeyboardRepository, + touchpadRepository: TouchpadRepository, + private val repo: TutorialSchedulerRepository ) { + private val isAnyDeviceConnected = + mapOf( + KEYBOARD to keyboardRepository.isAnyKeyboardConnected, + TOUCHPAD to touchpadRepository.isAnyTouchpadConnected + ) + fun start() { - applicationScope.launch { - val info = tutorialSchedulerRepository.loadData() - if (!info.keyboard.isLaunched) { - applicationScope.launch { - schedule( - keyboardRepository.isAnyKeyboardConnected, - info.keyboard, - DeviceType.KEYBOARD - ) - } - } - if (!info.touchpad.isLaunched) { - applicationScope.launch { - schedule( - touchpadRepository.isAnyTouchpadConnected, - info.touchpad, - DeviceType.TOUCHPAD - ) - } + backgroundScope.launch { + // Merging two flows to ensure that launch tutorial is launched consecutively in order + // to avoid race condition + merge(touchpadScheduleFlow, keyboardScheduleFlow).collect { + val tutorialType = resolveTutorialType(it) + launchTutorial(tutorialType) } } } - private suspend fun schedule( - isAnyDeviceConnected: Flow<Boolean>, - info: DeviceSchedulerInfo, - deviceType: DeviceType - ) { - if (!info.wasEverConnected) { - waitForDeviceConnection(isAnyDeviceConnected) - info.connectTime = Instant.now().toEpochMilli() - tutorialSchedulerRepository.updateConnectTime(deviceType, info.connectTime!!) + private val touchpadScheduleFlow = flow { + if (!repo.isLaunched(TOUCHPAD)) { + schedule(TOUCHPAD) + emit(TOUCHPAD) } - delay(remainingTimeMillis(info.connectTime!!)) - waitForDeviceConnection(isAnyDeviceConnected) - info.isLaunched = true - tutorialSchedulerRepository.updateLaunch(deviceType) - launchTutorial() } - private suspend fun waitForDeviceConnection(isAnyDeviceConnected: Flow<Boolean>): Boolean { - return isAnyDeviceConnected.filter { it }.first() + private val keyboardScheduleFlow = flow { + if (!repo.isLaunched(KEYBOARD)) { + schedule(KEYBOARD) + emit(KEYBOARD) + } } - private fun launchTutorial() { - val intent = Intent(TUTORIAL_ACTION) - intent.addCategory(Intent.CATEGORY_DEFAULT) - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - context.startActivity(intent) + private suspend fun schedule(deviceType: DeviceType) { + if (!repo.wasEverConnected(deviceType)) { + waitForDeviceConnection(deviceType) + repo.updateConnectTime(deviceType, Instant.now().toEpochMilli()) + } + delay(remainingTimeMillis(start = repo.connectTime(deviceType))) + waitForDeviceConnection(deviceType) + } + + private suspend fun waitForDeviceConnection(deviceType: DeviceType) = + isAnyDeviceConnected[deviceType]!!.filter { it }.first() + + private suspend fun launchTutorial(tutorialType: TutorialType) { + if (tutorialType == TutorialType.KEYBOARD || tutorialType == TutorialType.BOTH) + repo.updateLaunch(KEYBOARD) + if (tutorialType == TutorialType.TOUCHPAD || tutorialType == TutorialType.BOTH) + repo.updateLaunch(TOUCHPAD) + // TODO: launch tutorial + Log.d(TAG, "Launch tutorial for $tutorialType") + } + + private suspend fun resolveTutorialType(deviceType: DeviceType): TutorialType { + // Resolve the type of tutorial depending on which device are connected when the tutorial is + // launched. E.g. when the keyboard is connected for [LAUNCH_DELAY], both keyboard and + // touchpad are connected, we launch the tutorial for both. + if (repo.isLaunched(deviceType)) return TutorialType.NONE + val otherDevice = if (deviceType == KEYBOARD) TOUCHPAD else KEYBOARD + val isOtherDeviceConnected = isAnyDeviceConnected[otherDevice]!!.first() + if (!repo.isLaunched(otherDevice) && isOtherDeviceConnected) return TutorialType.BOTH + return if (deviceType == KEYBOARD) TutorialType.KEYBOARD else TutorialType.TOUCHPAD } private fun remainingTimeMillis(start: Long): Long { @@ -107,7 +119,20 @@ constructor( } companion object { - const val TUTORIAL_ACTION = "com.android.systemui.action.TOUCHPAD_TUTORIAL" - private val LAUNCH_DELAY = Duration.ofHours(72).toMillis() + const val TAG = "TutorialSchedulerInteractor" + private val DEFAULT_LAUNCH_DELAY = 72.hours.inWholeMilliseconds + private val LAUNCH_DELAY: Long + get() = + SystemProperties.getLong( + "persist.peripheral_tutorial_delay_ms", + DEFAULT_LAUNCH_DELAY + ) + } + + enum class TutorialType { + KEYBOARD, + TOUCHPAD, + BOTH, + NONE } } 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/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index 0b3d0f7160f0..17c5977fc80a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -42,6 +42,7 @@ import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STR import static com.android.systemui.DejankUtils.whitelistIpcs; import static com.android.systemui.Flags.notifyPowerManagerUserActivityBackground; import static com.android.systemui.Flags.refactorGetCurrentUser; +import static com.android.systemui.Flags.relockWithPowerButtonImmediately; import static com.android.systemui.Flags.translucentOccludingActivityFix; import static com.android.systemui.keyguard.ui.viewmodel.LockscreenToDreamingTransitionViewModel.DREAMING_ANIMATION_DURATION_MS; @@ -477,6 +478,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, private boolean mUnlockingAndWakingFromDream = false; private boolean mHideAnimationRun = false; private boolean mHideAnimationRunning = false; + private boolean mIsKeyguardExitAnimationCanceled = false; private SoundPool mLockSounds; private int mLockSoundId; @@ -1588,10 +1590,11 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, setShowingLocked(!shouldWaitForProvisioning() && !mLockPatternUtils.isLockScreenDisabled( mSelectedUserInteractor.getSelectedUserId()), - true /* forceCallbacks */); + true /* forceCallbacks */, "setupLocked - keyguard service enabled"); } else { // The system's keyguard is disabled or missing. - setShowingLocked(false /* showing */, true /* forceCallbacks */); + setShowingLocked(false /* showing */, true /* forceCallbacks */, + "setupLocked - keyguard service disabled"); } mKeyguardTransitions.register( @@ -2334,6 +2337,12 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, Log.e(TAG, "doKeyguard: we're still showing, but going away. Re-show the " + "keyguard rather than short-circuiting and resetting."); } else { + // We're removing "reset" in the refactor - "resetting" the views will happen + // as a reaction to the root cause of the "reset" signal. + if (KeyguardWmStateRefactor.isEnabled()) { + return; + } + // It's already showing, and we're not trying to show it while the screen is // off. We can simply reset all of the views, but don't hide the bouncer in case // the user is currently interacting with it. @@ -2399,6 +2408,16 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, */ private void handleDismiss(IKeyguardDismissCallback callback, CharSequence message) { if (mShowing) { + if (KeyguardWmStateRefactor.isEnabled()) { + Log.d(TAG, "Dismissing keyguard with keyguard_wm_refactor_enabled: " + + "cancelDoKeyguardLaterLocked"); + + // This won't get canceled in onKeyguardExitFinished() if the refactor is enabled, + // which can lead to the keyguard re-showing. Cancel here for now; this can be + // removed once we migrate the logic that posts doKeyguardLater in the first place. + cancelDoKeyguardLaterLocked(); + } + if (callback != null) { mDismissCallbackRegistry.addCallback(callback); } @@ -2817,9 +2836,10 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, playSound(mTrustedSoundId); } - private void updateActivityLockScreenState(boolean showing, boolean aodShowing) { + private void updateActivityLockScreenState(boolean showing, boolean aodShowing, String reason) { mUiBgExecutor.execute(() -> { - Log.d(TAG, "updateActivityLockScreenState(" + showing + ", " + aodShowing + ")"); + Log.d(TAG, "updateActivityLockScreenState(" + showing + ", " + aodShowing + ", " + + reason + ")"); if (KeyguardWmStateRefactor.isEnabled()) { // Handled in WmLockscreenVisibilityManager if flag is enabled. @@ -2879,7 +2899,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, // Force if we're showing in the middle of unlocking, to ensure we end up in the // correct state. - setShowingLocked(true, hidingOrGoingAway /* force */); + setShowingLocked(true, hidingOrGoingAway /* force */, "handleShowInner"); mHiding = false; if (!KeyguardWmStateRefactor.isEnabled()) { @@ -3051,15 +3071,14 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mHiding = true; mKeyguardGoingAwayRunnable.run(); } else { - Log.d(TAG, "Hiding keyguard while occluded. Just hide the keyguard view and exit."); - if (!KeyguardWmStateRefactor.isEnabled()) { mKeyguardViewControllerLazy.get().hide( mSystemClock.uptimeMillis() + mHideAnimation.getStartOffset(), mHideAnimation.getDuration()); } - onKeyguardExitFinished(); + onKeyguardExitFinished("Hiding keyguard while occluded. Just hide the keyguard " + + "view and exit."); } // It's possible that the device was unlocked (via BOUNCER or Fingerprint) while @@ -3090,6 +3109,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, Log.d(TAG, "handleStartKeyguardExitAnimation startTime=" + startTime + " fadeoutDuration=" + fadeoutDuration); synchronized (KeyguardViewMediator.this) { + mIsKeyguardExitAnimationCanceled = false; // Tell ActivityManager that we canceled the keyguard animation if // handleStartKeyguardExitAnimation was called, but we're not hiding the keyguard, // unless we're animating the surface behind the keyguard and will be hiding the @@ -3109,7 +3129,8 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, Slog.w(TAG, "Failed to call onAnimationFinished", e); } } - setShowingLocked(mShowing, true /* force */); + setShowingLocked(mShowing, true /* force */, + "handleStartKeyguardExitAnimation - canceled"); return; } mHiding = false; @@ -3133,9 +3154,11 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, Slog.w(TAG, "Failed to call onAnimationFinished", e); } } - onKeyguardExitFinished(); - mKeyguardViewControllerLazy.get().hide(0 /* startTime */, - 0 /* fadeoutDuration */); + if (!mIsKeyguardExitAnimationCanceled) { + onKeyguardExitFinished("onRemoteAnimationFinished"); + mKeyguardViewControllerLazy.get().hide(0 /* startTime */, + 0 /* fadeoutDuration */); + } mInteractionJankMonitor.end(CUJ_LOCKSCREEN_UNLOCK_ANIMATION); } @@ -3272,12 +3295,12 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, anim.start(); }); - onKeyguardExitFinished(); + onKeyguardExitFinished("remote animation disabled"); } } } - private void onKeyguardExitFinished() { + private void onKeyguardExitFinished(String reason) { if (DEBUG) Log.d(TAG, "onKeyguardExitFinished()"); // only play "unlock" noises if not on a call (since the incall UI // disables the keyguard) @@ -3285,7 +3308,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, playSounds(false); } - setShowingLocked(false); + setShowingLocked(false, "onKeyguardExitFinished: " + reason); mWakeAndUnlocking = false; mDismissCallbackRegistry.notifyDismissSucceeded(); resetKeyguardDonePendingLocked(); @@ -3333,6 +3356,9 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, // A lock is pending, meaning the keyguard exit animation was cancelled because we're // re-locking. We should just end the surface-behind animation without exiting the // keyguard. The pending lock will be handled by onFinishedGoingToSleep(). + if (relockWithPowerButtonImmediately()) { + mIsKeyguardExitAnimationCanceled = true; + } finishSurfaceBehindRemoteAnimation(true /* showKeyguard */); maybeHandlePendingLock(); } else { @@ -3381,12 +3407,13 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, doKeyguardLocked(null); finishSurfaceBehindRemoteAnimation(true /* showKeyguard */); // Ensure WM is notified that we made a decision to show - setShowingLocked(true /* showing */, true /* force */); + setShowingLocked(true /* showing */, true /* force */, + "exitKeyguardAndFinishSurfaceBehindRemoteAnimation - relocked"); return; } - onKeyguardExitFinished(); + onKeyguardExitFinished("exitKeyguardAndFinishSurfaceBehindRemoteAnimation"); if (mKeyguardStateController.isDismissingFromSwipe() || wasShowing) { Log.d(TAG, "onKeyguardExitRemoteAnimationFinished" @@ -3443,7 +3470,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mSurfaceBehindRemoteAnimationRequested = false; mKeyguardStateController.notifyKeyguardGoingAway(false); if (mShowing) { - setShowingLocked(true, true); + setShowingLocked(true, true, "hideSurfaceBehindKeyguard"); } } @@ -3789,7 +3816,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, // update lock screen state in ATMS here, otherwise ATMS tries to resume activities when // enabling doze state. if (mShowing || !mPendingLock || !mDozeParameters.canControlUnlockedScreenOff()) { - setShowingLocked(mShowing); + setShowingLocked(mShowing, "setDozing"); } } @@ -3799,7 +3826,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, // is 1f), then show the activity lock screen. if (mAnimatingScreenOff && mDozing && linear == 1f) { mAnimatingScreenOff = false; - setShowingLocked(mShowing, true); + setShowingLocked(mShowing, true, "onDozeAmountChanged"); } } @@ -3837,11 +3864,11 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } } - void setShowingLocked(boolean showing) { - setShowingLocked(showing, false /* forceCallbacks */); + void setShowingLocked(boolean showing, String reason) { + setShowingLocked(showing, false /* forceCallbacks */, reason); } - private void setShowingLocked(boolean showing, boolean forceCallbacks) { + private void setShowingLocked(boolean showing, boolean forceCallbacks, String reason) { final boolean aodShowing = mDozing && !mWakeAndUnlocking; final boolean notifyDefaultDisplayCallbacks = showing != mShowing || forceCallbacks; final boolean updateActivityLockScreenState = showing != mShowing @@ -3852,9 +3879,8 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, notifyDefaultDisplayCallbacks(showing); } if (updateActivityLockScreenState) { - updateActivityLockScreenState(showing, aodShowing); + updateActivityLockScreenState(showing, aodShowing, reason); } - } private void notifyDefaultDisplayCallbacks(boolean showing) { 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/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt index de60c1117c19..797a4ec419a9 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt @@ -285,7 +285,7 @@ constructor( state: TransitionState ) { if (updateTransitionId != transitionId) { - Log.wtf(TAG, "Attempting to update with old/invalid transitionId: $transitionId") + Log.e(TAG, "Attempting to update with old/invalid transitionId: $transitionId") return } 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/FromLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt index 51d92f054bbe..5dc020f41ad3 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt @@ -300,7 +300,9 @@ constructor( swipeToDismissInteractor.dismissFling .filterNotNull() .filterRelevantKeyguardState() - .collect { _ -> startTransitionTo(KeyguardState.GONE) } + .collect { _ -> + startTransitionTo(KeyguardState.GONE, ownerReason = "dismissFling != null") + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt index 710b710aa7d5..aea57ce15794 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt @@ -157,6 +157,13 @@ constructor( } } + /** Starts a transition to dismiss the keyguard from the OCCLUDED state. */ + fun dismissFromOccluded() { + scope.launch { + startTransitionTo(KeyguardState.GONE, ownerReason = "Dismiss from occluded") + } + } + private fun listenForOccludedToGone() { if (KeyguardWmStateRefactor.isEnabled) { // We don't think OCCLUDED to GONE is possible. You should always have to go via a 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/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt index afbe3579315d..efdae6202805 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt @@ -75,6 +75,7 @@ constructor( private val fromAlternateBouncerTransitionInteractor: dagger.Lazy<FromAlternateBouncerTransitionInteractor>, private val fromDozingTransitionInteractor: dagger.Lazy<FromDozingTransitionInteractor>, + private val fromOccludedTransitionInteractor: dagger.Lazy<FromOccludedTransitionInteractor>, private val sceneInteractor: SceneInteractor, ) { private val transitionMap = mutableMapOf<Edge.StateToState, MutableSharedFlow<TransitionStep>>() @@ -418,6 +419,7 @@ constructor( fromAlternateBouncerTransitionInteractor.get().dismissAlternateBouncer() AOD -> fromAodTransitionInteractor.get().dismissAod() DOZING -> fromDozingTransitionInteractor.get().dismissFromDozing() + KeyguardState.OCCLUDED -> fromOccludedTransitionInteractor.get().dismissFromOccluded() KeyguardState.GONE -> Log.i( TAG, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SwipeToDismissInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SwipeToDismissInteractor.kt index 86e41154205e..906d58664de9 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SwipeToDismissInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SwipeToDismissInteractor.kt @@ -19,14 +19,15 @@ package com.android.systemui.keyguard.domain.interactor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.StatusBarState import com.android.systemui.shade.data.repository.ShadeRepository import com.android.systemui.util.kotlin.Utils.Companion.sample -import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import javax.inject.Inject /** * Handles logic around the swipe to dismiss gesture, where the user swipes up on the dismissable @@ -53,12 +54,14 @@ constructor( shadeRepository.currentFling .sample( transitionInteractor.startedKeyguardState, - keyguardInteractor.isKeyguardDismissible + keyguardInteractor.isKeyguardDismissible, + keyguardInteractor.statusBarState, ) - .filter { (flingInfo, startedState, keyguardDismissable) -> + .filter { (flingInfo, startedState, keyguardDismissable, statusBarState) -> flingInfo != null && - !flingInfo.expand && - startedState == KeyguardState.LOCKSCREEN && + !flingInfo.expand && + statusBarState != StatusBarState.SHADE_LOCKED && + startedState == KeyguardState.LOCKSCREEN && keyguardDismissable } .map { (flingInfo, _) -> flingInfo } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt index e1b333dd1d06..25b2b7cad7ec 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt @@ -93,6 +93,14 @@ constructor( KeyguardState.ALTERNATE_BOUNCER -> { fromAlternateBouncerInteractor.surfaceBehindVisibility } + KeyguardState.OCCLUDED -> { + // OCCLUDED -> GONE occurs when an app is on top of the keyguard, and then + // requests manual dismissal of the keyguard in the background. The app will + // remain visible on top of the stack throughout this transition, so we + // should not trigger the keyguard going away animation by returning + // surfaceBehindVisibility = true. + flowOf(false) + } else -> flowOf(null) } } @@ -253,6 +261,18 @@ constructor( ) { // Dreams dismiss keyguard and return to GONE if they can. false + } else if ( + startedWithPrev.newValue.from == KeyguardState.OCCLUDED && + startedWithPrev.newValue.to == KeyguardState.GONE + ) { + // OCCLUDED -> GONE directly, without transiting a *_BOUNCER state, occurs + // when an app uses intent flags to launch over an insecure keyguard without + // dismissing it, and then manually requests keyguard dismissal while + // OCCLUDED. This transition is not user-visible; the device unlocks in the + // background and the app remains on top, while we're now GONE. In this case + // we should simply tell WM that the lockscreen is no longer visible, and + // *not* play the going away animation or related animations. + false } else { // Otherwise, use the visibility of the current state. KeyguardState.lockscreenVisibleInState(currentState) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerUdfpsViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerUdfpsViewBinder.kt index 9dc77d3dc9d3..fb9719142b54 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerUdfpsViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerUdfpsViewBinder.kt @@ -26,6 +26,7 @@ import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor import com.android.systemui.keyguard.ui.view.DeviceEntryIconView import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerUdfpsIconViewModel import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.scene.shared.flag.SceneContainerFlag import kotlinx.coroutines.ExperimentalCoroutinesApi @ExperimentalCoroutinesApi @@ -52,7 +53,11 @@ object AlternateBouncerUdfpsViewBinder { } } - launch("$TAG#viewModel.alpha") { viewModel.alpha.collect { view.alpha = it } } + if (SceneContainerFlag.isEnabled) { + view.alpha = 1f + } else { + launch("$TAG#viewModel.alpha") { viewModel.alpha.collect { view.alpha = it } } + } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt index a250b22dde07..91a7f7fc66bd 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt @@ -37,13 +37,13 @@ import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor import com.android.systemui.deviceentry.ui.binder.UdfpsAccessibilityOverlayBinder import com.android.systemui.deviceentry.ui.view.UdfpsAccessibilityOverlay import com.android.systemui.deviceentry.ui.viewmodel.AlternateBouncerUdfpsAccessibilityOverlayViewModel -import com.android.systemui.keyguard.DismissCallbackRegistry import com.android.systemui.keyguard.ui.view.DeviceEntryIconView import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerDependencies import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerUdfpsIconViewModel import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerWindowViewModel import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.res.R +import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.scrim.ScrimView import dagger.Lazy import javax.inject.Inject @@ -67,7 +67,6 @@ constructor( private val alternateBouncerDependencies: Lazy<AlternateBouncerDependencies>, private val windowManager: Lazy<WindowManager>, private val layoutInflater: Lazy<LayoutInflater>, - private val dismissCallbackRegistry: DismissCallbackRegistry, ) : CoreStartable { private val layoutParams: WindowManager.LayoutParams get() = @@ -95,9 +94,10 @@ constructor( private var alternateBouncerView: ConstraintLayout? = null override fun start() { - if (!DeviceEntryUdfpsRefactor.isEnabled) { + if (!DeviceEntryUdfpsRefactor.isEnabled || SceneContainerFlag.isEnabled) { return } + applicationScope.launch("$TAG#alternateBouncerWindowViewModel") { alternateBouncerWindowViewModel.get().alternateBouncerWindowRequired.collect { addAlternateBouncerWindowView -> @@ -110,7 +110,7 @@ constructor( bind(alternateBouncerView!!, alternateBouncerDependencies.get()) } else { removeViewFromWindowManager() - alternateBouncerDependencies.get().viewModel.hideAlternateBouncer() + alternateBouncerDependencies.get().viewModel.onRemovedFromWindow() } } } @@ -144,7 +144,7 @@ constructor( private val onAttachAddBackGestureHandler = object : View.OnAttachStateChangeListener { private val onBackInvokedCallback: OnBackInvokedCallback = OnBackInvokedCallback { - onBackRequested() + alternateBouncerDependencies.get().viewModel.onBackRequested() } override fun onViewAttachedToWindow(view: View) { @@ -161,14 +161,12 @@ constructor( .findOnBackInvokedDispatcher() ?.unregisterOnBackInvokedCallback(onBackInvokedCallback) } - - fun onBackRequested() { - alternateBouncerDependencies.get().viewModel.hideAlternateBouncer() - dismissCallbackRegistry.notifyDismissCancelled() - } } private fun addViewToWindowManager() { + if (SceneContainerFlag.isEnabled) { + return + } if (alternateBouncerView != null) { return } @@ -190,6 +188,7 @@ constructor( if (DeviceEntryUdfpsRefactor.isUnexpectedlyInLegacyMode()) { return } + optionallyAddUdfpsViews( view = view, udfpsIconViewModel = alternateBouncerDependencies.udfpsIconViewModel, @@ -202,12 +201,13 @@ constructor( viewModel = alternateBouncerDependencies.messageAreaViewModel, ) - val scrim = view.requireViewById(R.id.alternate_bouncer_scrim) as ScrimView + val scrim: ScrimView = view.requireViewById(R.id.alternate_bouncer_scrim) val viewModel = alternateBouncerDependencies.viewModel val swipeUpAnywhereGestureHandler = alternateBouncerDependencies.swipeUpAnywhereGestureHandler val tapGestureDetector = alternateBouncerDependencies.tapGestureDetector - view.repeatWhenAttached { alternateBouncerViewContainer -> + + view.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.STARTED) { launch("$TAG#viewModel.registerForDismissGestures") { viewModel.registerForDismissGestures.collect { registerForDismissGestures -> @@ -216,11 +216,11 @@ constructor( swipeTag ) { _ -> alternateBouncerDependencies.powerInteractor.onUserTouch() - viewModel.showPrimaryBouncer() + viewModel.onTapped() } tapGestureDetector.addOnGestureDetectedCallback(tapTag) { _ -> alternateBouncerDependencies.powerInteractor.onUserTouch() - viewModel.showPrimaryBouncer() + viewModel.onTapped() } } else { swipeUpAnywhereGestureHandler.removeOnGestureDetectedCallback( 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/DefaultNotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt index 380e361eb33e..6ac33af26605 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt @@ -24,7 +24,6 @@ import androidx.constraintlayout.widget.ConstraintSet.END import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID import androidx.constraintlayout.widget.ConstraintSet.START import androidx.constraintlayout.widget.ConstraintSet.TOP -import com.android.systemui.Flags.centralizedStatusBarHeightFix import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.res.R import com.android.systemui.shade.LargeScreenHeaderHelper @@ -64,13 +63,7 @@ constructor( val useLargeScreenHeader = context.resources.getBoolean(R.bool.config_use_large_screen_shade_header) val marginTopLargeScreen = - if (centralizedStatusBarHeightFix()) { - largeScreenHeaderHelperLazy.get().getLargeScreenHeaderHeight() - } else { - context.resources.getDimensionPixelSize( - R.dimen.large_screen_shade_header_height - ) - } + largeScreenHeaderHelperLazy.get().getLargeScreenHeaderHeight() connect( R.id.nssl_placeholder, TOP, 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/AlternateBouncerUdfpsIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerUdfpsIconViewModel.kt index df0b3dc3cbce..4908dbdec61e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerUdfpsIconViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerUdfpsIconViewModel.kt @@ -23,6 +23,7 @@ import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor import com.android.systemui.keyguard.ui.view.DeviceEntryIconView +import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shared.recents.utilities.Utilities.clamp import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -50,6 +51,7 @@ constructor( private val isSupported: Flow<Boolean> = deviceEntryUdfpsInteractor.isUdfpsSupported val alpha: Flow<Float> = alternateBouncerViewModel.transitionToAlternateBouncerProgress.map { + SceneContainerFlag.assertInLegacyMode() clamp(it * 2f, 0f, 1f) } 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..7b0b23ffb2ff 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 @@ -18,15 +18,20 @@ package com.android.systemui.keyguard.ui.viewmodel import android.graphics.Color +import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor +import com.android.systemui.keyguard.DismissCallbackRegistry import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.KeyguardState.ALTERNATE_BOUNCER +import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager +import dagger.Lazy import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach @ExperimentalCoroutinesApi class AlternateBouncerViewModel @@ -34,17 +39,20 @@ class AlternateBouncerViewModel constructor( private val statusBarKeyguardViewManager: StatusBarKeyguardViewManager, keyguardTransitionInteractor: KeyguardTransitionInteractor, + private val dismissCallbackRegistry: DismissCallbackRegistry, + alternateBouncerInteractor: Lazy<AlternateBouncerInteractor>, ) { // When we're fully transitioned to the AlternateBouncer, the alpha of the scrim should be: private val alternateBouncerScrimAlpha = .66f + /** Reports the alternate bouncer visible state if the scene container flag is enabled. */ + val isVisible: Flow<Boolean> = + alternateBouncerInteractor.get().isVisible.onEach { SceneContainerFlag.assertInNewMode() } + /** Progress to a fully transitioned alternate bouncer. 1f represents fully transitioned. */ - val transitionToAlternateBouncerProgress = + val transitionToAlternateBouncerProgress: Flow<Float> = 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 } @@ -54,11 +62,16 @@ constructor( val registerForDismissGestures: Flow<Boolean> = transitionToAlternateBouncerProgress.map { it == 1f }.distinctUntilChanged() - fun showPrimaryBouncer() { + fun onTapped() { statusBarKeyguardViewManager.showPrimaryBouncer(/* scrimmed */ true) } - fun hideAlternateBouncer() { + fun onRemovedFromWindow() { + statusBarKeyguardViewManager.hideAlternateBouncer(false) + } + + fun onBackRequested() { statusBarKeyguardViewManager.hideAlternateBouncer(false) + dismissCallbackRegistry.notifyDismissCancelled() } } 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 661da6d2af13..c2b5d98699b4 100644 --- a/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt +++ b/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt @@ -227,13 +227,33 @@ 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. * - * The [block] may be run multiple times, running once per every time the view is attached. + * [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 { @@ -249,7 +269,7 @@ suspend fun View.repeatWhenAttachedToWindow(block: suspend CoroutineScope.() -> * * Only use from the main thread. * - * The [block] may be run multiple times, running once per every time the window becomes visible. + * [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 { @@ -265,7 +285,7 @@ suspend fun View.repeatWhenWindowIsVisible(block: suspend CoroutineScope.() -> U * * Only use from the main thread. * - * The [block] may be run multiple times, running once per every time the window is focused. + * [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 { @@ -274,6 +294,21 @@ 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 0af5feaff3b2..77314813c34a 100644 --- a/packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt @@ -16,9 +16,10 @@ package com.android.systemui.lifecycle +import android.view.View import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.remember +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch /** Base class for all System UI view-models. */ abstract class SysUiViewModel : SafeActivatable() { @@ -37,8 +38,20 @@ abstract class SysUiViewModel : SafeActivatable() { fun <T : SysUiViewModel> rememberViewModel( key: Any = Unit, factory: () -> T, -): T { - val instance = remember(key) { factory() } - LaunchedEffect(instance) { instance.activate() } - return instance -} +): 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) + } diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewController.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewController.kt index 46aa0644035c..2ce7044897be 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewController.kt @@ -27,7 +27,6 @@ import android.view.ViewGroup import android.window.RemoteTransition import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import com.android.systemui.Flags.pssAppSelectorAbruptExitFix import com.android.systemui.Flags.pssAppSelectorRecentsSplitScreen import com.android.systemui.display.naturalBounds import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorResultHandler @@ -160,7 +159,7 @@ constructor( private fun createAnimation(task: RecentTask, view: View): ActivityOptions = - if (pssAppSelectorAbruptExitFix() && task.isForegroundTask) { + if (task.isForegroundTask) { // When the selected task is in the foreground, the scale up animation doesn't work. // We fallback to the default close animation. ActivityOptions.makeCustomTaskAnimation( 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/navigationbar/NavigationBarModule.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarModule.java index e2ba76141845..a8b979e05276 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarModule.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarModule.java @@ -16,11 +16,16 @@ package com.android.systemui.navigationbar; +import static com.android.systemui.Flags.enableViewCaptureTracing; +import static com.android.systemui.util.ConvenienceExtensionsKt.toKotlinLazy; + import android.content.Context; import android.view.LayoutInflater; import android.view.View; import android.view.WindowManager; +import com.android.app.viewcapture.ViewCapture; +import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.systemui.dagger.qualifiers.DisplayId; import com.android.systemui.navigationbar.NavigationBarComponent.NavigationBarScope; import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler; @@ -28,6 +33,7 @@ import com.android.systemui.navigationbar.views.NavigationBarFrame; import com.android.systemui.navigationbar.views.NavigationBarView; import com.android.systemui.res.R; +import dagger.Lazy; import dagger.Module; import dagger.Provides; @@ -73,4 +79,15 @@ public interface NavigationBarModule { static WindowManager provideWindowManager(@DisplayId Context context) { return context.getSystemService(WindowManager.class); } + + /** A ViewCaptureAwareWindowManager specific to the display's context. */ + @Provides + @NavigationBarScope + @DisplayId + static ViewCaptureAwareWindowManager provideViewCaptureAwareWindowManager( + @DisplayId WindowManager windowManager, Lazy<ViewCapture> daggerLazyViewCapture) { + return new ViewCaptureAwareWindowManager(windowManager, + /* lazyViewCapture= */ toKotlinLazy(daggerLazyViewCapture), + /* isViewCaptureEnabled= */ enableViewCaptureTracing()); + } } diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java index 7b248eb876a8..e895d83758f7 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java @@ -102,6 +102,7 @@ import android.view.inputmethod.InputMethodManager; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.internal.accessibility.dialog.AccessibilityButtonChooserActivity; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.UiEvent; @@ -196,6 +197,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements private final Context mContext; private final Bundle mSavedState; private final WindowManager mWindowManager; + private final ViewCaptureAwareWindowManager mViewCaptureAwareWindowManager; private final AccessibilityManager mAccessibilityManager; private final DeviceProvisionedController mDeviceProvisionedController; private final StatusBarStateController mStatusBarStateController; @@ -556,6 +558,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements @Nullable Bundle savedState, @DisplayId Context context, @DisplayId WindowManager windowManager, + @DisplayId ViewCaptureAwareWindowManager viewCaptureAwareWindowManager, Lazy<AssistManager> assistManagerLazy, AccessibilityManager accessibilityManager, DeviceProvisionedController deviceProvisionedController, @@ -601,6 +604,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements mContext = context; mSavedState = savedState; mWindowManager = windowManager; + mViewCaptureAwareWindowManager = viewCaptureAwareWindowManager; mAccessibilityManager = accessibilityManager; mDeviceProvisionedController = deviceProvisionedController; mStatusBarStateController = statusBarStateController; @@ -721,7 +725,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements if (DEBUG) Log.v(TAG, "addNavigationBar: about to add " + mView); - mWindowManager.addView(mFrame, + mViewCaptureAwareWindowManager.addView(mFrame, getBarLayoutParams(mContext.getResources().getConfiguration().windowConfiguration .getRotation())); mDisplayId = mContext.getDisplayId(); @@ -764,7 +768,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements mCommandQueue.removeCallback(this); Trace.beginSection("NavigationBar#removeViewImmediate"); try { - mWindowManager.removeViewImmediate(mView.getRootView()); + mViewCaptureAwareWindowManager.removeViewImmediate(mView.getRootView()); } finally { Trace.endSection(); } @@ -866,7 +870,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements if (mOrientationHandle != null) { resetSecondaryHandle(); getBarTransitions().removeDarkIntensityListener(mOrientationHandleIntensityListener); - mWindowManager.removeView(mOrientationHandle); + mViewCaptureAwareWindowManager.removeView(mOrientationHandle); mOrientationHandle.getViewTreeObserver().removeOnGlobalLayoutListener( mOrientationHandleGlobalLayoutListener); } @@ -937,7 +941,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements mOrientationParams.setTitle("SecondaryHomeHandle" + mContext.getDisplayId()); mOrientationParams.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION | WindowManager.LayoutParams.PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT; - mWindowManager.addView(mOrientationHandle, mOrientationParams); + mViewCaptureAwareWindowManager.addView(mOrientationHandle, mOrientationParams); mOrientationHandle.setVisibility(View.GONE); logNavbarOrientation("initSecondaryHomeHandleForRotation"); 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/QSContainerImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java index 6c2008476720..1511f31a3f92 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java @@ -18,8 +18,6 @@ package com.android.systemui.qs; import static android.app.StatusBarManager.DISABLE2_QUICK_SETTINGS; -import static com.android.systemui.Flags.centralizedStatusBarHeightFix; - import android.content.Context; import android.graphics.Canvas; import android.graphics.Path; @@ -194,12 +192,7 @@ public class QSContainerImpl extends FrameLayout implements Dumpable { QuickStatusBarHeaderController quickStatusBarHeaderController) { int topPadding = QSUtils.getQsHeaderSystemIconsAreaHeight(mContext); if (!LargeScreenUtils.shouldUseLargeScreenShadeHeader(mContext.getResources())) { - topPadding = - centralizedStatusBarHeightFix() - ? LargeScreenHeaderHelper.getLargeScreenHeaderHeight(mContext) - : mContext.getResources() - .getDimensionPixelSize( - R.dimen.large_screen_shade_header_height); + topPadding = LargeScreenHeaderHelper.getLargeScreenHeaderHeight(mContext); } if (mQSPanelContainer != null) { mQSPanelContainer.setPaddingRelative( diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java index efa80a17b487..8fde52c910da 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java @@ -17,8 +17,6 @@ package com.android.systemui.qs; import static android.app.StatusBarManager.DISABLE2_QUICK_SETTINGS; import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; -import static com.android.systemui.Flags.centralizedStatusBarHeightFix; - import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; @@ -100,10 +98,7 @@ public class QuickStatusBarHeader extends FrameLayout { qqsLP.topMargin = mContext.getResources() .getDimensionPixelSize(R.dimen.qqs_layout_margin_top); } else { - qqsLP.topMargin = centralizedStatusBarHeightFix() - ? LargeScreenHeaderHelper.getLargeScreenHeaderHeight(mContext) - : mContext.getResources() - .getDimensionPixelSize(R.dimen.large_screen_shade_header_min_height); + qqsLP.topMargin = LargeScreenHeaderHelper.getLargeScreenHeaderHeight(mContext); } mHeaderQsPanel.setLayoutParams(qqsLP); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/UserSettingObserver.java b/packages/SystemUI/src/com/android/systemui/qs/UserSettingObserver.java index 1b34c33c9ca0..89be17beaba1 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/UserSettingObserver.java +++ b/packages/SystemUI/src/com/android/systemui/qs/UserSettingObserver.java @@ -19,6 +19,7 @@ package com.android.systemui.qs; import android.database.ContentObserver; import android.os.Handler; +import com.android.systemui.Flags; import com.android.systemui.statusbar.policy.Listenable; import com.android.systemui.util.settings.SecureSettings; import com.android.systemui.util.settings.SystemSettings; @@ -76,10 +77,20 @@ public abstract class UserSettingObserver extends ContentObserver implements Lis mListening = listening; if (listening) { mObservedValue = getValueFromProvider(); - mSettingsProxy.registerContentObserverForUserSync( - mSettingsProxy.getUriFor(mSettingName), false, this, mUserId); + if (Flags.qsRegisterSettingObserverOnBgThread()) { + mSettingsProxy.registerContentObserverForUserAsync( + mSettingsProxy.getUriFor(mSettingName), this, mUserId, () -> + mObservedValue = getValueFromProvider()); + } else { + mSettingsProxy.registerContentObserverForUserSync( + mSettingsProxy.getUriFor(mSettingName), false, this, mUserId); + } } else { - mSettingsProxy.unregisterContentObserverSync(this); + if (Flags.qsRegisterSettingObserverOnBgThread()) { + mSettingsProxy.unregisterContentObserverAsync(this); + } else { + mSettingsProxy.unregisterContentObserverSync(this); + } mObservedValue = mDefaultValue; } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/flags/QSComposeFragment.kt b/packages/SystemUI/src/com/android/systemui/qs/flags/QSComposeFragment.kt index 664d49607f89..8772c51e5bd8 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/flags/QSComposeFragment.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/flags/QSComposeFragment.kt @@ -33,7 +33,7 @@ object QSComposeFragment { /** Is the refactor enabled */ @JvmStatic inline val isEnabled - get() = Flags.qsUiRefactorComposeFragment() && NewQsUI.isEnabled + get() = Flags.qsUiRefactorComposeFragment() /** * Called to ensure code is only run when the flag is enabled. This protects users from the diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt index b18358cedde7..6dcdea973d51 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt @@ -40,15 +40,15 @@ constructor( @Application private val applicationScope: CoroutineScope ) { - private val largeTilesSpecs = + val largeTilesSpecs = preferencesInteractor.largeTilesSpecs .onEach { logChange(it) } .stateIn(applicationScope, SharingStarted.Eagerly, repo.defaultLargeTiles) fun isIconTile(spec: TileSpec): Boolean = !largeTilesSpecs.value.contains(spec) - fun resize(spec: TileSpec, toIcon: Boolean) { - if (toIcon) { + fun resize(spec: TileSpec) { + if (largeTilesSpecs.value.contains(spec)) { preferencesInteractor.setLargeTilesSpecs(largeTilesSpecs.value - spec) } else { preferencesInteractor.setLargeTilesSpecs(largeTilesSpecs.value + spec) diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractor.kt index 0fe79af06a54..874b3b0a4636 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractor.kt @@ -19,6 +19,7 @@ package com.android.systemui.qs.panels.domain.interactor import android.util.Log import com.android.systemui.dagger.SysUISingleton import com.android.systemui.qs.panels.shared.model.SizedTile +import com.android.systemui.qs.panels.shared.model.SizedTileImpl import com.android.systemui.qs.panels.shared.model.TileRow import com.android.systemui.qs.pipeline.shared.TileSpec import javax.inject.Inject @@ -38,17 +39,12 @@ constructor( override fun reconcileTiles(tiles: List<TileSpec>): List<TileSpec> { val newTiles: MutableList<TileSpec> = mutableListOf() val row = TileRow<TileSpec>(columns = gridSizeInteractor.columns.value) - val tilesQueue = + val tilesQueue: ArrayDeque<SizedTile<TileSpec>> = ArrayDeque( tiles.map { - SizedTile( + SizedTileImpl( it, - width = - if (iconTilesInteractor.isIconTile(it)) { - 1 - } else { - 2 - } + if (iconTilesInteractor.isIconTile(it)) 1 else 2, ) } ) diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/shared/model/TileRow.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/shared/model/TileRow.kt index 7e4381bbff03..17b73a250524 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/shared/model/TileRow.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/shared/model/TileRow.kt @@ -17,7 +17,17 @@ package com.android.systemui.qs.panels.shared.model /** Represents a tile of type [T] associated with a width */ -data class SizedTile<T>(val tile: T, val width: Int) +interface SizedTile<T> { + val tile: T + val width: Int + val isIcon: Boolean + get() = width == 1 +} + +data class SizedTileImpl<T>( + override val tile: T, + override val width: Int, +) : SizedTile<T> /** Represents a row of [SizedTile] with a maximum width of [columns] */ class TileRow<T>(private val columns: Int) { @@ -51,3 +61,26 @@ class TileRow<T>(private val columns: Int) { fun isFull(): Boolean = availableColumns == 0 } + +/** + * Converts a list of [SizedTile] to a sequence of rows based on the number of columns of the grid + */ +fun <T> splitInRowsSequence( + tiles: List<SizedTile<T>>, + columns: Int, +): Sequence<List<SizedTile<T>>> = sequence { + val row = TileRow<T>(columns) + for (tile in tiles) { + check(tile.width <= columns) + if (!row.maybeAddTile(tile)) { + // Couldn't add tile to previous row, create a row with the current tiles + // and start a new one + yield(row.tiles) + row.clear() + row.maybeAddTile(tile) + } + } + if (row.tiles.isNotEmpty()) { + yield(row.tiles) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt index 71deeb61b9e9..2c578130e920 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt @@ -29,13 +29,14 @@ import androidx.compose.ui.draganddrop.DragAndDropEvent import androidx.compose.ui.draganddrop.DragAndDropTarget import androidx.compose.ui.draganddrop.DragAndDropTransferData import androidx.compose.ui.draganddrop.mimeTypes +import com.android.systemui.qs.panels.shared.model.SizedTile import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel import com.android.systemui.qs.pipeline.shared.TileSpec @Composable fun rememberDragAndDropState(listState: EditTileListState): DragAndDropState { - val sourceSpec: MutableState<EditTileViewModel?> = remember { mutableStateOf(null) } - return remember(listState) { DragAndDropState(sourceSpec, listState) } + val draggedCell: MutableState<SizedTile<EditTileViewModel>?> = remember { mutableStateOf(null) } + return remember(listState) { DragAndDropState(draggedCell, listState) } } /** @@ -43,37 +44,37 @@ fun rememberDragAndDropState(listState: EditTileListState): DragAndDropState { * drop events. */ class DragAndDropState( - val sourceSpec: MutableState<EditTileViewModel?>, - private val listState: EditTileListState + val draggedCell: MutableState<SizedTile<EditTileViewModel>?>, + private val listState: EditTileListState, ) { val dragInProgress: Boolean - get() = sourceSpec.value != null + get() = draggedCell.value != null /** Returns index of the dragged tile if it's present in the list. Returns -1 if not. */ fun currentPosition(): Int { - return sourceSpec.value?.let { listState.indexOf(it.tileSpec) } ?: -1 + return draggedCell.value?.let { listState.indexOf(it.tile.tileSpec) } ?: -1 } fun isMoving(tileSpec: TileSpec): Boolean { - return sourceSpec.value?.let { it.tileSpec == tileSpec } ?: false + return draggedCell.value?.let { it.tile.tileSpec == tileSpec } ?: false } - fun onStarted(tile: EditTileViewModel) { - sourceSpec.value = tile + fun onStarted(cell: SizedTile<EditTileViewModel>) { + draggedCell.value = cell } fun onMoved(targetSpec: TileSpec) { - sourceSpec.value?.let { listState.move(it, targetSpec) } + draggedCell.value?.let { listState.move(it, targetSpec) } } fun movedOutOfBounds() { // Removing the tiles from the current tile grid if it moves out of bounds. This clears // the spacer and makes it apparent that dropping the tile at that point would remove it. - sourceSpec.value?.let { listState.remove(it.tileSpec) } + draggedCell.value?.let { listState.remove(it.tile.tileSpec) } } fun onDrop() { - sourceSpec.value = null + draggedCell.value = null } } @@ -97,8 +98,8 @@ fun Modifier.dragAndDropTile( remember(dragAndDropState) { object : DragAndDropTarget { override fun onDrop(event: DragAndDropEvent): Boolean { - return dragAndDropState.sourceSpec.value?.let { - onDrop(it.tileSpec, dragAndDropState.currentPosition()) + return dragAndDropState.draggedCell.value?.let { + onDrop(it.tile.tileSpec, dragAndDropState.currentPosition()) dragAndDropState.onDrop() true } ?: false @@ -112,7 +113,7 @@ fun Modifier.dragAndDropTile( return dragAndDropTarget( shouldStartDragAndDrop = { event -> event.mimeTypes().contains(QsDragAndDrop.TILESPEC_MIME_TYPE) && - dragAndDropState.sourceSpec.value?.let { acceptDrops(it.tileSpec) } ?: false + dragAndDropState.draggedCell.value?.let { acceptDrops(it.tile.tileSpec) } ?: false }, target = target, ) @@ -134,8 +135,8 @@ fun Modifier.dragAndDropRemoveZone( remember(dragAndDropState) { object : DragAndDropTarget { override fun onDrop(event: DragAndDropEvent): Boolean { - return dragAndDropState.sourceSpec.value?.let { - onDrop(it.tileSpec) + return dragAndDropState.draggedCell.value?.let { + onDrop(it.tile.tileSpec) dragAndDropState.onDrop() true } ?: false @@ -176,8 +177,8 @@ fun Modifier.dragAndDropTileList( } override fun onDrop(event: DragAndDropEvent): Boolean { - return dragAndDropState.sourceSpec.value?.let { - onDrop(it.tileSpec, dragAndDropState.currentPosition()) + return dragAndDropState.draggedCell.value?.let { + onDrop(it.tile.tileSpec, dragAndDropState.currentPosition()) dragAndDropState.onDrop() true } ?: false @@ -188,23 +189,23 @@ fun Modifier.dragAndDropTileList( target = target, shouldStartDragAndDrop = { event -> event.mimeTypes().contains(QsDragAndDrop.TILESPEC_MIME_TYPE) && - dragAndDropState.sourceSpec.value?.let { acceptDrops(it.tileSpec) } ?: false + dragAndDropState.draggedCell.value?.let { acceptDrops(it.tile.tileSpec) } ?: false }, ) } fun Modifier.dragAndDropTileSource( - tile: EditTileViewModel, + sizedTile: SizedTile<EditTileViewModel>, onTap: (TileSpec) -> Unit, onDoubleTap: (TileSpec) -> Unit, dragAndDropState: DragAndDropState ): Modifier { return dragAndDropSource { detectTapGestures( - onTap = { onTap(tile.tileSpec) }, - onDoubleTap = { onDoubleTap(tile.tileSpec) }, + onTap = { onTap(sizedTile.tile.tileSpec) }, + onDoubleTap = { onDoubleTap(sizedTile.tile.tileSpec) }, onLongPress = { - dragAndDropState.onStarted(tile) + dragAndDropState.onStarted(sizedTile) // The tilespec from the ClipData transferred isn't actually needed as we're moving // a tile within the same application. We're using a custom MIME type to limit the @@ -214,7 +215,7 @@ fun Modifier.dragAndDropTileSource( ClipData( QsDragAndDrop.CLIPDATA_LABEL, arrayOf(QsDragAndDrop.TILESPEC_MIME_TYPE), - ClipData.Item(tile.tileSpec.spec) + ClipData.Item(sizedTile.tile.tileSpec.spec) ) ) ) diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt index e0fed2885799..fa3008e3f292 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt @@ -20,22 +20,23 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.toMutableStateList +import com.android.systemui.qs.panels.shared.model.SizedTile import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel import com.android.systemui.qs.pipeline.shared.TileSpec @Composable fun rememberEditListState( - tiles: List<EditTileViewModel>, + tiles: List<SizedTile<EditTileViewModel>>, ): EditTileListState { return remember(tiles) { EditTileListState(tiles) } } /** Holds the temporary state of the tile list during a drag movement where we move tiles around. */ -class EditTileListState(tiles: List<EditTileViewModel>) { - val tiles: SnapshotStateList<EditTileViewModel> = tiles.toMutableStateList() +class EditTileListState(tiles: List<SizedTile<EditTileViewModel>>) { + val tiles: SnapshotStateList<SizedTile<EditTileViewModel>> = tiles.toMutableStateList() - fun move(tile: EditTileViewModel, target: TileSpec) { - val fromIndex = indexOf(tile.tileSpec) + fun move(sizedTile: SizedTile<EditTileViewModel>, target: TileSpec) { + val fromIndex = indexOf(sizedTile.tile.tileSpec) val toIndex = indexOf(target) if (toIndex == -1 || fromIndex == toIndex) { @@ -44,7 +45,7 @@ class EditTileListState(tiles: List<EditTileViewModel>) { if (fromIndex == -1) { // If tile isn't in the list, simply insert it - tiles.add(toIndex, tile) + tiles.add(toIndex, sizedTile) } else { // If tile is present in the list, move it tiles.apply { add(toIndex, removeAt(fromIndex)) } @@ -52,10 +53,10 @@ class EditTileListState(tiles: List<EditTileViewModel>) { } fun remove(tileSpec: TileSpec) { - tiles.removeIf { it.tileSpec == tileSpec } + tiles.removeIf { it.tile.tileSpec == tileSpec } } fun indexOf(tileSpec: TileSpec): Int { - return tiles.indexOfFirst { it.tileSpec == tileSpec } + return tiles.indexOfFirst { it.tile.tileSpec == tileSpec } } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt index add830e9760d..bd925fee2800 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt @@ -22,12 +22,12 @@ import androidx.compose.foundation.lazy.grid.GridItemSpan import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.res.dimensionResource import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.qs.panels.shared.model.SizedTile +import com.android.systemui.qs.panels.shared.model.SizedTileImpl import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel import com.android.systemui.qs.panels.ui.viewmodel.FixedColumnsSizeViewModel import com.android.systemui.qs.panels.ui.viewmodel.IconTilesViewModel @@ -56,13 +56,14 @@ constructor( onDispose { tiles.forEach { it.stopListening(token) } } } val columns by gridSizeViewModel.columns.collectAsStateWithLifecycle() + val sizedTiles = tiles.map { SizedTileImpl(it, it.spec.width()) } TileLazyGrid(modifier = modifier, columns = GridCells.Fixed(columns)) { - items(tiles.size, span = { index -> GridItemSpan(tiles[index].spec.width()) }) { index - -> + items(sizedTiles.size, span = { index -> GridItemSpan(sizedTiles[index].width) }) { + index -> Tile( - tile = tiles[index], - iconOnly = iconTilesViewModel.isIconTile(tiles[index].spec), + tile = sizedTiles[index].tile, + iconOnly = iconTilesViewModel.isIconTile(sizedTiles[index].tile.spec), modifier = Modifier.height(dimensionResource(id = R.dimen.qs_tile_height)) ) } @@ -77,13 +78,21 @@ constructor( onRemoveTile: (TileSpec) -> Unit, ) { val columns by gridSizeViewModel.columns.collectAsStateWithLifecycle() - val isIcon: (TileSpec) -> Boolean by rememberUpdatedState { tileSpec -> - iconTilesViewModel.isIconTile(tileSpec) - } + val largeTiles by iconTilesViewModel.largeTiles.collectAsStateWithLifecycle() + + // Non-current tiles should always be displayed as icon tiles. + val sizedTiles = + remember(tiles, largeTiles) { + tiles.map { + SizedTileImpl( + it, + if (!it.isCurrent || !largeTiles.contains(it.tileSpec)) 1 else 2, + ) + } + } DefaultEditTileGrid( - tiles = tiles, - isIconOnly = isIcon, + sizedTiles = sizedTiles, columns = columns, modifier = modifier, onAddTile = onAddTile, @@ -99,7 +108,7 @@ constructor( ): List<List<TileViewModel>> { return PaginatableGridLayout.splitInRows( - tiles.map { SizedTile(it, it.spec.width()) }, + tiles.map { SizedTileImpl(it, it.spec.width()) }, columns, ) .chunked(rows) diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt index 9b4d10f27f9e..af3803b6ff34 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt @@ -52,7 +52,7 @@ fun QuickQuickSettings( ) { index -> Tile( tile = tiles[index], - iconOnly = sizedTiles[index].width == 1, + iconOnly = sizedTiles[index].isIcon, modifier = Modifier.height(dimensionResource(id = R.dimen.qs_tile_height)) ) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt index cb9d0f6a790e..7e6ccd635a96 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt @@ -53,7 +53,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.GridItemSpan import androidx.compose.foundation.lazy.grid.LazyGridScope import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.rememberScrollState @@ -98,7 +97,8 @@ import com.android.systemui.common.ui.compose.Icon import com.android.systemui.common.ui.compose.load import com.android.systemui.plugins.qs.QSTile import com.android.systemui.qs.panels.shared.model.SizedTile -import com.android.systemui.qs.panels.shared.model.TileRow +import com.android.systemui.qs.panels.ui.model.TileGridCell +import com.android.systemui.qs.panels.ui.model.toTileGridCells import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel import com.android.systemui.qs.panels.ui.viewmodel.toUiState @@ -107,12 +107,10 @@ import com.android.systemui.qs.pipeline.shared.TileSpec import com.android.systemui.qs.tileimpl.QSTileImpl import com.android.systemui.res.R import java.util.function.Supplier -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay object TileType -@OptIn(ExperimentalCoroutinesApi::class) @Composable fun Tile( tile: TileViewModel, @@ -286,15 +284,14 @@ fun TileLazyGrid( @Composable fun DefaultEditTileGrid( - tiles: List<EditTileViewModel>, - isIconOnly: (TileSpec) -> Boolean, + sizedTiles: List<SizedTile<EditTileViewModel>>, columns: Int, modifier: Modifier, onAddTile: (TileSpec, Int) -> Unit, onRemoveTile: (TileSpec) -> Unit, - onResize: (TileSpec, Boolean) -> Unit, + onResize: (TileSpec) -> Unit, ) { - val (currentTiles, otherTiles) = tiles.partition { it.isCurrent } + val (currentTiles, otherTiles) = sizedTiles.partition { it.tile.isCurrent } val currentListState = rememberEditListState(currentTiles) val dragAndDropState = rememberDragAndDropState(currentListState) @@ -304,9 +301,6 @@ fun DefaultEditTileGrid( val onDropAdd: (TileSpec, Int) -> Unit by rememberUpdatedState { tileSpec, position -> onAddTile(tileSpec, position) } - val onDoubleTap: (TileSpec) -> Unit by rememberUpdatedState { tileSpec -> - onResize(tileSpec, !isIconOnly(tileSpec)) - } val tilePadding = dimensionResource(R.dimen.qs_tile_margin_vertical) CompositionLocalProvider(LocalOverscrollConfiguration provides null) { @@ -332,9 +326,8 @@ fun DefaultEditTileGrid( currentListState.tiles, columns, tilePadding, - isIconOnly, onRemoveTile, - onDoubleTap, + onResize, dragAndDropState, onDropAdd, ) @@ -422,48 +415,32 @@ private fun CurrentTilesContainer(content: @Composable () -> Unit) { @Composable private fun CurrentTilesGrid( - tiles: List<EditTileViewModel>, + tiles: List<SizedTile<EditTileViewModel>>, columns: Int, tilePadding: Dp, - isIconOnly: (TileSpec) -> Boolean, onClick: (TileSpec) -> Unit, - onDoubleTap: (TileSpec) -> Unit, + onResize: (TileSpec) -> Unit, dragAndDropState: DragAndDropState, onDrop: (TileSpec, Int) -> Unit ) { - val tileHeight = tileHeight() - val currentRows = - remember(tiles) { - calculateRows( - tiles.map { - SizedTile( - it, - if (isIconOnly(it.tileSpec)) { - 1 - } else { - 2 - } - ) - }, - columns - ) - } - val currentGridHeight = gridHeight(currentRows, tileHeight, tilePadding) // Current tiles CurrentTilesContainer { + val cells = tiles.toTileGridCells(columns) + val tileHeight = tileHeight() + val totalRows = cells.lastOrNull()?.row ?: 0 + val totalHeight = gridHeight(totalRows + 1, tileHeight, tilePadding) TileLazyGrid( modifier = - Modifier.height(currentGridHeight) + Modifier.height(totalHeight) .dragAndDropTileList(dragAndDropState, { true }, onDrop), columns = GridCells.Fixed(columns) ) { editTiles( - tiles, + cells, ClickAction.REMOVE, onClick, - isIconOnly, dragAndDropState, - onDoubleTap = onDoubleTap, + onResize = onResize, indicatePosition = true, acceptDrops = { true }, onDrop = onDrop, @@ -474,13 +451,15 @@ private fun CurrentTilesGrid( @Composable private fun AvailableTileGrid( - tiles: List<EditTileViewModel>, + tiles: List<SizedTile<EditTileViewModel>>, columns: Int, tilePadding: Dp, onClick: (TileSpec) -> Unit, dragAndDropState: DragAndDropState, ) { - val (otherTilesStock, otherTilesCustom) = tiles.partition { it.appName == null } + // Available tiles aren't visible during drag and drop, so the row isn't needed + val (otherTilesStock, otherTilesCustom) = + tiles.map { TileGridCell(it, 0) }.partition { it.tile.appName == null } val availableTileHeight = tileHeight(true) val availableGridHeight = gridHeight(tiles.size, availableTileHeight, columns, tilePadding) @@ -493,7 +472,6 @@ private fun AvailableTileGrid( otherTilesStock, ClickAction.ADD, onClick, - isIconOnly = { true }, dragAndDropState = dragAndDropState, acceptDrops = { false }, showLabels = true, @@ -502,7 +480,6 @@ private fun AvailableTileGrid( otherTilesCustom, ClickAction.ADD, onClick, - isIconOnly = { true }, dragAndDropState = dragAndDropState, acceptDrops = { false }, showLabels = true, @@ -519,52 +496,27 @@ fun gridHeight(rows: Int, tileHeight: Dp, padding: Dp): Dp { return ((tileHeight + padding) * rows) - padding } -private fun calculateRows(tiles: List<SizedTile<EditTileViewModel>>, columns: Int): Int { - val row = TileRow<EditTileViewModel>(columns) - var count = 0 - - for (tile in tiles) { - if (row.maybeAddTile(tile)) { - if (row.isFull()) { - // Row is full, no need to stretch tiles - count += 1 - row.clear() - } - } else { - count += 1 - row.clear() - row.maybeAddTile(tile) - } - } - if (row.tiles.isNotEmpty()) { - count += 1 - } - return count -} - fun LazyGridScope.editTiles( - tiles: List<EditTileViewModel>, + cells: List<TileGridCell>, clickAction: ClickAction, onClick: (TileSpec) -> Unit, - isIconOnly: (TileSpec) -> Boolean, dragAndDropState: DragAndDropState, acceptDrops: (TileSpec) -> Boolean, - onDoubleTap: (TileSpec) -> Unit = {}, + onResize: (TileSpec) -> Unit = {}, onDrop: (TileSpec, Int) -> Unit = { _, _ -> }, showLabels: Boolean = false, indicatePosition: Boolean = false, ) { items( - count = tiles.size, - key = { tiles[it].tileSpec.spec }, - span = { GridItemSpan(if (isIconOnly(tiles[it].tileSpec)) 1 else 2) }, + count = cells.size, + key = { cells[it].key }, + span = { cells[it].span }, contentType = { TileType } ) { index -> - val viewModel = tiles[index] - val iconOnly = isIconOnly(viewModel.tileSpec) - val tileHeight = tileHeight(iconOnly && showLabels) + val cell = cells[index] + val tileHeight = tileHeight(cell.isIcon && showLabels) - if (!dragAndDropState.isMoving(viewModel.tileSpec)) { + if (!dragAndDropState.isMoving(cell.tile.tileSpec)) { val onClickActionName = when (clickAction) { ClickAction.ADD -> @@ -579,8 +531,8 @@ fun LazyGridScope.editTiles( "" } EditTile( - tileViewModel = viewModel, - iconOnly = iconOnly, + tileViewModel = cell.tile, + iconOnly = cell.isIcon, showLabels = showLabels, modifier = Modifier.height(tileHeight) @@ -589,11 +541,11 @@ fun LazyGridScope.editTiles( onClick(onClickActionName) { false } this.stateDescription = stateDescription } - .dragAndDropTile(dragAndDropState, viewModel.tileSpec, acceptDrops, onDrop) + .dragAndDropTile(dragAndDropState, cell.tile.tileSpec, acceptDrops, onDrop) .dragAndDropTileSource( - viewModel, + cell, onClick, - onDoubleTap, + onResize, dragAndDropState, ) ) diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/model/TileGridCell.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/model/TileGridCell.kt new file mode 100644 index 000000000000..c241fd87d9d5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/model/TileGridCell.kt @@ -0,0 +1,52 @@ +/* + * 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.panels.ui.model + +import androidx.compose.foundation.lazy.grid.GridItemSpan +import androidx.compose.runtime.Immutable +import com.android.systemui.qs.panels.shared.model.SizedTile +import com.android.systemui.qs.panels.shared.model.splitInRowsSequence +import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel + +/** + * Represents a [EditTileViewModel] from a grid associated with a tile format and the row it's + * positioned at + */ +@Immutable +data class TileGridCell( + override val tile: EditTileViewModel, + val row: Int, + val key: String = "${tile.tileSpec.spec}-$row", + override val width: Int, +) : SizedTile<EditTileViewModel> { + constructor( + sizedTile: SizedTile<EditTileViewModel>, + row: Int + ) : this( + tile = sizedTile.tile, + row = row, + width = sizedTile.width, + ) + + val span = GridItemSpan(width) +} + +fun List<SizedTile<EditTileViewModel>>.toTileGridCells(columns: Int): List<TileGridCell> { + return splitInRowsSequence(this, columns) + .flatMapIndexed { index, sizedTiles -> sizedTiles.map { TileGridCell(it, index) } } + .toList() +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/IconTilesViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/IconTilesViewModel.kt index 8d2d74af5835..b604e18b1e76 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/IconTilesViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/IconTilesViewModel.kt @@ -20,17 +20,22 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.qs.panels.domain.interactor.IconTilesInteractor import com.android.systemui.qs.pipeline.shared.TileSpec import javax.inject.Inject +import kotlinx.coroutines.flow.StateFlow interface IconTilesViewModel { + val largeTiles: StateFlow<Set<TileSpec>> + fun isIconTile(spec: TileSpec): Boolean - fun resize(spec: TileSpec, toIcon: Boolean) + fun resize(spec: TileSpec) } @SysUISingleton class IconTilesViewModelImpl @Inject constructor(private val interactor: IconTilesInteractor) : IconTilesViewModel { + override val largeTiles = interactor.largeTilesSpecs + override fun isIconTile(spec: TileSpec): Boolean = interactor.isIconTile(spec) - override fun resize(spec: TileSpec, toIcon: Boolean) = interactor.resize(spec, toIcon) + override fun resize(spec: TileSpec) = interactor.resize(spec) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModel.kt index bb004946a4d1..eee905f9f894 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModel.kt @@ -20,7 +20,8 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.qs.panels.domain.interactor.QuickQuickSettingsRowInteractor import com.android.systemui.qs.panels.shared.model.SizedTile -import com.android.systemui.qs.panels.shared.model.TileRow +import com.android.systemui.qs.panels.shared.model.SizedTileImpl +import com.android.systemui.qs.panels.shared.model.splitInRowsSequence import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor import com.android.systemui.qs.pipeline.shared.TileSpec import javax.inject.Inject @@ -59,7 +60,12 @@ constructor( .flatMapLatest { columns -> tilesInteractor.currentTiles.combine(rows, ::Pair).mapLatest { (tiles, rows) -> tiles - .map { SizedTile(TileViewModel(it.tile, it.spec), it.spec.width) } + .map { + SizedTileImpl( + TileViewModel(it.tile, it.spec), + it.spec.width, + ) + } .let { splitInRowsSequence(it, columns).take(rows).toList().flatten() } } } @@ -67,7 +73,12 @@ constructor( applicationScope, SharingStarted.WhileSubscribed(), tilesInteractor.currentTiles.value - .map { SizedTile(TileViewModel(it.tile, it.spec), it.spec.width) } + .map { + SizedTileImpl( + TileViewModel(it.tile, it.spec), + it.spec.width, + ) + } .let { splitInRowsSequence(it, columns.value).take(rows.value).toList().flatten() } @@ -75,26 +86,4 @@ constructor( private val TileSpec.width: Int get() = if (iconTilesViewModel.isIconTile(this)) 1 else 2 - - companion object { - private fun splitInRowsSequence( - tiles: List<SizedTile<TileViewModel>>, - columns: Int, - ): Sequence<List<SizedTile<TileViewModel>>> = sequence { - val row = TileRow<TileViewModel>(columns) - for (tile in tiles) { - check(tile.width <= columns) - if (!row.maybeAddTile(tile)) { - // Couldn't add tile to previous row, create a row with the current tiles - // and start a new one - yield(row.tiles) - row.clear() - row.maybeAddTile(tile) - } - } - if (row.tiles.isNotEmpty()) { - yield(row.tiles) - } - } - } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/airplane/domain/AirplaneModeMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/airplane/domain/AirplaneModeMapper.kt index 9b8dba166274..9fb1d46c4241 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/airplane/domain/AirplaneModeMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/airplane/domain/AirplaneModeMapper.kt @@ -37,19 +37,22 @@ constructor( override fun map(config: QSTileConfig, data: AirplaneModeTileModel): QSTileState = QSTileState.build(resources, theme, config.uiConfig) { - val icon = + iconRes = + if (data.isEnabled) { + R.drawable.qs_airplane_icon_on + } else { + R.drawable.qs_airplane_icon_off + } + + icon = { Icon.Loaded( resources.getDrawable( - if (data.isEnabled) { - R.drawable.qs_airplane_icon_on - } else { - R.drawable.qs_airplane_icon_off - }, + iconRes!!, theme, ), contentDescription = null ) - this.icon = { icon } + } if (data.isEnabled) { activationState = QSTileState.ActivationState.ACTIVE secondaryLabel = resources.getStringArray(R.array.tile_states_airplane)[2] diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/CustomTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/CustomTileMapper.kt index 875079cae8eb..984228d80b7f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/CustomTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/CustomTileMapper.kt @@ -41,16 +41,25 @@ constructor( ) : QSTileDataToStateMapper<CustomTileDataModel> { override fun map(config: QSTileConfig, data: CustomTileDataModel): QSTileState { - val userContext = context.createContextAsUser(UserHandle(data.user.identifier), 0) + val userContext = + try { + context.createContextAsUser(UserHandle(data.user.identifier), 0) + } catch (exception: IllegalStateException) { + null + } val iconResult = - getIconProvider( - userContext = userContext, - icon = data.tile.icon, - callingAppUid = data.callingAppUid, - packageName = data.componentName.packageName, - defaultIcon = data.defaultTileIcon, - ) + if (userContext != null) { + getIconProvider( + userContext = userContext, + icon = data.tile.icon, + callingAppUid = data.callingAppUid, + packageName = data.componentName.packageName, + defaultIcon = data.defaultTileIcon, + ) + } else { + IconResult({ null }, true) + } return QSTileState.build(iconResult.iconProvider, data.tile.label) { var tileState: Int = data.tile.state @@ -61,7 +70,7 @@ constructor( icon = iconResult.iconProvider activationState = if (iconResult.failedToLoad) { - QSTileState.ActivationState.INACTIVE + QSTileState.ActivationState.UNAVAILABLE } else { QSTileState.ActivationState.valueOf(tileState) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt index eec5d3d915f8..204ead3fe29c 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt @@ -79,6 +79,7 @@ constructor( flowOf( InternetTileModel.Active( secondaryTitle = secondary, + iconId = wifiIcon.icon.res, icon = Icon.Loaded(context.getDrawable(wifiIcon.icon.res)!!, null), stateDescription = wifiIcon.contentDescription, contentDescription = ContentDescription.Loaded("$internetLabel,$secondary"), 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/recordissue/CustomTraceSettingsDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/recordissue/CustomTraceSettingsDialogDelegate.kt index 56270cef7afd..a42bd0a0ab1c 100644 --- a/packages/SystemUI/src/com/android/systemui/recordissue/CustomTraceSettingsDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/recordissue/CustomTraceSettingsDialogDelegate.kt @@ -81,9 +81,11 @@ class CustomTraceSettingsDialogDelegate( } setOnClickListener { showCategorySelector(this) } } + val attachToBRLabel = context.getString(T.string.attach_to_bug_report) requireViewById<Switch>(R.id.attach_to_bugreport_switch).apply { isChecked = builder.attachToBugreport setOnCheckedChangeListener { _, isChecked -> builder.attachToBugreport = isChecked } + contentDescription = attachToBRLabel } requireViewById<TextView>(R.id.cpu_buffer_size).setupSingleChoiceText( T.array.buffer_size_values, @@ -111,6 +113,7 @@ class CustomTraceSettingsDialogDelegate( ) { builder.maxLongTraceDurationMinutes = it } + val longTracesLabel = context.getString(T.string.long_traces) requireViewById<Switch>(R.id.long_traces_switch).apply { isChecked = builder.longTrace val disabledAlpha by lazy { getDisabledAlpha(context) } @@ -127,23 +130,24 @@ class CustomTraceSettingsDialogDelegate( longTraceDurationText.alpha = newAlpha longTraceSizeText.alpha = newAlpha } + contentDescription = longTracesLabel } + val winscopeLabel = context.getString(T.string.winscope_tracing) requireViewById<Switch>(R.id.winscope_switch).apply { isChecked = builder.winscope setOnCheckedChangeListener { _, isChecked -> builder.winscope = isChecked } + contentDescription = winscopeLabel } + val debuggableAppsLabel = context.getString(T.string.trace_debuggable_applications) requireViewById<Switch>(R.id.trace_debuggable_apps_switch).apply { isChecked = builder.apps setOnCheckedChangeListener { _, isChecked -> builder.apps = isChecked } + contentDescription = debuggableAppsLabel } - requireViewById<TextView>(R.id.long_traces_switch_label).text = - context.getString(T.string.long_traces) - requireViewById<TextView>(R.id.debuggable_apps_switch_label).text = - context.getString(T.string.trace_debuggable_applications) - requireViewById<TextView>(R.id.winscope_switch_label).text = - context.getString(T.string.winscope_tracing) - requireViewById<TextView>(R.id.attach_to_bugreport_switch_label).text = - context.getString(T.string.attach_to_bug_report) + requireViewById<TextView>(R.id.long_traces_switch_label).text = longTracesLabel + requireViewById<TextView>(R.id.debuggable_apps_switch_label).text = debuggableAppsLabel + requireViewById<TextView>(R.id.winscope_switch_label).text = winscopeLabel + requireViewById<TextView>(R.id.attach_to_bugreport_switch_label).text = attachToBRLabel } } diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt index 98a61df4ca55..863a899b6f4c 100644 --- a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt +++ b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt @@ -24,6 +24,7 @@ import android.content.res.Resources import android.net.Uri import android.os.Handler import android.os.UserHandle +import android.util.Log import com.android.internal.logging.UiEventLogger import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.dagger.qualifiers.LongRunning @@ -71,6 +72,7 @@ constructor( override fun provideRecordingServiceStrings(): RecordingServiceStrings = IrsStrings(resources) override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + Log.d(getTag(), "handling action: ${intent?.action}") when (intent?.action) { ACTION_START -> { bgExecutor.execute { @@ -95,7 +97,7 @@ constructor( bgExecutor.execute { mNotificationManager.cancelAsUser( null, - mNotificationId, + intent.getIntExtra(EXTRA_NOTIFICATION_ID, mNotificationId), UserHandle(mUserContextTracker.userContext.userId) ) 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..2ffcad4d1fc9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/scene/OWNERS @@ -0,0 +1,13 @@ +set noparent + +# Bug component: 1215786 + +justinweir@google.com +nijamkin@google.com + +# SysUI Dr No's. +# Don't send reviews here. +cinek@google.com +dsandler@android.com +juliacr@google.com +pixel@google.com diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt index 5b5013352c29..3ec088c66e10 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt @@ -25,6 +25,7 @@ import com.android.internal.logging.UiEventLogger import com.android.systemui.CoreStartable import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor import com.android.systemui.bouncer.domain.interactor.BouncerInteractor import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor import com.android.systemui.bouncer.shared.logging.BouncerUiEvent @@ -132,6 +133,7 @@ constructor( private val keyguardEnabledInteractor: KeyguardEnabledInteractor, private val dismissCallbackRegistry: DismissCallbackRegistry, private val statusBarStateController: SysuiStatusBarStateController, + private val alternateBouncerInteractor: AlternateBouncerInteractor, ) : CoreStartable { private val centralSurfaces: CentralSurfaces? get() = centralSurfacesOptLazy.get().getOrNull() @@ -152,6 +154,7 @@ constructor( handleKeyguardEnabledness() notifyKeyguardDismissCallbacks() refreshLockscreenEnabled() + handleHideAlternateBouncerOnTransitionToGone() } else { sceneLogger.logFrameworkEnabled( isEnabled = false, @@ -228,13 +231,16 @@ constructor( }, headsUpInteractor.isHeadsUpOrAnimatingAway, occlusionInteractor.invisibleDueToOcclusion, + alternateBouncerInteractor.isVisible, ) { visibilityForTransitionState, isHeadsUpOrAnimatingAway, invisibleDueToOcclusion, + isAlternateBouncerVisible, -> when { isHeadsUpOrAnimatingAway -> true to "showing a HUN" + isAlternateBouncerVisible -> true to "showing alternate bouncer" invisibleDueToOcclusion -> false to "invisible due to occlusion" else -> visibilityForTransitionState } @@ -351,7 +357,9 @@ constructor( ) } val isOnLockscreen = renderedScenes.contains(Scenes.Lockscreen) - val isOnBouncer = renderedScenes.contains(Scenes.Bouncer) + val isOnBouncer = + renderedScenes.contains(Scenes.Bouncer) || + alternateBouncerInteractor.isVisibleState() if (!deviceUnlockStatus.isUnlocked) { return@mapNotNull if (isOnLockscreen || isOnBouncer) { // Already on lockscreen or bouncer, no need to change scenes. @@ -379,12 +387,13 @@ constructor( !statusBarStateController.leaveOpenOnKeyguardHide() ) { Scenes.Gone to - "device was unlocked in Bouncer scene and shade" + + "device was unlocked with bouncer showing and shade" + " didn't need to be left open" } else { val prevScene = previousScene.value (prevScene ?: Scenes.Gone) to - "device was unlocked in Bouncer scene, from sceneKey=$prevScene" + "device was unlocked with bouncer showing," + + " from sceneKey=$prevScene" } isOnLockscreen -> // The lockscreen should be dismissed automatically in 2 scenarios: @@ -776,4 +785,14 @@ constructor( .collectLatest { deviceEntryInteractor.refreshLockscreenEnabled() } } } + + private fun handleHideAlternateBouncerOnTransitionToGone() { + applicationScope.launch { + sceneInteractor.transitionState + .map { it.isIdle(Scenes.Gone) } + .distinctUntilChanged() + .filter { it } + .collectLatest { alternateBouncerInteractor.hide() } + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt index bccbb1130bcc..f6924f222e11 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt @@ -5,15 +5,18 @@ import android.util.AttributeSet import android.view.MotionEvent import android.view.View import android.view.WindowInsets +import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerDependencies import com.android.systemui.scene.shared.model.Scene import com.android.systemui.scene.shared.model.SceneContainerConfig import com.android.systemui.scene.shared.model.SceneDataSourceDelegator import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel import com.android.systemui.shade.TouchLogger import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow /** A root view of the main SysUI window that supports scenes. */ +@ExperimentalCoroutinesApi class SceneWindowRootView( context: Context, attrs: AttributeSet?, @@ -35,6 +38,7 @@ class SceneWindowRootView( scenes: Set<Scene>, layoutInsetController: LayoutInsetsController, sceneDataSourceDelegator: SceneDataSourceDelegator, + alternateBouncerDependencies: AlternateBouncerDependencies, ) { this.viewModel = viewModel setLayoutInsetsController(layoutInsetController) @@ -49,6 +53,7 @@ class SceneWindowRootView( super.setVisibility(if (isVisible) View.VISIBLE else View.INVISIBLE) }, dataSourceDelegator = sceneDataSourceDelegator, + alternateBouncerDependencies = alternateBouncerDependencies, ) } diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt index d31d6f4137c1..73a8e4c24578 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt @@ -37,6 +37,8 @@ import com.android.internal.policy.ScreenDecorationsUtils import com.android.systemui.common.ui.compose.windowinsets.CutoutLocation import com.android.systemui.common.ui.compose.windowinsets.DisplayCutout import com.android.systemui.common.ui.compose.windowinsets.ScreenDecorProvider +import com.android.systemui.keyguard.ui.composable.AlternateBouncer +import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerDependencies import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.res.R import com.android.systemui.scene.shared.flag.SceneContainerFlag @@ -48,12 +50,14 @@ import com.android.systemui.scene.ui.composable.SceneContainer import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +@ExperimentalCoroutinesApi object SceneWindowRootViewBinder { /** Binds between the view and view-model pertaining to a specific scene container. */ @@ -66,6 +70,7 @@ object SceneWindowRootViewBinder { scenes: Set<Scene>, onVisibilityChangedInternal: (isVisible: Boolean) -> Unit, dataSourceDelegator: SceneDataSourceDelegator, + alternateBouncerDependencies: AlternateBouncerDependencies, ) { val unsortedSceneByKey: Map<SceneKey, Scene> = scenes.associateBy { scene -> scene.key } val sortedSceneByKey: Map<SceneKey, Scene> = buildMap { @@ -120,6 +125,14 @@ object SceneWindowRootViewBinder { sharedNotificationContainer ) view.addView(sharedNotificationContainer) + + // TODO (b/358354906): use an overlay for the alternate bouncer + view.addView( + createAlternateBouncerView( + context = view.context, + alternateBouncerDependencies = alternateBouncerDependencies, + ) + ) } launch { @@ -164,6 +177,19 @@ object SceneWindowRootViewBinder { } } + private fun createAlternateBouncerView( + context: Context, + alternateBouncerDependencies: AlternateBouncerDependencies, + ): ComposeView { + return ComposeView(context).apply { + setContent { + AlternateBouncer( + alternateBouncerDependencies = alternateBouncerDependencies, + ) + } + } + } + // TODO(b/298525212): remove once Compose exposes window inset bounds. private fun displayCutoutFromWindowInsets( scope: CoroutineScope, diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java index cbb61b37b7a4..700253babb82 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"; @@ -78,6 +80,7 @@ public class RecordingService extends Service implements ScreenMediaRecorderList "com.android.systemui.screenrecord.STOP_FROM_NOTIF"; protected static final String ACTION_SHARE = "com.android.systemui.screenrecord.SHARE"; private static final String PERMISSION_SELF = "com.android.systemui.permission.SELF"; + protected static final String EXTRA_NOTIFICATION_ID = "notification_id"; private final RecordingController mController; protected final KeyguardDismissUtil mKeyguardDismissUtil; @@ -181,7 +184,7 @@ public class RecordingService extends Service implements ScreenMediaRecorderList mUiEventLogger.log(Events.ScreenRecordEvent.SCREEN_RECORD_START); } else { updateState(false); - createErrorNotification(); + createErrorStartingNotification(currentUser); stopForeground(STOP_FOREGROUND_DETACH); stopSelf(); return Service.START_NOT_STICKY; @@ -272,17 +275,35 @@ public class RecordingService extends Service implements ScreenMediaRecorderList } /** - * Simple error notification, needed since startForeground must be called to avoid errors + * Simple "error starting" notification, needed since startForeground must be called to avoid + * errors. */ @VisibleForTesting - protected void createErrorNotification() { + protected void createErrorStartingNotification(UserHandle currentUser) { + createErrorNotification(currentUser, strings().getStartError(), GROUP_KEY_ERROR_STARTING); + } + + /** + * Simple "error saving" notification, needed since startForeground must be called to avoid + * errors. + */ + @VisibleForTesting + protected void createErrorSavingNotification(UserHandle currentUser) { + createErrorNotification(currentUser, strings().getSaveError(), GROUP_KEY_ERROR_SAVING); + } + + 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()); - String notificationTitle = strings().getStartError(); Notification.Builder builder = new Notification.Builder(this, getChannelId()) .setSmallIcon(R.drawable.ic_screenrecord) - .setContentTitle(notificationTitle) + .setContentTitle(notificationContentTitle) + .setGroup(groupKey) .addExtras(extras); startForeground(mNotificationId, builder.build()); } @@ -337,7 +358,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(); } @@ -374,7 +395,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 @@ -389,21 +410,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() { @@ -414,6 +442,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 { @@ -427,11 +456,11 @@ public class RecordingService extends Service implements ScreenMediaRecorderList // let's release the recorder and delete all temporary files in this case getRecorder().release(); } - showErrorToast(R.string.screenrecord_start_error); + showErrorToast(R.string.screenrecord_save_error); Log.e(getTag(), "stopRecording called, but there was an error when ending" + "recording"); exception.printStackTrace(); - createErrorNotification(); + createErrorSavingNotification(currentUser); } catch (Throwable throwable) { if (getRecorder() != null) { // Something unexpected happen, SystemUI will crash but let's delete @@ -455,7 +484,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) { @@ -514,7 +543,8 @@ public class RecordingService extends Service implements ScreenMediaRecorderList private Intent getShareIntent(Context context, Uri path) { return new Intent(context, this.getClass()).setAction(ACTION_SHARE) - .putExtra(EXTRA_PATH, path); + .putExtra(EXTRA_PATH, path) + .putExtra(EXTRA_NOTIFICATION_ID, mNotificationId); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt index b54bf6ca9ef8..46ac54f63183 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt @@ -270,15 +270,18 @@ class ScreenRecordPermissionDialogDelegate( return listOf( ScreenShareOption( SINGLE_APP, - R.string.screen_share_permission_dialog_option_single_app, + R.string.screenrecord_permission_dialog_option_text_single_app, R.string.screenrecord_permission_dialog_warning_single_app, - startButtonText = R.string.screenrecord_permission_dialog_continue, + startButtonText = + R.string + .media_projection_entry_generic_permission_dialog_continue_single_app, ), ScreenShareOption( ENTIRE_SCREEN, - R.string.screen_share_permission_dialog_option_entire_screen, + R.string.screenrecord_permission_dialog_option_text_entire_screen, R.string.screenrecord_permission_dialog_warning_entire_screen, - startButtonText = R.string.screenrecord_permission_dialog_continue, + startButtonText = + R.string.screenrecord_permission_dialog_continue_entire_screen, ) ) } 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/NotificationsQSContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt index 8b88da1754f0..348b6bab1617 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt @@ -27,7 +27,6 @@ import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID import androidx.constraintlayout.widget.ConstraintSet.START import androidx.constraintlayout.widget.ConstraintSet.TOP import androidx.lifecycle.lifecycleScope -import com.android.systemui.Flags.centralizedStatusBarHeightFix import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.fragments.FragmentService @@ -191,11 +190,7 @@ constructor( } private fun calculateLargeShadeHeaderHeight(): Int { - return if (centralizedStatusBarHeightFix()) { - largeScreenHeaderHelperLazy.get().getLargeScreenHeaderHeight() - } else { - resources.getDimensionPixelSize(R.dimen.large_screen_shade_header_height) - } + return largeScreenHeaderHelperLazy.get().getLargeScreenHeaderHeight() } private fun calculateShadeHeaderHeight(): Int { diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java index 9f61d4e5d949..16aef6586ee9 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java @@ -19,7 +19,6 @@ package com.android.systemui.shade; import static android.view.WindowInsets.Type.ime; -import static com.android.systemui.Flags.centralizedStatusBarHeightFix; import static com.android.systemui.classifier.Classifier.QS_COLLAPSE; import static com.android.systemui.shade.NotificationPanelViewController.COUNTER_PANEL_OPEN_QS; import static com.android.systemui.shade.NotificationPanelViewController.FLING_COLLAPSE; @@ -444,10 +443,7 @@ public class QuickSettingsControllerImpl implements QuickSettingsController, Dum mUseLargeScreenShadeHeader = LargeScreenUtils.shouldUseLargeScreenShadeHeader(mPanelView.getResources()); mLargeScreenShadeHeaderHeight = - centralizedStatusBarHeightFix() - ? mLargeScreenHeaderHelperLazy.get().getLargeScreenHeaderHeight() - : mResources.getDimensionPixelSize( - R.dimen.large_screen_shade_header_height); + mLargeScreenHeaderHelperLazy.get().getLargeScreenHeaderHeight(); int topMargin = mUseLargeScreenShadeHeader ? mLargeScreenShadeHeaderHeight : mResources.getDimensionPixelSize(R.dimen.notification_panel_margin_top); mShadeHeaderController.setLargeScreenActive(mUseLargeScreenShadeHeader); @@ -2256,8 +2252,11 @@ public class QuickSettingsControllerImpl implements QuickSettingsController, Dum // panel, mQs will not need to be null cause it will be tied to the same lifecycle. if (fragment == mQs) { // Clear it to remove bindings to mQs from the provider. - mNotificationStackScrollLayoutController.setQsHeaderBoundsProvider(null); - mNotificationStackScrollLayoutController.setQsHeader(null); + if (QSComposeFragment.isEnabled()) { + mNotificationStackScrollLayoutController.setQsHeaderBoundsProvider(null); + } else { + mNotificationStackScrollLayoutController.setQsHeader(null); + } mQs = null; } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt index 37da114137fe..c49cfbde25a5 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt @@ -38,7 +38,6 @@ import androidx.core.view.doOnLayout import com.android.app.animation.Interpolators import com.android.settingslib.Utils import com.android.systemui.Dumpable -import com.android.systemui.Flags.centralizedStatusBarHeightFix import com.android.systemui.animation.ShadeInterpolation import com.android.systemui.battery.BatteryMeterView import com.android.systemui.battery.BatteryMeterViewController @@ -231,10 +230,12 @@ constructor( private val demoModeReceiver = object : DemoMode { override fun demoCommands() = listOf(DemoMode.COMMAND_CLOCK) + override fun dispatchDemoCommand(command: String, args: Bundle) = clock.dispatchDemoCommand(command, args) override fun onDemoModeStarted() = clock.onDemoModeStarted() + override fun onDemoModeFinished() = clock.onDemoModeFinished() } @@ -442,9 +443,7 @@ constructor( changes += combinedShadeHeadersConstraintManager.emptyCutoutConstraints() } - if (centralizedStatusBarHeightFix()) { - view.setPadding(view.paddingLeft, sbInsets.top, view.paddingRight, view.paddingBottom) - } + view.setPadding(view.paddingLeft, sbInsets.top, view.paddingRight, view.paddingBottom) view.updateAllConstraints(changes) updateBatteryMode() } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt index bc2377895101..21bbaa5a41f2 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt @@ -31,6 +31,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.flags.FeatureFlags import com.android.systemui.keyguard.ui.view.KeyguardRootView +import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerDependencies import com.android.systemui.privacy.OngoingPrivacyChip import com.android.systemui.res.R import com.android.systemui.scene.shared.flag.SceneContainerFlag @@ -83,6 +84,7 @@ abstract class ShadeViewProviderModule { scenesProvider: Provider<Set<@JvmSuppressWildcards Scene>>, layoutInsetController: NotificationInsetsController, sceneDataSourceDelegator: Provider<SceneDataSourceDelegator>, + alternateBouncerDependencies: Provider<AlternateBouncerDependencies>, ): WindowRootView { return if (SceneContainerFlag.isEnabled) { checkNoSceneDuplicates(scenesProvider.get()) @@ -96,6 +98,7 @@ abstract class ShadeViewProviderModule { scenes = scenesProvider.get(), layoutInsetController = layoutInsetController, sceneDataSourceDelegator = sceneDataSourceDelegator.get(), + alternateBouncerDependencies = alternateBouncerDependencies.get(), ) sceneWindowRootView } else { 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/domain/interactor/ShadeInteractorLegacyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImpl.kt index 9e221d3d2341..f48e31e1d7eb 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImpl.kt @@ -21,6 +21,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.data.repository.KeyguardRepository import com.android.systemui.keyguard.shared.model.StatusBarState +import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.data.repository.ShadeRepository import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor import javax.inject.Inject @@ -46,6 +47,10 @@ constructor( sharedNotificationContainerInteractor: SharedNotificationContainerInteractor, repository: ShadeRepository, ) : BaseShadeInteractor { + init { + SceneContainerFlag.assertInLegacyMode() + } + /** * The amount [0-1] that the shade has been opened. Uses stateIn to avoid redundant calculations * in downstream flows. diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt index 9617b542b427..6a21531d9c06 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt @@ -22,8 +22,9 @@ import com.android.compose.animation.scene.SceneKey import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.scene.shared.model.SceneFamilies -import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor +import com.android.systemui.shade.data.repository.ShadeRepository import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -43,8 +44,12 @@ class ShadeInteractorSceneContainerImpl constructor( @Application scope: CoroutineScope, sceneInteractor: SceneInteractor, - sharedNotificationContainerInteractor: SharedNotificationContainerInteractor, + shadeRepository: ShadeRepository, ) : BaseShadeInteractor { + init { + SceneContainerFlag.assertInNewMode() + } + override val shadeExpansion: StateFlow<Float> = sceneBasedExpansion(sceneInteractor, SceneFamilies.NotifShade) .traceAsCounter("panel_expansion") { (it * 100f).toInt() } @@ -55,7 +60,7 @@ constructor( override val qsExpansion: StateFlow<Float> = combine( - sharedNotificationContainerInteractor.isSplitShadeEnabled, + shadeRepository.isShadeLayoutWide, shadeExpansion, sceneBasedQsExpansion, ) { isSplitShadeEnabled, shadeExpansion, qsExpansion -> 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/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt index 6ba4fefd6f3c..9e6cacb8b9ff 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt @@ -41,6 +41,7 @@ import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel import com.android.systemui.statusbar.chips.ui.viewmodel.ChipTransitionHelper import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel.Companion.createDialogLaunchOnClickListener +import com.android.systemui.util.kotlin.pairwise import com.android.systemui.util.time.SystemClock import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -64,7 +65,8 @@ constructor( @StatusBarChipsLog private val logger: LogBuffer, ) : OngoingActivityChipViewModel { - private val internalChip = + /** A direct mapping from [ScreenRecordChipModel] to [OngoingActivityChipModel]. */ + private val simpleChip = interactor.screenRecordState .map { state -> when (state) { @@ -105,10 +107,31 @@ constructor( // See b/347726238 for [SharingStarted.Lazily] reasoning. .stateIn(scope, SharingStarted.Lazily, OngoingActivityChipModel.Hidden()) + /** + * The screen record chip to show that also ensures that the start time doesn't change once we + * enter the recording state. If we change the start time while we're recording, the chronometer + * could skip a second. See b/349620526. + */ + private val chipWithConsistentTimer: StateFlow<OngoingActivityChipModel> = + simpleChip + .pairwise(initialValue = OngoingActivityChipModel.Hidden()) + .map { (old, new) -> + if ( + old is OngoingActivityChipModel.Shown.Timer && + new is OngoingActivityChipModel.Shown.Timer + ) { + new.copy(startTimeMs = old.startTimeMs) + } else { + new + } + } + // See b/347726238 for [SharingStarted.Lazily] reasoning. + .stateIn(scope, SharingStarted.Lazily, OngoingActivityChipModel.Hidden()) + private val chipTransitionHelper = ChipTransitionHelper(scope) override val chip: StateFlow<OngoingActivityChipModel> = - chipTransitionHelper.createChipFlow(internalChip) + chipTransitionHelper.createChipFlow(chipWithConsistentTimer) private fun createDelegate( recordedTask: ActivityManager.RunningTaskInfo? diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/ColorsModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/ColorsModel.kt index 130b1170c9f1..8a5165d8df7b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/ColorsModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/ColorsModel.kt @@ -18,7 +18,6 @@ package com.android.systemui.statusbar.chips.ui.model import android.content.Context import android.content.res.ColorStateList -import android.view.ContextThemeWrapper import androidx.annotation.ColorInt import com.android.settingslib.Utils import com.android.systemui.res.R @@ -43,9 +42,7 @@ sealed interface ColorsModel { /** The chip should have a red background with white text. */ data object Red : ColorsModel { override fun background(context: Context): ColorStateList { - val themedContext = - ContextThemeWrapper(context, com.android.internal.R.style.Theme_DeviceDefault_Light) - return Utils.getColorAttr(themedContext, com.android.internal.R.attr.materialColorError) + return ColorStateList.valueOf(context.getColor(R.color.GM2_red_700)) } override fun text(context: Context) = context.getColor(android.R.color.white) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/GesturePointerEventListener.kt b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/GesturePointerEventListener.kt index 0d5ade7ab49c..c7b3c9c4b32a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/GesturePointerEventListener.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/GesturePointerEventListener.kt @@ -103,7 +103,7 @@ constructor(context: Context, gestureDetector: GesturePointerEventDetector) : Co mSwipeDistanceThreshold = r.getDimensionPixelSize(R.dimen.system_gestures_distance_threshold) val display = DisplayManagerGlobal.getInstance().getRealDisplay(mContext.displayId) - val displayCutout = display.cutout + val displayCutout = display?.cutout if (displayCutout != null) { // Expand swipe start threshold such that we can catch touches that just start beyond // the notch area 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..9b382e61b2e3 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,16 @@ 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 } + } + .distinctUntilChanged() + .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/domain/interactor/SharedNotificationContainerInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt index 9b21fa9bbe35..5d3747628e7e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt @@ -18,13 +18,14 @@ package com.android.systemui.statusbar.notification.stack.domain.interactor import android.content.Context -import com.android.systemui.Flags.centralizedStatusBarHeightFix import com.android.systemui.common.ui.data.repository.ConfigurationRepository import com.android.systemui.dagger.SysUISingleton import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.res.R +import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.LargeScreenHeaderHelper +import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.policy.SplitShadeStateController import dagger.Lazy import javax.inject.Inject @@ -44,7 +45,8 @@ class SharedNotificationContainerInteractor constructor( configurationRepository: ConfigurationRepository, private val context: Context, - private val splitShadeStateController: SplitShadeStateController, + private val splitShadeStateController: Lazy<SplitShadeStateController>, + private val shadeInteractor: Lazy<ShadeInteractor>, keyguardInteractor: KeyguardInteractor, deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor, largeScreenHeaderHelperLazy: Lazy<LargeScreenHeaderHelper>, @@ -57,16 +59,33 @@ constructor( /** An internal modification was made to notifications */ val notificationStackChanged = _notificationStackChanged.debounce(20L) + private val configurationChangeEvents = + configurationRepository.onAnyConfigurationChange.onStart { emit(Unit) } + + /* Warning: Even though the value it emits only contains the split shade status, this flow must + * emit a value whenever the configuration *or* the split shade status changes. Adding a + * distinctUntilChanged() to this would cause configurationBasedDimensions to miss configuration + * updates that affect other resources, like margins or the large screen header flag. + */ + private val dimensionsUpdateEventsWithShouldUseSplitShade: Flow<Boolean> = + if (SceneContainerFlag.isEnabled) { + combine(configurationChangeEvents, shadeInteractor.get().isShadeLayoutWide) { + _, + isShadeLayoutWide -> + isShadeLayoutWide + } + } else { + configurationChangeEvents.map { + splitShadeStateController.get().shouldUseSplitNotificationShade(context.resources) + } + } + val configurationBasedDimensions: Flow<ConfigurationBasedDimensions> = - configurationRepository.onAnyConfigurationChange - .onStart { emit(Unit) } - .map { _ -> + dimensionsUpdateEventsWithShouldUseSplitShade + .map { shouldUseSplitShade -> with(context.resources) { ConfigurationBasedDimensions( - useSplitShade = - splitShadeStateController.shouldUseSplitNotificationShade( - context.resources - ), + useSplitShade = shouldUseSplitShade, useLargeScreenHeader = getBoolean(R.bool.config_use_large_screen_shade_header), marginHorizontal = @@ -75,11 +94,7 @@ constructor( getDimensionPixelSize(R.dimen.notification_panel_margin_bottom), marginTop = getDimensionPixelSize(R.dimen.notification_panel_margin_top), marginTopLargeScreen = - if (centralizedStatusBarHeightFix()) { - largeScreenHeaderHelperLazy.get().getLargeScreenHeaderHeight() - } else { - getDimensionPixelSize(R.dimen.large_screen_shade_header_height) - }, + largeScreenHeaderHelperLazy.get().getLargeScreenHeaderHeight(), keyguardSplitShadeTopMargin = getDimensionPixelSize(R.dimen.keyguard_split_shade_top_margin), ) 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 fd08e898fce3..a30b8772c3d1 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,7 +33,6 @@ 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 @@ -46,7 +45,7 @@ constructor( dumpManager: DumpManager, @Main private val mainImmediateDispatcher: CoroutineDispatcher, private val view: NotificationScrollView, - private val viewModel: NotificationScrollViewModel, + private val viewModelFactory: NotificationScrollViewModel.Factory, private val configuration: ConfigurationState, ) : FlowDumperImpl(dumpManager) { @@ -61,38 +60,42 @@ constructor( } fun bindWhileAttached(): DisposableHandle { - return view.asView().repeatWhenAttached(mainImmediateDispatcher) { - repeatOnLifecycle(Lifecycle.State.CREATED) { bind() } - } + return view.asView().repeatWhenAttached(mainImmediateDispatcher) { bind() } } - suspend fun bind() = coroutineScope { - launchAndDispose { - updateViewPosition() - view.asView().onLayoutChanged { updateViewPosition() } - } + suspend fun bind(): Nothing = + view.asView().viewModel( + minWindowLifecycleState = WindowLifecycleState.ATTACHED, + factory = viewModelFactory::create, + ) { viewModel -> + 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/viewbinder/SharedNotificationContainerBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt index 05415503675c..aa1911e4cd2a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt @@ -99,19 +99,20 @@ constructor( disposables += view.repeatWhenAttached(mainImmediateDispatcher) { repeatOnLifecycle(Lifecycle.State.CREATED) { - launch { - // Only temporarily needed, until flexi notifs go live - viewModel.shadeCollapseFadeIn.collect { fadeIn -> - if (fadeIn) { - android.animation.ValueAnimator.ofFloat(0f, 1f).apply { - duration = 250 - addUpdateListener { animation -> - controller.setMaxAlphaForKeyguard( - animation.animatedFraction, - "SharedNotificationContainerVB (collapseFadeIn)" - ) + if (!SceneContainerFlag.isEnabled) { + launch { + viewModel.shadeCollapseFadeIn.collect { fadeIn -> + if (fadeIn) { + android.animation.ValueAnimator.ofFloat(0f, 1f).apply { + duration = 250 + addUpdateListener { animation -> + controller.setMaxAlphaForKeyguard( + animation.animatedFraction, + "SharedNotificationContainerVB (collapseFadeIn)" + ) + } + start() } - start() } } } 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 2ba79a8612bb..bfb624a9287b 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,9 +33,11 @@ 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.FlowDumperImpl +import com.android.systemui.util.kotlin.ActivatableFlowDumper +import com.android.systemui.util.kotlin.ActivatableFlowDumperImpl import dagger.Lazy -import javax.inject.Inject +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged @@ -43,9 +45,8 @@ 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 -@Inject +@AssistedInject constructor( dumpManager: DumpManager, stackAppearanceInteractor: NotificationStackAppearanceInteractor, @@ -54,7 +55,14 @@ 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>, -) : FlowDumperImpl(dumpManager) { +) : + ActivatableFlowDumper by ActivatableFlowDumperImpl(dumpManager, "NotificationScrollViewModel"), + SysUiViewModel() { + + override suspend fun onActivated() { + activateFlowDumper() + } + /** * 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 @@ -186,4 +194,9 @@ 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/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index 2c7ce00adbeb..c4fbc37b2dd5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -30,6 +30,7 @@ import static com.android.systemui.Dependency.TIME_TICK_HANDLER_NAME; import static com.android.systemui.Flags.keyboardShortcutHelperRewrite; import static com.android.systemui.Flags.lightRevealMigration; import static com.android.systemui.Flags.newAodTransition; +import static com.android.systemui.Flags.relockWithPowerButtonImmediately; import static com.android.systemui.charging.WirelessChargingAnimation.UNKNOWN_BATTERY_LEVEL; import static com.android.systemui.flags.Flags.SHORTCUT_LIST_SEARCH_LAYOUT; import static com.android.systemui.statusbar.NotificationLockscreenUserManager.PERMISSION_SELF; @@ -2269,7 +2270,10 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { // applying the dimming effect twice. mUiBgExecutor.execute(() -> { float dimAmount = 0f; - if (mWallpaperManager.lockScreenWallpaperExists()) { + // Note that access to WallpaperManager APIs should be guarded by a check into + // WallpaperManager#isWallpaperSupported. Form factors that do not use wallpaper + // may crash SysUI during improper access. ref: b/355307617 + if (!mWallpaperSupported || mWallpaperManager.lockScreenWallpaperExists()) { dimAmount = mWallpaperManager.getWallpaperDimAmount(); } final float scrimDimAmount = dimAmount; @@ -2349,8 +2353,14 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { } else if (mState == StatusBarState.KEYGUARD && !mStatusBarKeyguardViewManager.primaryBouncerIsOrWillBeShowing() && mStatusBarKeyguardViewManager.isSecure()) { - Log.d(TAG, "showBouncerOrLockScreenIfKeyguard, showingBouncer"); - mStatusBarKeyguardViewManager.showBouncer(true /* scrimmed */); + if (!relockWithPowerButtonImmediately()) { + Log.d(TAG, "showBouncerOrLockScreenIfKeyguard, showingBouncer"); + if (SceneContainerFlag.isEnabled()) { + mStatusBarKeyguardViewManager.showPrimaryBouncer(true /* scrimmed */); + } else { + mStatusBarKeyguardViewManager.showBouncer(true /* scrimmed */); + } + } } } } @@ -2982,7 +2992,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { @Override public void onFalse() { // Hides quick settings, bouncer, and quick-quick settings. - mStatusBarKeyguardViewManager.reset(true); + mStatusBarKeyguardViewManager.reset(true, /* isFalsingReset= */true); } }; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java index 0adc1b0af66f..013903a5eb3d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java @@ -16,7 +16,6 @@ package com.android.systemui.statusbar.phone; -import static com.android.systemui.Flags.centralizedStatusBarHeightFix; import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInOffset; import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInScale; import static com.android.systemui.statusbar.notification.NotificationUtils.interpolate; @@ -169,9 +168,7 @@ public class KeyguardClockPositionAlgorithm { mStatusViewBottomMargin = res.getDimensionPixelSize(R.dimen.keyguard_status_view_bottom_margin); mSplitShadeTopNotificationsMargin = - centralizedStatusBarHeightFix() - ? LargeScreenHeaderHelper.getLargeScreenHeaderHeight(context) - : res.getDimensionPixelSize(R.dimen.large_screen_shade_header_height); + LargeScreenHeaderHelper.getLargeScreenHeaderHeight(context); mSplitShadeTargetTopMargin = res.getDimensionPixelSize(R.dimen.keyguard_split_shade_top_margin); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java index 84e601848b91..c3da7fcc86c8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java @@ -16,7 +16,6 @@ package com.android.systemui.statusbar.phone; -import static com.android.systemui.Flags.centralizedStatusBarHeightFix; import static com.android.systemui.ScreenDecorations.DisplayCutoutView.boundsFromDirection; import static com.android.systemui.util.Utils.getStatusBarHeaderHeightKeyguard; @@ -133,9 +132,6 @@ public class KeyguardStatusBarView extends RelativeLayout { mUserSwitcherContainer = findViewById(R.id.user_switcher_container); mIsPrivacyDotEnabled = mContext.getResources().getBoolean(R.bool.config_enablePrivacyDot); loadDimens(); - if (!centralizedStatusBarHeightFix()) { - setGravity(Gravity.CENTER_VERTICAL); - } } /** @@ -322,7 +318,7 @@ public class KeyguardStatusBarView extends RelativeLayout { final int minRight = (!isLayoutRtl() && mIsPrivacyDotEnabled) ? Math.max(mMinDotWidth, mPadding.right) : mPadding.right; - int top = centralizedStatusBarHeightFix() ? waterfallTop + mPadding.top : waterfallTop; + int top = waterfallTop + mPadding.top; setPadding(minLeft, top, minRight, 0); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java index 2d775b74eb32..5486abba9987 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java @@ -708,7 +708,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb * Shows the notification keyguard or the bouncer depending on * {@link #needsFullscreenBouncer()}. */ - protected void showBouncerOrKeyguard(boolean hideBouncerWhenShowing) { + protected void showBouncerOrKeyguard(boolean hideBouncerWhenShowing, boolean isFalsingReset) { boolean isDozing = mDozing; if (Flags.simPinRaceConditionOnRestart()) { KeyguardState toState = mKeyguardTransitionInteractor.getTransitionState().getValue() @@ -734,8 +734,11 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb mPrimaryBouncerInteractor.show(/* isScrimmed= */ true); } } - } else { - Log.e(TAG, "Attempted to show the sim bouncer when it is already showing."); + } else if (!isFalsingReset) { + // Falsing resets can cause this to flicker, so don't reset in this case + Log.i(TAG, "Sim bouncer is already showing, issuing a refresh"); + mPrimaryBouncerInteractor.show(/* isScrimmed= */ true); + } } else { mCentralSurfaces.showKeyguard(); @@ -957,6 +960,10 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb @Override public void reset(boolean hideBouncerWhenShowing) { + reset(hideBouncerWhenShowing, /* isFalsingReset= */false); + } + + public void reset(boolean hideBouncerWhenShowing, boolean isFalsingReset) { if (mKeyguardStateController.isShowing() && !bouncerIsAnimatingAway()) { final boolean isOccluded = mKeyguardStateController.isOccluded(); // Hide quick settings. @@ -965,10 +972,14 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb if (isOccluded && !mDozing) { mCentralSurfaces.hideKeyguard(); if (hideBouncerWhenShowing || needsFullscreenBouncer()) { - hideBouncer(false /* destroyView */); + // We're removing "reset" in the refactor - bouncer will be hidden by the root + // cause of the "reset" calls. + if (!KeyguardWmStateRefactor.isEnabled()) { + hideBouncer(false /* destroyView */); + } } } else { - showBouncerOrKeyguard(hideBouncerWhenShowing); + showBouncerOrKeyguard(hideBouncerWhenShowing, isFalsingReset); } if (hideBouncerWhenShowing) { hideAlternateBouncer(true); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt index 4368239c31f0..bd6a1c05ddc9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt @@ -227,6 +227,15 @@ constructor( callNotificationInfo // This shouldn't happen, but protect against it in case ?: return OngoingCallModel.NoCall + logger.log( + TAG, + LogLevel.DEBUG, + { + bool1 = Flags.statusBarCallChipNotificationIcon() + bool2 = currentInfo.notificationIconView != null + }, + { "Creating OngoingCallModel.InCall. notifIconFlag=$bool1 hasIcon=$bool2" } + ) val icon = if (Flags.statusBarCallChipNotificationIcon()) { currentInfo.notificationIconView @@ -257,6 +266,7 @@ constructor( private fun updateInfoFromNotifModel(notifModel: ActiveNotificationModel?) { if (notifModel == null) { + logger.log(TAG, LogLevel.DEBUG, {}, { "NotifInteractorCallModel: null" }) removeChip() } else if (notifModel.callType != CallType.Ongoing) { logger.log( @@ -267,6 +277,18 @@ constructor( ) removeChip() } else { + logger.log( + TAG, + LogLevel.DEBUG, + { + str1 = notifModel.key + long1 = notifModel.whenTime + str1 = notifModel.callType.name + bool1 = notifModel.statusBarChipIconView != null + }, + { "NotifInteractorCallModel: key=$str1 when=$long1 callType=$str2 hasIcon=$bool1" } + ) + val newOngoingCallInfo = CallNotificationInfo( notifModel.key, 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/TouchpadTutorialModule.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/TouchpadTutorialModule.kt new file mode 100644 index 000000000000..238e8a1c01ad --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/TouchpadTutorialModule.kt @@ -0,0 +1,73 @@ +/* + * 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.touchpad.tutorial + +import android.app.Activity +import androidx.compose.runtime.Composable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.inputdevice.tutorial.TouchpadTutorialScreensProvider +import com.android.systemui.model.SysUiState +import com.android.systemui.settings.DisplayTracker +import com.android.systemui.touchpad.tutorial.domain.interactor.TouchpadGesturesInteractor +import com.android.systemui.touchpad.tutorial.ui.composable.BackGestureTutorialScreen +import com.android.systemui.touchpad.tutorial.ui.composable.HomeGestureTutorialScreen +import com.android.systemui.touchpad.tutorial.ui.view.TouchpadTutorialActivity +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap +import kotlinx.coroutines.CoroutineScope + +@Module +interface TouchpadTutorialModule { + + @Binds + @IntoMap + @ClassKey(TouchpadTutorialActivity::class) + fun activity(impl: TouchpadTutorialActivity): Activity + + companion object { + @Provides + fun touchpadScreensProvider(): TouchpadTutorialScreensProvider { + return ScreensProvider + } + + @SysUISingleton + @Provides + fun touchpadGesturesInteractor( + sysUiState: SysUiState, + displayTracker: DisplayTracker, + @Background backgroundScope: CoroutineScope + ): TouchpadGesturesInteractor { + return TouchpadGesturesInteractor(sysUiState, displayTracker, backgroundScope) + } + } +} + +private object ScreensProvider : TouchpadTutorialScreensProvider { + @Composable + override fun BackGesture(onDoneButtonClicked: () -> Unit, onBack: () -> Unit) { + BackGestureTutorialScreen(onDoneButtonClicked, onBack) + } + + @Composable + override fun HomeGesture(onDoneButtonClicked: () -> Unit, onBack: () -> Unit) { + HomeGestureTutorialScreen(onDoneButtonClicked, onBack) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/domain/interactor/TouchpadGesturesInteractor.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/domain/interactor/TouchpadGesturesInteractor.kt index b6c2ae794da9..df95232758a4 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/domain/interactor/TouchpadGesturesInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/domain/interactor/TouchpadGesturesInteractor.kt @@ -16,22 +16,16 @@ package com.android.systemui.touchpad.tutorial.domain.interactor -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.model.SysUiState import com.android.systemui.settings.DisplayTracker import com.android.systemui.shared.system.QuickStepContract -import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -@SysUISingleton -class TouchpadGesturesInteractor -@Inject -constructor( +class TouchpadGesturesInteractor( private val sysUiState: SysUiState, private val displayTracker: DisplayTracker, - @Background private val backgroundScope: CoroutineScope + private val backgroundScope: CoroutineScope ) { fun disableGestures() { setGesturesState(disabled = true) 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..1c8041ff5b31 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 @@ -21,6 +21,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import com.airbnb.lottie.compose.rememberLottieDynamicProperties import com.android.compose.theme.LocalAndroidColorScheme +import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialScreenConfig +import com.android.systemui.inputdevice.tutorial.ui.composable.rememberColorFilterProperty import com.android.systemui.res.R import com.android.systemui.touchpad.tutorial.ui.gesture.BackGestureMonitor import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState @@ -38,8 +40,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..57d7c84af4ba 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 @@ -16,57 +16,21 @@ package com.android.systemui.touchpad.tutorial.ui.composable -import android.graphics.ColorFilter -import android.graphics.PorterDuff -import android.graphics.PorterDuffColorFilter import androidx.activity.compose.BackHandler -import androidx.annotation.RawRes -import androidx.annotation.StringRes -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.EnterTransition -import androidx.compose.animation.ExitTransition -import androidx.compose.animation.animateColorAsState -import androidx.compose.animation.core.LinearEasing -import androidx.compose.animation.core.snap -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.togetherWith -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawBehind -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.input.pointer.pointerInteropFilter import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import com.airbnb.lottie.LottieProperty -import com.airbnb.lottie.compose.LottieAnimation -import com.airbnb.lottie.compose.LottieCompositionSpec -import com.airbnb.lottie.compose.LottieConstants -import com.airbnb.lottie.compose.LottieDynamicProperties -import com.airbnb.lottie.compose.LottieDynamicProperty -import com.airbnb.lottie.compose.animateLottieCompositionAsState -import com.airbnb.lottie.compose.rememberLottieComposition -import com.airbnb.lottie.compose.rememberLottieDynamicProperty +import com.android.systemui.inputdevice.tutorial.ui.composable.ActionTutorialContent +import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState +import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialScreenConfig import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.FINISHED import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.IN_PROGRESS @@ -81,6 +45,14 @@ interface GestureMonitorProvider { ): TouchpadGestureMonitor } +fun GestureState.toTutorialActionState(): TutorialActionState { + return when (this) { + NOT_STARTED -> TutorialActionState.NOT_STARTED + IN_PROGRESS -> TutorialActionState.IN_PROGRESS + FINISHED -> TutorialActionState.FINISHED + } +} + @Composable fun GestureTutorialScreen( screenConfig: TutorialScreenConfig, @@ -104,7 +76,11 @@ fun GestureTutorialScreen( ) } TouchpadGesturesHandlingBox(gestureHandler, gestureState) { - GestureTutorialContent(gestureState, onDoneButtonClicked, screenConfig) + ActionTutorialContent( + gestureState.toTutorialActionState(), + onDoneButtonClicked, + screenConfig + ) } } @@ -135,165 +111,3 @@ private fun TouchpadGesturesHandlingBox( content() } } - -@Composable -private fun GestureTutorialContent( - gestureState: GestureState, - onDoneButtonClicked: () -> Unit, - config: TutorialScreenConfig -) { - val animatedColor by - animateColorAsState( - targetValue = - if (gestureState == FINISHED) config.colors.successBackground - else config.colors.background, - animationSpec = tween(durationMillis = 150, easing = LinearEasing), - label = "backgroundColor" - ) - Column( - verticalArrangement = Arrangement.Center, - modifier = - Modifier.fillMaxSize() - .drawBehind { drawRect(animatedColor) } - .padding(start = 48.dp, top = 124.dp, end = 48.dp, bottom = 48.dp) - ) { - Row(modifier = Modifier.fillMaxWidth().weight(1f)) { - TutorialDescription( - titleTextId = - if (gestureState == FINISHED) config.strings.titleSuccessResId - else config.strings.titleResId, - titleColor = config.colors.title, - bodyTextId = - if (gestureState == FINISHED) config.strings.bodySuccessResId - else config.strings.bodyResId, - modifier = Modifier.weight(1f) - ) - Spacer(modifier = Modifier.width(76.dp)) - TutorialAnimation( - gestureState, - config, - modifier = Modifier.weight(1f).padding(top = 8.dp) - ) - } - DoneButton(onDoneButtonClicked = onDoneButtonClicked) - } -} - -@Composable -fun TutorialDescription( - @StringRes titleTextId: Int, - titleColor: Color, - @StringRes bodyTextId: Int, - modifier: Modifier = Modifier -) { - Column(verticalArrangement = Arrangement.Top, modifier = modifier) { - Text( - text = stringResource(id = titleTextId), - style = MaterialTheme.typography.displayLarge, - color = titleColor - ) - Spacer(modifier = Modifier.height(16.dp)) - Text( - text = stringResource(id = bodyTextId), - style = MaterialTheme.typography.bodyLarge, - color = Color.White - ) - } -} - -@Composable -fun TutorialAnimation( - gestureState: GestureState, - config: TutorialScreenConfig, - modifier: Modifier = Modifier -) { - Box(modifier = modifier.fillMaxWidth()) { - AnimatedContent( - targetState = gestureState, - transitionSpec = { - if (initialState == NOT_STARTED && targetState == IN_PROGRESS) { - val transitionDurationMillis = 150 - fadeIn( - animationSpec = tween(transitionDurationMillis, easing = LinearEasing) - ) togetherWith - fadeOut(animationSpec = snap(delayMillis = transitionDurationMillis)) - } else { - // empty transition works because all remaining transitions are from IN_PROGRESS - // state which shares initial animation frame with both FINISHED and NOT_STARTED - EnterTransition.None togetherWith ExitTransition.None - } - } - ) { gestureState -> - when (gestureState) { - NOT_STARTED -> - EducationAnimation( - config.animations.educationResId, - config.colors.animationColors - ) - IN_PROGRESS -> - FrozenSuccessAnimation( - config.animations.successResId, - config.colors.animationColors - ) - FINISHED -> - SuccessAnimation(config.animations.successResId, config.colors.animationColors) - } - } - } -} - -@Composable -private fun FrozenSuccessAnimation( - @RawRes successAnimationId: Int, - animationProperties: LottieDynamicProperties -) { - val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(successAnimationId)) - LottieAnimation( - composition = composition, - progress = { 0f }, // animation should freeze on 1st frame - dynamicProperties = animationProperties, - ) -} - -@Composable -private fun EducationAnimation( - @RawRes educationAnimationId: Int, - animationProperties: LottieDynamicProperties -) { - val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(educationAnimationId)) - val progress by - animateLottieCompositionAsState(composition, iterations = LottieConstants.IterateForever) - LottieAnimation( - composition = composition, - progress = { progress }, - dynamicProperties = animationProperties, - ) -} - -@Composable -private fun SuccessAnimation( - @RawRes successAnimationId: Int, - animationProperties: LottieDynamicProperties -) { - val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(successAnimationId)) - val progress by animateLottieCompositionAsState(composition, iterations = 1) - LottieAnimation( - composition = composition, - progress = { progress }, - dynamicProperties = animationProperties, - ) -} - -@Composable -fun rememberColorFilterProperty( - layerName: String, - color: Color -): LottieDynamicProperty<ColorFilter> { - return rememberLottieDynamicProperty( - LottieProperty.COLOR_FILTER, - value = PorterDuffColorFilter(color.toArgb(), PorterDuff.Mode.SRC_ATOP), - // "**" below means match zero or more layers, so ** layerName ** means find layer with that - // name at any depth - keyPath = arrayOf("**", layerName, "**") - ) -} 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 new file mode 100644 index 000000000000..0a6283aa7417 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt @@ -0,0 +1,86 @@ +/* + * 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.touchpad.tutorial.ui.composable + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import com.airbnb.lottie.compose.rememberLottieDynamicProperties +import com.android.compose.theme.LocalAndroidColorScheme +import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialScreenConfig +import com.android.systemui.inputdevice.tutorial.ui.composable.rememberColorFilterProperty +import com.android.systemui.res.R +import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState +import com.android.systemui.touchpad.tutorial.ui.gesture.HomeGestureMonitor +import com.android.systemui.touchpad.tutorial.ui.gesture.TouchpadGestureMonitor + +@Composable +fun HomeGestureTutorialScreen( + onDoneButtonClicked: () -> Unit, + onBack: () -> Unit, +) { + val screenConfig = + TutorialScreenConfig( + colors = rememberScreenColors(), + strings = + TutorialScreenConfig.Strings( + titleResId = R.string.touchpad_home_gesture_action_title, + bodyResId = R.string.touchpad_home_gesture_guidance, + titleSuccessResId = R.string.touchpad_home_gesture_success_title, + bodySuccessResId = R.string.touchpad_home_gesture_success_body + ), + animations = + TutorialScreenConfig.Animations( + educationResId = R.raw.trackpad_home_edu, + successResId = R.raw.trackpad_home_success + ) + ) + val gestureMonitorProvider = + object : GestureMonitorProvider { + override fun createGestureMonitor( + gestureDistanceThresholdPx: Int, + gestureStateChangedCallback: (GestureState) -> Unit + ): TouchpadGestureMonitor { + return HomeGestureMonitor(gestureDistanceThresholdPx, gestureStateChangedCallback) + } + } + GestureTutorialScreen(screenConfig, gestureMonitorProvider, onDoneButtonClicked, onBack) +} + +@Composable +private fun rememberScreenColors(): TutorialScreenConfig.Colors { + val primaryFixedDim = LocalAndroidColorScheme.current.primaryFixedDim + val onPrimaryFixed = LocalAndroidColorScheme.current.onPrimaryFixed + val onPrimaryFixedVariant = LocalAndroidColorScheme.current.onPrimaryFixedVariant + val surfaceContainer = MaterialTheme.colorScheme.surfaceContainer + val dynamicProperties = + rememberLottieDynamicProperties( + rememberColorFilterProperty(".primaryFixedDim", primaryFixedDim), + rememberColorFilterProperty(".onPrimaryFixed", onPrimaryFixed), + rememberColorFilterProperty(".onPrimaryFixedVariant", onPrimaryFixedVariant) + ) + val screenColors = + remember(surfaceContainer, dynamicProperties) { + TutorialScreenConfig.Colors( + background = onPrimaryFixed, + successBackground = surfaceContainer, + title = primaryFixedDim, + animationColors = dynamicProperties, + ) + } + return screenColors +} diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt index 14355fa18335..65b452a81da8 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt @@ -34,6 +34,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import com.android.systemui.inputdevice.tutorial.ui.composable.DoneButton import com.android.systemui.res.R @Composable diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt index 088a8fd95e60..256c5b590b14 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt @@ -27,8 +27,11 @@ import androidx.compose.runtime.getValue import androidx.lifecycle.Lifecycle.State.STARTED import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.compose.theme.PlatformTheme +import com.android.systemui.inputdevice.tutorial.ui.composable.ActionKeyTutorialScreen import com.android.systemui.touchpad.tutorial.ui.composable.BackGestureTutorialScreen +import com.android.systemui.touchpad.tutorial.ui.composable.HomeGestureTutorialScreen import com.android.systemui.touchpad.tutorial.ui.composable.TutorialSelectionScreen +import com.android.systemui.touchpad.tutorial.ui.viewmodel.Screen.ACTION_KEY import com.android.systemui.touchpad.tutorial.ui.viewmodel.Screen.BACK_GESTURE import com.android.systemui.touchpad.tutorial.ui.viewmodel.Screen.HOME_GESTURE import com.android.systemui.touchpad.tutorial.ui.viewmodel.Screen.TUTORIAL_SELECTION @@ -70,7 +73,7 @@ fun TouchpadTutorialScreen(vm: TouchpadTutorialViewModel, closeTutorial: () -> U TutorialSelectionScreen( onBackTutorialClicked = { vm.goTo(BACK_GESTURE) }, onHomeTutorialClicked = { vm.goTo(HOME_GESTURE) }, - onActionKeyTutorialClicked = {}, + onActionKeyTutorialClicked = { vm.goTo(ACTION_KEY) }, onDoneButtonClicked = closeTutorial ) BACK_GESTURE -> @@ -78,6 +81,15 @@ fun TouchpadTutorialScreen(vm: TouchpadTutorialViewModel, closeTutorial: () -> U onDoneButtonClicked = { vm.goTo(TUTORIAL_SELECTION) }, onBack = { vm.goTo(TUTORIAL_SELECTION) }, ) - HOME_GESTURE -> {} + HOME_GESTURE -> + HomeGestureTutorialScreen( + onDoneButtonClicked = { vm.goTo(TUTORIAL_SELECTION) }, + onBack = { vm.goTo(TUTORIAL_SELECTION) }, + ) + ACTION_KEY -> // TODO(b/358105049) move action key tutorial to OOBE flow + ActionKeyTutorialScreen( + onDoneButtonClicked = { vm.goTo(TUTORIAL_SELECTION) }, + onBack = { vm.goTo(TUTORIAL_SELECTION) }, + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/TouchpadTutorialViewModel.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/TouchpadTutorialViewModel.kt index 11984af0281a..d3aeaa7e3dca 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/TouchpadTutorialViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/TouchpadTutorialViewModel.kt @@ -55,4 +55,5 @@ enum class Screen { TUTORIAL_SELECTION, BACK_GESTURE, HOME_GESTURE, + ACTION_KEY, } diff --git a/packages/SystemUI/src/com/android/systemui/user/CreateUserActivity.java b/packages/SystemUI/src/com/android/systemui/user/CreateUserActivity.java index 56b466249e93..32f2ca6fb696 100644 --- a/packages/SystemUI/src/com/android/systemui/user/CreateUserActivity.java +++ b/packages/SystemUI/src/com/android/systemui/user/CreateUserActivity.java @@ -116,7 +116,7 @@ public class CreateUserActivity extends Activity { return mCreateUserDialogController.createDialog( this, this::startActivity, - (mUserCreator.isMultipleAdminEnabled() && mUserCreator.isUserAdmin() + (mUserCreator.canCreateAdminUser() && mUserCreator.isUserAdmin() && !isKeyguardShowing), this::addUserNow, this::finish diff --git a/packages/SystemUI/src/com/android/systemui/user/UserCreator.kt b/packages/SystemUI/src/com/android/systemui/user/UserCreator.kt index 9304a462b6a8..a426da929d6b 100644 --- a/packages/SystemUI/src/com/android/systemui/user/UserCreator.kt +++ b/packages/SystemUI/src/com/android/systemui/user/UserCreator.kt @@ -19,6 +19,7 @@ import android.app.Dialog import android.content.Context import android.content.pm.UserInfo import android.graphics.drawable.Drawable +import android.multiuser.Flags import android.os.UserManager import com.android.internal.util.UserIcons import com.android.settingslib.users.UserCreatingDialog @@ -91,7 +92,17 @@ constructor( return userManager.isAdminUser } - fun isMultipleAdminEnabled(): Boolean { - return UserManager.isMultipleAdminEnabled() + /** + * Checks if the creation of a new admin user is allowed. + * + * @return `true` if creating a new admin is allowed, `false` otherwise. + */ + fun canCreateAdminUser(): Boolean { + return if (Flags.unicornModeRefactoringForHsumReadOnly()) { + UserManager.isMultipleAdminEnabled() && + !userManager.hasUserRestriction(UserManager.DISALLOW_GRANT_ADMIN) + } else { + UserManager.isMultipleAdminEnabled() + } } } diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/FlowDumper.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/FlowDumper.kt index e17274c435aa..ade6c3df2e0f 100644 --- a/packages/SystemUI/src/com/android/systemui/util/kotlin/FlowDumper.kt +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/FlowDumper.kt @@ -19,11 +19,14 @@ package com.android.systemui.util.kotlin import android.util.IndentingPrintWriter import com.android.systemui.Dumpable import com.android.systemui.dump.DumpManager +import com.android.systemui.lifecycle.SafeActivatable +import com.android.systemui.lifecycle.SysUiViewModel import com.android.systemui.util.asIndenting import com.android.systemui.util.printCollection import java.io.PrintWriter import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.atomic.AtomicBoolean +import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow @@ -64,19 +67,20 @@ interface FlowDumper : Dumpable { } /** - * An implementation of [FlowDumper]. This be extended directly, or can be used to implement - * [FlowDumper] by delegation. - * - * @param dumpManager if provided, this will be used by the [FlowDumperImpl] to register and - * unregister itself when there is something to dump. - * @param tag a static name by which this [FlowDumperImpl] is registered. If not provided, this - * class's name will be used. If you're implementing by delegation, you probably want to provide - * this tag to get a meaningful dumpable name. + * The minimal implementation of FlowDumper. The owner must either register this with the + * DumpManager, or else call [dumpFlows] from its own [Dumpable.dump] method. */ -open class FlowDumperImpl(private val dumpManager: DumpManager?, tag: String? = null) : FlowDumper { +open class SimpleFlowDumper : FlowDumper { + private val stateFlowMap = ConcurrentHashMap<String, StateFlow<*>>() private val sharedFlowMap = ConcurrentHashMap<String, SharedFlow<*>>() private val flowCollectionMap = ConcurrentHashMap<Pair<String, String>, Any>() + + protected fun isNotEmpty(): Boolean = + stateFlowMap.isNotEmpty() || sharedFlowMap.isNotEmpty() || flowCollectionMap.isNotEmpty() + + protected open fun onMapKeysChanged(added: Boolean) {} + override fun dumpFlows(pw: IndentingPrintWriter) { pw.printCollection("StateFlow (value)", stateFlowMap.toSortedMap().entries) { (key, flow) -> append(key).append('=').println(flow.value) @@ -92,43 +96,62 @@ open class FlowDumperImpl(private val dumpManager: DumpManager?, tag: String? = } } - private val Any.idString: String - get() = Integer.toHexString(System.identityHashCode(this)) - override fun <T> Flow<T>.dumpWhileCollecting(dumpName: String): Flow<T> = flow { val mapKey = dumpName to idString try { collect { flowCollectionMap[mapKey] = it ?: "null" - updateRegistration(required = true) + onMapKeysChanged(added = true) emit(it) } } finally { flowCollectionMap.remove(mapKey) - updateRegistration(required = false) + onMapKeysChanged(added = false) } } override fun <T, F : StateFlow<T>> F.dumpValue(dumpName: String): F { stateFlowMap[dumpName] = this + onMapKeysChanged(added = true) return this } override fun <T, F : SharedFlow<T>> F.dumpReplayCache(dumpName: String): F { sharedFlowMap[dumpName] = this + onMapKeysChanged(added = true) return this } - private val dumpManagerName = tag ?: "[$idString] ${javaClass.simpleName}" + protected val Any.idString: String + get() = Integer.toHexString(System.identityHashCode(this)) +} + +/** + * An implementation of [FlowDumper] that registers itself whenever there is something to dump. This + * class is meant to be extended. + * + * @param dumpManager this will be used by the [FlowDumperImpl] to register and unregister itself + * when there is something to dump. + * @param tag a static name by which this [FlowDumperImpl] is registered. If not provided, this + * class's name will be used. + */ +abstract class FlowDumperImpl( + private val dumpManager: DumpManager, + private val tag: String? = null, +) : SimpleFlowDumper() { + + override fun onMapKeysChanged(added: Boolean) { + updateRegistration(required = added) + } + + private val dumpManagerName = "[$idString] ${tag ?: javaClass.simpleName}" + private var registered = AtomicBoolean(false) + private fun updateRegistration(required: Boolean) { - if (dumpManager == null) return if (required && registered.get()) return synchronized(registered) { - val shouldRegister = - stateFlowMap.isNotEmpty() || - sharedFlowMap.isNotEmpty() || - flowCollectionMap.isNotEmpty() + val shouldRegister = isNotEmpty() val wasRegistered = registered.getAndSet(shouldRegister) if (wasRegistered != shouldRegister) { if (shouldRegister) { @@ -140,3 +163,49 @@ open class FlowDumperImpl(private val dumpManager: DumpManager?, tag: String? = } } } + +/** + * A [FlowDumper] that also has an [activateFlowDumper] suspend function that allows the dumper to + * be registered with the [DumpManager] only when activated, just like + * [Activatable.activate()][com.android.systemui.lifecycle.Activatable.activate]. + */ +interface ActivatableFlowDumper : FlowDumper { + suspend fun activateFlowDumper() +} + +/** + * Implementation of [ActivatableFlowDumper] that only registers when activated. + * + * This is generally used to implement [ActivatableFlowDumper] by delegation, especially for + * [SysUiViewModel] implementations. + * + * @param dumpManager used to automatically register and unregister this instance when activated and + * there is something to dump. + * @param tag the name with which this is dumper registered. + */ +class ActivatableFlowDumperImpl( + private val dumpManager: DumpManager, + tag: String, +) : SimpleFlowDumper(), ActivatableFlowDumper { + + private val registration = + object : SafeActivatable() { + override suspend fun onActivated() { + try { + dumpManager.registerCriticalDumpable( + dumpManagerName, + this@ActivatableFlowDumperImpl + ) + awaitCancellation() + } finally { + dumpManager.unregisterDumpable(dumpManagerName) + } + } + } + + private val dumpManagerName = "[$idString] $tag" + + override suspend fun activateFlowDumper() { + registration.activate() + } +} 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/SettingsProxyExt.kt b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxyExt.kt index d757e33fcc29..364681444c1b 100644 --- a/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxyExt.kt +++ b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxyExt.kt @@ -19,6 +19,7 @@ package com.android.systemui.util.settings import android.annotation.UserIdInt import android.database.ContentObserver +import com.android.systemui.Flags import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow @@ -39,9 +40,21 @@ object SettingsProxyExt { } } - names.forEach { name -> registerContentObserverForUserSync(name, observer, userId) } + names.forEach { name -> + if (Flags.settingsExtRegisterContentObserverOnBgThread()) { + registerContentObserverForUser(name, observer, userId) + } else { + registerContentObserverForUserSync(name, observer, userId) + } + } - awaitClose { unregisterContentObserverSync(observer) } + awaitClose { + if (Flags.settingsExtRegisterContentObserverOnBgThread()) { + unregisterContentObserverAsync(observer) + } else { + unregisterContentObserverSync(observer) + } + } } } @@ -57,9 +70,21 @@ object SettingsProxyExt { } } - names.forEach { name -> registerContentObserverSync(name, observer) } + names.forEach { name -> + if (Flags.settingsExtRegisterContentObserverOnBgThread()) { + registerContentObserver(name, observer) + } else { + registerContentObserverSync(name, observer) + } + } - awaitClose { unregisterContentObserverSync(observer) } + awaitClose { + if (Flags.settingsExtRegisterContentObserverOnBgThread()) { + unregisterContentObserverAsync(observer) + } else { + unregisterContentObserverSync(observer) + } + } } } } 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/src/com/android/systemui/volume/dagger/AudioModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt index 5d8b6f144d97..d39daafd2311 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt @@ -79,13 +79,15 @@ interface AudioModule { localBluetoothManager: LocalBluetoothManager?, @Application coroutineScope: CoroutineScope, @Background coroutineContext: CoroutineContext, + volumeLogger: VolumeLogger ): AudioSharingRepository = if (Flags.enableLeAudioSharing() && localBluetoothManager != null) { AudioSharingRepositoryImpl( contentResolver, localBluetoothManager, coroutineScope, - coroutineContext + coroutineContext, + volumeLogger ) } else { AudioSharingRepositoryEmptyImpl() diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/data/repository/VolumePanelGlobalStateRepository.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/data/repository/VolumePanelGlobalStateRepository.kt index e46ce2699beb..24fb001a1b6d 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/data/repository/VolumePanelGlobalStateRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/data/repository/VolumePanelGlobalStateRepository.kt @@ -19,6 +19,7 @@ package com.android.systemui.volume.panel.data.repository import com.android.systemui.Dumpable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dump.DumpManager +import com.android.systemui.volume.panel.shared.VolumePanelLogger import com.android.systemui.volume.panel.shared.model.VolumePanelGlobalState import java.io.PrintWriter import javax.inject.Inject @@ -27,10 +28,15 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update -private const val TAG = "VolumePanelGlobalState" +private const val TAG = "VolumePanelGlobalStateRepository" @SysUISingleton -class VolumePanelGlobalStateRepository @Inject constructor(dumpManager: DumpManager) : Dumpable { +class VolumePanelGlobalStateRepository +@Inject +constructor( + dumpManager: DumpManager, + private val logger: VolumePanelLogger, +) : Dumpable { private val mutableGlobalState = MutableStateFlow( @@ -48,6 +54,7 @@ class VolumePanelGlobalStateRepository @Inject constructor(dumpManager: DumpMana update: (currentState: VolumePanelGlobalState) -> VolumePanelGlobalState ) { mutableGlobalState.update(update) + logger.onVolumePanelGlobalStateChanged(mutableGlobalState.value) } override fun dump(pw: PrintWriter, args: Array<out String>) { diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractor.kt index 5301b008bab7..9de862a814d6 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractor.kt @@ -19,6 +19,7 @@ package com.android.systemui.volume.panel.domain.interactor import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope import com.android.systemui.volume.panel.domain.ComponentAvailabilityCriteria import com.android.systemui.volume.panel.domain.model.ComponentModel +import com.android.systemui.volume.panel.shared.VolumePanelLogger import com.android.systemui.volume.panel.shared.model.VolumePanelComponentKey import javax.inject.Inject import javax.inject.Provider @@ -26,8 +27,12 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn interface ComponentsInteractor { @@ -45,6 +50,7 @@ constructor( enabledComponents: Collection<VolumePanelComponentKey>, defaultCriteria: Provider<ComponentAvailabilityCriteria>, @VolumePanelScope coroutineScope: CoroutineScope, + private val logger: VolumePanelLogger, private val criteriaByKey: Map< VolumePanelComponentKey, @@ -57,12 +63,18 @@ constructor( combine( enabledComponents.map { componentKey -> val componentCriteria = (criteriaByKey[componentKey] ?: defaultCriteria).get() - componentCriteria.isAvailable().map { isAvailable -> - ComponentModel(componentKey, isAvailable = isAvailable) - } + componentCriteria + .isAvailable() + .distinctUntilChanged() + .conflate() + .onEach { logger.onComponentAvailabilityChanged(componentKey, it) } + .map { isAvailable -> + ComponentModel(componentKey, isAvailable = isAvailable) + } } ) { it.asList() } - .shareIn(coroutineScope, SharingStarted.Eagerly, replay = 1) + .stateIn(coroutineScope, SharingStarted.Eagerly, null) + .filterNotNull() } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/shared/VolumePanelLogger.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/shared/VolumePanelLogger.kt index cc513b5d820c..276326cbf430 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/shared/VolumePanelLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/shared/VolumePanelLogger.kt @@ -20,15 +20,41 @@ import com.android.settingslib.volume.shared.model.AudioStream import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.LogLevel import com.android.systemui.log.dagger.VolumeLog -import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope +import com.android.systemui.volume.panel.shared.model.VolumePanelComponentKey +import com.android.systemui.volume.panel.shared.model.VolumePanelGlobalState +import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelState import javax.inject.Inject private const val TAG = "SysUI_VolumePanel" /** Logs events related to the Volume Panel. */ -@VolumePanelScope class VolumePanelLogger @Inject constructor(@VolumeLog private val logBuffer: LogBuffer) { + fun onVolumePanelStateChanged(state: VolumePanelState) { + logBuffer.log(TAG, LogLevel.DEBUG, { str1 = state.toString() }, { "State changed: $str1" }) + } + + fun onComponentAvailabilityChanged(key: VolumePanelComponentKey, isAvailable: Boolean) { + logBuffer.log( + TAG, + LogLevel.DEBUG, + { + str1 = key + bool1 = isAvailable + }, + { "$str1 isAvailable=$bool1" } + ) + } + + fun onVolumePanelGlobalStateChanged(globalState: VolumePanelGlobalState) { + logBuffer.log( + TAG, + LogLevel.DEBUG, + { bool1 = globalState.isVisible }, + { "Global state changed: isVisible=$bool1" } + ) + } + fun onSetVolumeRequested(audioStream: AudioStream, volume: Int) { logBuffer.log( TAG, diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/layout/ComponentsLayout.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/layout/ComponentsLayout.kt index 1c51236689d8..a06d3e3c6785 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/layout/ComponentsLayout.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/layout/ComponentsLayout.kt @@ -17,6 +17,7 @@ package com.android.systemui.volume.panel.ui.layout import com.android.systemui.volume.panel.ui.viewmodel.ComponentState +import com.android.systemui.volume.panel.ui.viewmodel.toLogString /** Represents components grouping into the layout. */ data class ComponentsLayout( @@ -29,3 +30,12 @@ data class ComponentsLayout( /** This is a separated entity that is always visible on the bottom of the Volume Panel. */ val bottomBarComponent: ComponentState, ) + +fun ComponentsLayout.toLogString(): String { + return "(" + + " headerComponents=${headerComponents.joinToString { it.toLogString() }}" + + " contentComponents=${contentComponents.joinToString { it.toLogString() }}" + + " footerComponents=${footerComponents.joinToString { it.toLogString() }}" + + " bottomBarComponent=${bottomBarComponent.toLogString()}" + + " )" +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/ComponentState.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/ComponentState.kt index 5f4dbfb4235e..41c80fa58527 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/ComponentState.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/ComponentState.kt @@ -32,3 +32,5 @@ data class ComponentState( val component: VolumePanelUiComponent, val isVisible: Boolean, ) + +fun ComponentState.toLogString(): String = "$key:visible=$isVisible" diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModel.kt index f495a02f6cc7..2f60c4b29a81 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModel.kt @@ -19,23 +19,30 @@ package com.android.systemui.volume.panel.ui.viewmodel import android.content.Context import android.content.IntentFilter import android.content.res.Resources +import com.android.systemui.Dumpable import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dump.DumpManager import com.android.systemui.res.R import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.onConfigChanged +import com.android.systemui.util.kotlin.launchAndDispose import com.android.systemui.volume.VolumePanelDialogReceiver import com.android.systemui.volume.panel.dagger.VolumePanelComponent import com.android.systemui.volume.panel.dagger.factory.VolumePanelComponentFactory import com.android.systemui.volume.panel.domain.VolumePanelStartable import com.android.systemui.volume.panel.domain.interactor.ComponentsInteractor import com.android.systemui.volume.panel.domain.interactor.VolumePanelGlobalStateInteractor +import com.android.systemui.volume.panel.shared.VolumePanelLogger import com.android.systemui.volume.panel.ui.composable.ComponentsFactory import com.android.systemui.volume.panel.ui.layout.ComponentsLayout import com.android.systemui.volume.panel.ui.layout.ComponentsLayoutManager +import com.android.systemui.volume.panel.ui.layout.toLogString +import java.io.PrintWriter import javax.inject.Inject import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.DisposableHandle +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine @@ -43,19 +50,23 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart -import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn +private const val TAG = "VolumePanelViewModel" + // Can't inject a constructor here because VolumePanelComponent provides this view model for its // components. +@OptIn(ExperimentalCoroutinesApi::class) class VolumePanelViewModel( resources: Resources, coroutineScope: CoroutineScope, daggerComponentFactory: VolumePanelComponentFactory, configurationController: ConfigurationController, broadcastDispatcher: BroadcastDispatcher, + private val dumpManager: DumpManager, + private val logger: VolumePanelLogger, private val volumePanelGlobalStateInteractor: VolumePanelGlobalStateInteractor, -) { +) : Dumpable { private val volumePanelComponent: VolumePanelComponent = daggerComponentFactory.create(this, coroutineScope) @@ -77,9 +88,10 @@ class VolumePanelViewModel( .onStart { emit(resources.configuration) } .map { configuration -> VolumePanelState( - orientation = configuration.orientation, - isLargeScreen = resources.getBoolean(R.bool.volume_panel_is_large_screen), - ) + orientation = configuration.orientation, + isLargeScreen = resources.getBoolean(R.bool.volume_panel_is_large_screen), + ) + .also { logger.onVolumePanelStateChanged(it) } } .stateIn( scope, @@ -89,7 +101,7 @@ class VolumePanelViewModel( isLargeScreen = resources.getBoolean(R.bool.volume_panel_is_large_screen) ), ) - val componentsLayout: Flow<ComponentsLayout> = + val componentsLayout: StateFlow<ComponentsLayout?> = combine( componentsInteractor.components, volumePanelState, @@ -104,13 +116,18 @@ class VolumePanelViewModel( } componentsLayoutManager.layout(scope, componentStates) } - .shareIn( + .stateIn( scope, SharingStarted.Eagerly, - replay = 1, + null, ) init { + scope.launchAndDispose { + dumpManager.registerNormalDumpable(TAG, this) + DisposableHandle { dumpManager.unregisterDumpable(TAG) } + } + volumePanelComponent.volumePanelStartables().onEach(VolumePanelStartable::start) broadcastDispatcher .broadcastFlow(IntentFilter(VolumePanelDialogReceiver.DISMISS_ACTION)) @@ -122,6 +139,13 @@ class VolumePanelViewModel( volumePanelGlobalStateInteractor.setVisible(false) } + override fun dump(pw: PrintWriter, args: Array<out String>) { + with(pw) { + println("volumePanelState=${volumePanelState.value}") + println("componentsLayout=${componentsLayout.value?.toLogString()}") + } + } + class Factory @Inject constructor( @@ -129,6 +153,8 @@ class VolumePanelViewModel( private val daggerComponentFactory: VolumePanelComponentFactory, private val configurationController: ConfigurationController, private val broadcastDispatcher: BroadcastDispatcher, + private val dumpManager: DumpManager, + private val logger: VolumePanelLogger, private val volumePanelGlobalStateInteractor: VolumePanelGlobalStateInteractor, ) { @@ -139,6 +165,8 @@ class VolumePanelViewModel( daggerComponentFactory, configurationController, broadcastDispatcher, + dumpManager, + logger, volumePanelGlobalStateInteractor, ) } diff --git a/packages/SystemUI/src/com/android/systemui/volume/shared/VolumeLogger.kt b/packages/SystemUI/src/com/android/systemui/volume/shared/VolumeLogger.kt index 869a82a78848..d6b159e9b13a 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/shared/VolumeLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/shared/VolumeLogger.kt @@ -16,7 +16,8 @@ package com.android.systemui.volume.shared -import com.android.settingslib.volume.data.repository.AudioRepositoryImpl +import com.android.settingslib.volume.shared.AudioLogger +import com.android.settingslib.volume.shared.AudioSharingLogger import com.android.settingslib.volume.shared.model.AudioStream import com.android.settingslib.volume.shared.model.AudioStreamModel import com.android.systemui.dagger.SysUISingleton @@ -30,7 +31,7 @@ private const val TAG = "SysUI_Volume" /** Logs general System UI volume events. */ @SysUISingleton class VolumeLogger @Inject constructor(@VolumeLog private val logBuffer: LogBuffer) : - AudioRepositoryImpl.Logger { + AudioLogger, AudioSharingLogger { override fun onSetVolumeRequested(audioStream: AudioStream, volume: Int) { logBuffer.log( @@ -55,4 +56,35 @@ class VolumeLogger @Inject constructor(@VolumeLog private val logBuffer: LogBuff { "Volume update received: stream=$str1 volume=$int1" } ) } + + override fun onAudioSharingStateChanged(state: Boolean) { + logBuffer.log( + TAG, + LogLevel.DEBUG, + { bool1 = state }, + { "Audio sharing state update: state=$bool1" } + ) + } + + override fun onSecondaryGroupIdChanged(groupId: Int) { + logBuffer.log( + TAG, + LogLevel.DEBUG, + { int1 = groupId }, + { "Secondary group id in audio sharing update: groupId=$int1" } + ) + } + + override fun onVolumeMapChanged(map: Map<Int, Int>) { + logBuffer.log( + TAG, + LogLevel.DEBUG, + { str1 = map.toString() }, + { "Volume map update: map=$str1" } + ) + } + + override fun onSetDeviceVolumeRequested(volume: Int) { + logBuffer.log(TAG, LogLevel.DEBUG, { int1 = volume }, { "Set device volume: volume=$int1" }) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java index 16b9ab5c1652..ff47fd1106bb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java @@ -48,6 +48,7 @@ import android.view.accessibility.IRemoteMagnificationAnimationCallback; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.systemui.Flags; import com.android.systemui.SysuiTestCase; import com.android.systemui.model.SysUiState; @@ -102,6 +103,8 @@ public class IMagnificationConnectionTest extends SysuiTestCase { private AccessibilityLogger mA11yLogger; @Mock private IWindowManager mIWindowManager; + @Mock + private ViewCaptureAwareWindowManager mViewCaptureAwareWindowManager; private IMagnificationConnection mIMagnificationConnection; private MagnificationImpl mMagnification; @@ -123,7 +126,8 @@ public class IMagnificationConnectionTest extends SysuiTestCase { mTestableLooper.getLooper(), mContext.getMainExecutor(), mCommandQueue, mModeSwitchesController, mSysUiState, mOverviewProxyService, mSecureSettings, mDisplayTracker, getContext().getSystemService(DisplayManager.class), - mA11yLogger, mIWindowManager, mAccessibilityManager); + mA11yLogger, mIWindowManager, mAccessibilityManager, + mViewCaptureAwareWindowManager); mMagnification.mWindowMagnificationControllerSupplier = new FakeWindowMagnificationControllerSupplier( mContext.getSystemService(DisplayManager.class)); 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/MagnificationSettingsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationSettingsControllerTest.java index d0f8e7863537..3cd3fefb8ef0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationSettingsControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationSettingsControllerTest.java @@ -27,6 +27,7 @@ import android.testing.TestableLooper; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.internal.graphics.SfVsyncFrameCallbackProvider; import com.android.systemui.SysuiTestCase; import com.android.systemui.accessibility.WindowMagnificationSettings.MagnificationSize; @@ -56,6 +57,8 @@ public class MagnificationSettingsControllerTest extends SysuiTestCase { private SfVsyncFrameCallbackProvider mSfVsyncFrameProvider; @Mock private SecureSettings mSecureSettings; + @Mock + private ViewCaptureAwareWindowManager mViewCaptureAwareWindowManager; @Before public void setUp() { @@ -63,7 +66,7 @@ public class MagnificationSettingsControllerTest extends SysuiTestCase { mMagnificationSettingsController = new MagnificationSettingsController( mContext, mSfVsyncFrameProvider, mMagnificationSettingControllerCallback, mSecureSettings, - mWindowMagnificationSettings); + mWindowMagnificationSettings, mViewCaptureAwareWindowManager); } @After diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationTest.java index 038b81b34d77..057ddcd54e68 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationTest.java @@ -50,6 +50,7 @@ import android.view.accessibility.IMagnificationConnectionCallback; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.systemui.SysuiTestCase; import com.android.systemui.model.SysUiState; import com.android.systemui.recents.OverviewProxyService; @@ -96,6 +97,8 @@ public class MagnificationTest extends SysuiTestCase { private AccessibilityLogger mA11yLogger; @Mock private IWindowManager mIWindowManager; + @Mock + private ViewCaptureAwareWindowManager mViewCaptureAwareWindowManager; @Before public void setUp() throws Exception { @@ -129,7 +132,8 @@ public class MagnificationTest extends SysuiTestCase { mCommandQueue, mModeSwitchesController, mSysUiState, mOverviewProxyService, mSecureSettings, mDisplayTracker, getContext().getSystemService(DisplayManager.class), mA11yLogger, mIWindowManager, - getContext().getSystemService(AccessibilityManager.class)); + getContext().getSystemService(AccessibilityManager.class), + mViewCaptureAwareWindowManager); mMagnification.mWindowMagnificationControllerSupplier = new FakeControllerSupplier( mContext.getSystemService(DisplayManager.class), mWindowMagnificationController); mMagnification.mMagnificationSettingsSupplier = new FakeSettingsSupplier( 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/accessibility/WindowMagnificationSettingsTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationSettingsTest.java index 003f7e4479ba..9507077a89ae 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationSettingsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationSettingsTest.java @@ -61,6 +61,8 @@ import androidx.test.InstrumentationRegistry; 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.common.ui.view.SeekBarWithIconButtonsView; @@ -68,6 +70,8 @@ import com.android.systemui.common.ui.view.SeekBarWithIconButtonsView.OnSeekBarW import com.android.systemui.res.R; import com.android.systemui.util.settings.SecureSettings; +import kotlin.Lazy; + import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -95,6 +99,8 @@ public class WindowMagnificationSettingsTest extends SysuiTestCase { private SecureSettings mSecureSettings; @Mock private WindowMagnificationSettingsCallback mWindowMagnificationSettingsCallback; + @Mock + private Lazy<ViewCapture> mLazyViewCapture; private TestableWindowManager mWindowManager; private WindowMagnificationSettings mWindowMagnificationSettings; private MotionEventHelper mMotionEventHelper = new MotionEventHelper(); @@ -119,9 +125,11 @@ public class WindowMagnificationSettingsTest extends SysuiTestCase { when(mSecureSettings.getFloatForUser(anyString(), anyFloat(), anyInt())).then( returnsSecondArg()); + ViewCaptureAwareWindowManager vwm = new ViewCaptureAwareWindowManager(mWindowManager, + mLazyViewCapture, /* isViewCaptureEnabled= */ false); mWindowMagnificationSettings = new WindowMagnificationSettings(mContext, mWindowMagnificationSettingsCallback, mSfVsyncFrameProvider, - mSecureSettings); + mSecureSettings, vwm); mSettingView = mWindowMagnificationSettings.getSettingView(); mZoomSeekbar = mSettingView.findViewById(R.id.magnifier_zoom_slider); diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesPresetsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesPresetsControllerTest.java new file mode 100644 index 000000000000..2ac5d105ba99 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesPresetsControllerTest.java @@ -0,0 +1,285 @@ +/* + * 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.accessibility.hearingaid; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import static java.util.Collections.emptyList; + +import android.bluetooth.BluetoothCsipSetCoordinator; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothHapClient; +import android.bluetooth.BluetoothHapPresetInfo; +import android.testing.TestableLooper; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.settingslib.bluetooth.HapClientProfile; +import com.android.settingslib.bluetooth.LocalBluetoothProfile; +import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; +import com.android.systemui.SysuiTestCase; + +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.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.util.List; +import java.util.concurrent.Executor; + +/** Tests for {@link HearingDevicesPresetsController}. */ +@RunWith(AndroidJUnit4.class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) +@SmallTest +public class HearingDevicesPresetsControllerTest extends SysuiTestCase { + + private static final int TEST_PRESET_INDEX = 1; + private static final String TEST_PRESET_NAME = "test_preset"; + private static final int TEST_HAP_GROUP_ID = 1; + private static final int TEST_REASON = 1024; + + @Rule + public MockitoRule mockito = MockitoJUnit.rule(); + + @Mock + private LocalBluetoothProfileManager mProfileManager; + @Mock + private HapClientProfile mHapClientProfile; + @Mock + private CachedBluetoothDevice mCachedBluetoothDevice; + @Mock + private CachedBluetoothDevice mSubCachedBluetoothDevice; + @Mock + private BluetoothDevice mBluetoothDevice; + @Mock + private BluetoothDevice mSubBluetoothDevice; + + @Mock + private HearingDevicesPresetsController.PresetCallback mCallback; + + private HearingDevicesPresetsController mController; + + @Before + public void setUp() { + when(mProfileManager.getHapClientProfile()).thenReturn(mHapClientProfile); + when(mHapClientProfile.isProfileReady()).thenReturn(true); + when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); + when(mCachedBluetoothDevice.getSubDevice()).thenReturn(mSubCachedBluetoothDevice); + when(mSubCachedBluetoothDevice.getDevice()).thenReturn(mSubBluetoothDevice); + + mController = new HearingDevicesPresetsController(mProfileManager, mCallback); + } + + @Test + public void onServiceConnected_callExpectedCallback() { + mController.onServiceConnected(); + + verify(mHapClientProfile).registerCallback(any(Executor.class), + any(BluetoothHapClient.Callback.class)); + verify(mCallback).onPresetInfoUpdated(anyList(), anyInt()); + } + + @Test + public void getAllPresetInfo_setInvalidHearingDevice_getEmpty() { + when(mCachedBluetoothDevice.getProfiles()).thenReturn(emptyList()); + mController.setHearingDeviceIfSupportHap(mCachedBluetoothDevice); + BluetoothHapPresetInfo hapPresetInfo = getHapPresetInfo(true); + when(mHapClientProfile.getAllPresetInfo(mBluetoothDevice)).thenReturn( + List.of(hapPresetInfo)); + + assertThat(mController.getAllPresetInfo()).isEmpty(); + } + + @Test + public void getAllPresetInfo_containsNotAvailablePresetInfo_getEmpty() { + setValidHearingDeviceSupportHap(); + BluetoothHapPresetInfo hapPresetInfo = getHapPresetInfo(false); + when(mHapClientProfile.getAllPresetInfo(mBluetoothDevice)).thenReturn( + List.of(hapPresetInfo)); + + assertThat(mController.getAllPresetInfo()).isEmpty(); + } + + @Test + public void getAllPresetInfo_containsOnePresetInfo_getOnePresetInfo() { + setValidHearingDeviceSupportHap(); + BluetoothHapPresetInfo hapPresetInfo = getHapPresetInfo(true); + when(mHapClientProfile.getAllPresetInfo(mBluetoothDevice)).thenReturn( + List.of(hapPresetInfo)); + + assertThat(mController.getAllPresetInfo()).contains(hapPresetInfo); + } + + @Test + public void getActivePresetIndex_getExpectedIndex() { + setValidHearingDeviceSupportHap(); + when(mHapClientProfile.getActivePresetIndex(mBluetoothDevice)).thenReturn( + TEST_PRESET_INDEX); + + assertThat(mController.getActivePresetIndex()).isEqualTo(TEST_PRESET_INDEX); + } + + @Test + public void onPresetSelected_presetIndex_callOnPresetInfoUpdatedWithExpectedPresetIndex() { + setValidHearingDeviceSupportHap(); + BluetoothHapPresetInfo hapPresetInfo = getHapPresetInfo(true); + when(mHapClientProfile.getAllPresetInfo(mBluetoothDevice)).thenReturn( + List.of(hapPresetInfo)); + when(mHapClientProfile.getActivePresetIndex(mBluetoothDevice)).thenReturn( + TEST_PRESET_INDEX); + + mController.onPresetSelected(mBluetoothDevice, TEST_PRESET_INDEX, TEST_REASON); + + verify(mCallback).onPresetInfoUpdated(eq(List.of(hapPresetInfo)), eq(TEST_PRESET_INDEX)); + } + + @Test + public void onPresetInfoChanged_presetIndex_callOnPresetInfoUpdatedWithExpectedPresetIndex() { + setValidHearingDeviceSupportHap(); + BluetoothHapPresetInfo hapPresetInfo = getHapPresetInfo(true); + when(mHapClientProfile.getAllPresetInfo(mBluetoothDevice)).thenReturn( + List.of(hapPresetInfo)); + when(mHapClientProfile.getActivePresetIndex(mBluetoothDevice)).thenReturn( + TEST_PRESET_INDEX); + + mController.onPresetInfoChanged(mBluetoothDevice, List.of(hapPresetInfo), TEST_REASON); + + verify(mCallback).onPresetInfoUpdated(List.of(hapPresetInfo), TEST_PRESET_INDEX); + } + + @Test + public void onPresetSelectionFailed_callOnPresetCommandFailed() { + setValidHearingDeviceSupportHap(); + + mController.onPresetSelectionFailed(mBluetoothDevice, TEST_REASON); + + verify(mCallback).onPresetCommandFailed(TEST_REASON); + } + + @Test + public void onSetPresetNameFailed_callOnPresetCommandFailed() { + setValidHearingDeviceSupportHap(); + + mController.onSetPresetNameFailed(mBluetoothDevice, TEST_REASON); + + verify(mCallback).onPresetCommandFailed(TEST_REASON); + } + + @Test + public void onPresetSelectionForGroupFailed_callSelectPresetIndividual() { + setValidHearingDeviceSupportHap(); + mController.selectPreset(TEST_PRESET_INDEX); + Mockito.reset(mHapClientProfile); + when(mHapClientProfile.getHapGroup(mBluetoothDevice)).thenReturn(TEST_HAP_GROUP_ID); + + mController.onPresetSelectionForGroupFailed(TEST_HAP_GROUP_ID, TEST_REASON); + + + verify(mHapClientProfile).selectPreset(mBluetoothDevice, TEST_PRESET_INDEX); + verify(mHapClientProfile).selectPreset(mSubBluetoothDevice, TEST_PRESET_INDEX); + } + + @Test + public void onSetPresetNameForGroupFailed_callOnPresetCommandFailed() { + setValidHearingDeviceSupportHap(); + + mController.onSetPresetNameForGroupFailed(TEST_HAP_GROUP_ID, TEST_REASON); + + verify(mCallback).onPresetCommandFailed(TEST_REASON); + } + + @Test + public void registerHapCallback_callHapRegisterCallback() { + mController.registerHapCallback(); + + verify(mHapClientProfile).registerCallback(any(Executor.class), + any(BluetoothHapClient.Callback.class)); + } + + @Test + public void unregisterHapCallback_callHapUnregisterCallback() { + mController.unregisterHapCallback(); + + verify(mHapClientProfile).unregisterCallback(any(BluetoothHapClient.Callback.class)); + } + + @Test + public void selectPreset_supportSynchronized_validGroupId_callSelectPresetForGroup() { + setValidHearingDeviceSupportHap(); + when(mHapClientProfile.supportsSynchronizedPresets(mBluetoothDevice)).thenReturn(true); + when(mHapClientProfile.getHapGroup(mBluetoothDevice)).thenReturn(TEST_HAP_GROUP_ID); + + mController.selectPreset(TEST_PRESET_INDEX); + + verify(mHapClientProfile).selectPresetForGroup(TEST_HAP_GROUP_ID, TEST_PRESET_INDEX); + } + + @Test + public void selectPreset_supportSynchronized_invalidGroupId_callSelectPresetIndividual() { + setValidHearingDeviceSupportHap(); + when(mHapClientProfile.supportsSynchronizedPresets(mBluetoothDevice)).thenReturn(true); + when(mHapClientProfile.getHapGroup(mBluetoothDevice)).thenReturn( + BluetoothCsipSetCoordinator.GROUP_ID_INVALID); + + mController.selectPreset(TEST_PRESET_INDEX); + + verify(mHapClientProfile).selectPreset(mBluetoothDevice, TEST_PRESET_INDEX); + verify(mHapClientProfile).selectPreset(mSubBluetoothDevice, TEST_PRESET_INDEX); + } + + @Test + public void selectPreset_notSupportSynchronized_validGroupId_callSelectPresetIndividual() { + setValidHearingDeviceSupportHap(); + when(mHapClientProfile.supportsSynchronizedPresets(mBluetoothDevice)).thenReturn(false); + when(mHapClientProfile.getHapGroup(mBluetoothDevice)).thenReturn(TEST_HAP_GROUP_ID); + + mController.selectPreset(TEST_PRESET_INDEX); + + verify(mHapClientProfile).selectPreset(mBluetoothDevice, TEST_PRESET_INDEX); + verify(mHapClientProfile).selectPreset(mSubBluetoothDevice, TEST_PRESET_INDEX); + } + + private BluetoothHapPresetInfo getHapPresetInfo(boolean available) { + BluetoothHapPresetInfo info = mock(BluetoothHapPresetInfo.class); + when(info.getName()).thenReturn(TEST_PRESET_NAME); + when(info.getIndex()).thenReturn(TEST_PRESET_INDEX); + when(info.isAvailable()).thenReturn(available); + return info; + } + + private void setValidHearingDeviceSupportHap() { + LocalBluetoothProfile hapClientProfile = mock(HapClientProfile.class); + List<LocalBluetoothProfile> profiles = List.of(hapClientProfile); + when(mCachedBluetoothDevice.getProfiles()).thenReturn(profiles); + + mController.setHearingDeviceIfSupportHap(mCachedBluetoothDevice); + } +} 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/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt index dc69cdadc320..f94a6f24a106 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt @@ -29,7 +29,6 @@ import android.hardware.biometrics.PromptVerticalListContentView import android.hardware.face.FaceSensorPropertiesInternal import android.hardware.fingerprint.FingerprintManager import android.hardware.fingerprint.FingerprintSensorPropertiesInternal -import android.os.Handler import android.os.IBinder import android.os.UserManager import android.testing.TestableLooper @@ -45,7 +44,6 @@ import androidx.test.filters.SmallTest import com.android.internal.jank.InteractionJankMonitor import com.android.internal.widget.LockPatternUtils import com.android.launcher3.icons.IconProvider -import com.android.systemui.Flags.FLAG_CONSTRAINT_BP import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.data.repository.FakeBiometricStatusRepository import com.android.systemui.biometrics.data.repository.FakeDisplayStateRepository @@ -105,7 +103,6 @@ open class AuthContainerViewTest : SysuiTestCase() { @Mock lateinit var fingerprintManager: FingerprintManager @Mock lateinit var lockPatternUtils: LockPatternUtils @Mock lateinit var wakefulnessLifecycle: WakefulnessLifecycle - @Mock lateinit var panelInteractionDetector: AuthDialogPanelInteractionDetector @Mock lateinit var windowToken: IBinder @Mock lateinit var interactionJankMonitor: InteractionJankMonitor @Mock lateinit var vibrator: VibratorHelper @@ -265,14 +262,6 @@ open class AuthContainerViewTest : SysuiTestCase() { } @Test - fun testActionCancel_panelInteractionDetectorDisable() { - val container = initializeFingerprintContainer() - container.mBiometricCallback.onUserCanceled() - waitForIdleSync() - verify(panelInteractionDetector).disable() - } - - @Test fun testActionAuthenticated_sendsDismissedAuthenticated() { val container = initializeFingerprintContainer() container.mBiometricCallback.onAuthenticated() @@ -416,19 +405,7 @@ open class AuthContainerViewTest : SysuiTestCase() { } @Test - fun testShowBiometricUI() { - mSetFlagsRule.disableFlags(FLAG_CONSTRAINT_BP) - val container = initializeFingerprintContainer() - - waitForIdleSync() - - assertThat(container.hasCredentialView()).isFalse() - assertThat(container.hasBiometricPrompt()).isTrue() - } - - @Test fun testShowBiometricUI_ContentViewWithMoreOptionsButton() { - mSetFlagsRule.enableFlags(FLAG_CONSTRAINT_BP) mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) var isButtonClicked = false val contentView = @@ -466,7 +443,6 @@ open class AuthContainerViewTest : SysuiTestCase() { @Test fun testShowCredentialUI_withVerticalListContentView() { - mSetFlagsRule.enableFlags(FLAG_CONSTRAINT_BP) mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) val container = initializeFingerprintContainer( @@ -488,7 +464,6 @@ open class AuthContainerViewTest : SysuiTestCase() { @Test fun testShowCredentialUI_withContentViewWithMoreOptionsButton() { - mSetFlagsRule.enableFlags(FLAG_CONSTRAINT_BP) mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) val contentView = PromptContentViewWithMoreOptionsButton.Builder() @@ -674,7 +649,6 @@ open class AuthContainerViewTest : SysuiTestCase() { fingerprintProps, faceProps, wakefulnessLifecycle, - panelInteractionDetector, userManager, lockPatternUtils, interactionJankMonitor, @@ -690,7 +664,6 @@ open class AuthContainerViewTest : SysuiTestCase() { activityTaskManager ), { credentialViewModel }, - Handler(TestableLooper.get(this).looper), fakeExecutor, vibrator ) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetectorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetectorTest.kt deleted file mode 100644 index 023148603b50..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetectorTest.kt +++ /dev/null @@ -1,156 +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.biometrics - -import android.platform.test.flag.junit.FlagsParameterization -import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import com.android.systemui.flags.andSceneContainer -import com.android.systemui.kosmos.applicationCoroutineScope -import com.android.systemui.kosmos.testScope -import com.android.systemui.shade.domain.interactor.shadeInteractor -import com.android.systemui.shade.shadeTestUtil -import com.android.systemui.testKosmos -import kotlinx.coroutines.test.runCurrent -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.Mockito.verify -import org.mockito.MockitoAnnotations -import org.mockito.kotlin.verifyZeroInteractions -import platform.test.runner.parameterized.ParameterizedAndroidJunit4 -import platform.test.runner.parameterized.Parameters - -@SmallTest -@RunWith(ParameterizedAndroidJunit4::class) -class AuthDialogPanelInteractionDetectorTest(flags: FlagsParameterization?) : SysuiTestCase() { - - companion object { - @JvmStatic - @Parameters(name = "{0}") - fun getParams(): List<FlagsParameterization> { - return FlagsParameterization.allCombinationsOf().andSceneContainer() - } - } - - init { - mSetFlagsRule.setFlagsParameterization(flags!!) - } - - private val kosmos = testKosmos() - private val testScope = kosmos.testScope - - private val shadeTestUtil by lazy { kosmos.shadeTestUtil } - - @Mock private lateinit var action: Runnable - - lateinit var detector: AuthDialogPanelInteractionDetector - - @Before - fun setUp() { - MockitoAnnotations.initMocks(this) - detector = - AuthDialogPanelInteractionDetector( - kosmos.applicationCoroutineScope, - { kosmos.shadeInteractor }, - ) - } - - @Test - fun enableDetector_expand_shouldRunAction() = - testScope.runTest { - // GIVEN shade is closed and detector is enabled - shadeTestUtil.setShadeExpansion(0f) - detector.enable(action) - runCurrent() - - // WHEN shade expands - shadeTestUtil.setTracking(true) - shadeTestUtil.setShadeExpansion(.5f) - runCurrent() - - // THEN action was run - verify(action).run() - } - - @Test - fun enableDetector_isUserInteractingTrue_shouldNotPostRunnable() = - testScope.runTest { - // GIVEN isInteracting starts true - shadeTestUtil.setTracking(true) - runCurrent() - detector.enable(action) - - // THEN action was not run - verifyZeroInteractions(action) - } - - @Test - fun enableDetector_shadeExpandImmediate_shouldNotPostRunnable() = - testScope.runTest { - // GIVEN shade is closed and detector is enabled - shadeTestUtil.setShadeExpansion(0f) - detector.enable(action) - runCurrent() - - // WHEN shade expands fully instantly - shadeTestUtil.setShadeExpansion(1f) - runCurrent() - - // THEN action not run - verifyZeroInteractions(action) - detector.disable() - } - - @Test - fun disableDetector_shouldNotPostRunnable() = - testScope.runTest { - // GIVEN shade is closed and detector is enabled - shadeTestUtil.setShadeExpansion(0f) - detector.enable(action) - runCurrent() - - // WHEN detector is disabled and shade opens - detector.disable() - runCurrent() - shadeTestUtil.setTracking(true) - shadeTestUtil.setShadeExpansion(.5f) - runCurrent() - - // THEN action not run - verifyZeroInteractions(action) - } - - @Test - fun enableDetector_beginCollapse_shouldNotPostRunnable() = - testScope.runTest { - // GIVEN shade is open and detector is enabled - shadeTestUtil.setShadeExpansion(1f) - detector.enable(action) - runCurrent() - - // WHEN shade begins to collapse - shadeTestUtil.programmaticCollapseShade() - runCurrent() - - // THEN action not run - verifyZeroInteractions(action) - detector.disable() - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt index 720f2071ac73..dc499cd2fe8d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt @@ -155,7 +155,7 @@ class PromptSelectorInteractorImplTest : SysuiTestCase() { Authenticators.BIOMETRIC_STRONG } isDeviceCredentialAllowed = allowCredentialFallback - componentNameForConfirmDeviceCredentialActivity = + realCallerForConfirmDeviceCredentialActivity = if (setComponentNameForConfirmDeviceCredentialActivity) componentNameOverriddenForConfirmDeviceCredentialActivity else null diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt index 534f25c33900..6047e7d1bf79 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt @@ -43,7 +43,6 @@ import android.view.MotionEvent import android.view.Surface import androidx.test.filters.SmallTest import com.android.app.activityTaskManager -import com.android.systemui.Flags.FLAG_CONSTRAINT_BP import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.AuthController import com.android.systemui.biometrics.Utils.toBitmap @@ -1385,7 +1384,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT, FLAG_CONSTRAINT_BP) + @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) fun descriptionOverriddenByVerticalListContentView() = runGenericTest(description = "test description", contentView = promptContentView) { val contentView by collectLastValue(kosmos.promptViewModel.contentView) @@ -1396,7 +1395,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT, FLAG_CONSTRAINT_BP) + @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) fun descriptionOverriddenByContentViewWithMoreOptionsButton() = runGenericTest( description = "test description", @@ -1410,7 +1409,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT, FLAG_CONSTRAINT_BP) + @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) fun descriptionWithoutContentView() = runGenericTest(description = "test description") { val contentView by collectLastValue(kosmos.promptViewModel.contentView) @@ -1421,7 +1420,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT, FLAG_CONSTRAINT_BP) + @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) fun logo_nullIfPkgNameNotFound() = runGenericTest(packageName = OP_PACKAGE_NAME_CAN_NOT_BE_FOUND) { val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo) @@ -1430,7 +1429,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT, FLAG_CONSTRAINT_BP) + @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) fun logo_defaultFromActivityInfo() = runGenericTest(packageName = OP_PACKAGE_NAME_WITH_ACTIVITY_LOGO) { val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo) @@ -1445,7 +1444,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT, FLAG_CONSTRAINT_BP) + @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) fun logo_defaultIsNull() = runGenericTest(packageName = OP_PACKAGE_NAME_NO_ICON) { val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo) @@ -1454,7 +1453,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT, FLAG_CONSTRAINT_BP) + @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) fun logo_default() = runGenericTest { val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo) assertThat(logoInfo).isNotNull() @@ -1462,7 +1461,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT, FLAG_CONSTRAINT_BP) + @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) fun logo_resSetByApp() = runGenericTest(logoRes = logoResFromApp) { val expectedBitmap = context.getDrawable(logoResFromApp).toBitmap() @@ -1472,7 +1471,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT, FLAG_CONSTRAINT_BP) + @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) fun logo_bitmapSetByApp() = runGenericTest(logoBitmap = logoBitmapFromApp) { val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo) @@ -1480,7 +1479,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT, FLAG_CONSTRAINT_BP) + @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) fun logoDescription_emptyIfPkgNameNotFound() = runGenericTest(packageName = OP_PACKAGE_NAME_CAN_NOT_BE_FOUND) { val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo) @@ -1488,7 +1487,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT, FLAG_CONSTRAINT_BP) + @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) fun logoDescription_defaultFromActivityInfo() = runGenericTest(packageName = OP_PACKAGE_NAME_WITH_ACTIVITY_LOGO) { val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo) @@ -1500,7 +1499,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT, FLAG_CONSTRAINT_BP) + @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) fun logoDescription_defaultIsEmpty() = runGenericTest(packageName = OP_PACKAGE_NAME_NO_ICON) { val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo) @@ -1508,14 +1507,14 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT, FLAG_CONSTRAINT_BP) + @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) fun logoDescription_default() = runGenericTest { val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo) assertThat(logoInfo!!.second).isEqualTo(defaultLogoDescriptionFromAppInfo) } @Test - @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT, FLAG_CONSTRAINT_BP) + @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) fun logoDescription_setByApp() = runGenericTest(logoDescription = logoDescriptionFromApp) { val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo) @@ -1523,7 +1522,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CONSTRAINT_BP) fun position_bottom_rotation0() = runGenericTest { kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_0) val position by collectLastValue(kosmos.promptViewModel.position) @@ -1531,7 +1529,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } // TODO(b/335278136): Add test for no sensor landscape @Test - @EnableFlags(FLAG_CONSTRAINT_BP) fun position_bottom_forceLarge() = runGenericTest { kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_270) kosmos.promptViewModel.onSwitchToCredential() @@ -1540,7 +1537,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CONSTRAINT_BP) fun position_bottom_largeScreen() = runGenericTest { kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_270) kosmos.displayStateRepository.setIsLargeScreen(true) @@ -1549,7 +1545,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CONSTRAINT_BP) fun position_right_rotation90() = runGenericTest { kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_90) val position by collectLastValue(kosmos.promptViewModel.position) @@ -1557,7 +1552,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CONSTRAINT_BP) fun position_left_rotation270() = runGenericTest { kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_270) val position by collectLastValue(kosmos.promptViewModel.position) @@ -1565,7 +1559,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CONSTRAINT_BP) fun position_top_rotation180() = runGenericTest { kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_180) val position by collectLastValue(kosmos.promptViewModel.position) @@ -1577,7 +1570,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CONSTRAINT_BP) fun guideline_bottom() = runGenericTest { kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_0) val guidelineBounds by collectLastValue(kosmos.promptViewModel.guidelineBounds) @@ -1585,7 +1577,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } // TODO(b/335278136): Add test for no sensor landscape @Test - @EnableFlags(FLAG_CONSTRAINT_BP) fun guideline_right() = runGenericTest { kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_90) @@ -1602,7 +1593,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CONSTRAINT_BP) fun guideline_right_onlyShortTitle() = runGenericTest(subtitle = "") { kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_90) @@ -1617,7 +1607,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CONSTRAINT_BP) fun guideline_left() = runGenericTest { kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_270) @@ -1634,7 +1623,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CONSTRAINT_BP) fun guideline_left_onlyShortTitle() = runGenericTest(subtitle = "") { kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_270) @@ -1649,7 +1637,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CONSTRAINT_BP) fun guideline_top() = runGenericTest { kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_180) val guidelineBounds by collectLastValue(kosmos.promptViewModel.guidelineBounds) diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt index 3b2cf61dde68..0db7b62b8ef1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt @@ -32,7 +32,6 @@ import androidx.test.filters.SmallTest import com.airbnb.lottie.model.KeyPath import com.android.keyguard.keyguardUpdateMonitor import com.android.settingslib.Utils -import com.android.systemui.Flags.FLAG_CONSTRAINT_BP import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.FingerprintInteractiveToAuthProvider import com.android.systemui.biometrics.data.repository.biometricStatusRepository @@ -199,7 +198,6 @@ class SideFpsOverlayViewModelTest : SysuiTestCase() { @Test fun updatesOverlayViewParams_onDisplayRotationChange_xAlignedSensor() { kosmos.testScope.runTest { - mSetFlagsRule.disableFlags(FLAG_CONSTRAINT_BP) setupTestConfiguration( DeviceConfig.X_ALIGNED, rotation = DisplayRotation.ROTATION_0, @@ -230,11 +228,11 @@ class SideFpsOverlayViewModelTest : SysuiTestCase() { .isEqualTo( displayWidth - sensorLocation.sensorLocationX - sensorLocation.sensorRadius * 2 ) - assertThat(overlayViewParams!!.y).isEqualTo(displayHeight - boundsHeight) + assertThat(overlayViewParams!!.y).isEqualTo(displayHeight) kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_270) assertThat(overlayViewParams).isNotNull() - assertThat(overlayViewParams!!.x).isEqualTo(displayWidth - boundsWidth) + assertThat(overlayViewParams!!.x).isEqualTo(displayWidth) assertThat(overlayViewParams!!.y).isEqualTo(sensorLocation.sensorLocationX) } } @@ -242,7 +240,6 @@ class SideFpsOverlayViewModelTest : SysuiTestCase() { @Test fun updatesOverlayViewParams_onDisplayRotationChange_yAlignedSensor() { kosmos.testScope.runTest { - mSetFlagsRule.disableFlags(FLAG_CONSTRAINT_BP) setupTestConfiguration( DeviceConfig.Y_ALIGNED, rotation = DisplayRotation.ROTATION_0, @@ -256,7 +253,7 @@ class SideFpsOverlayViewModelTest : SysuiTestCase() { runCurrent() assertThat(overlayViewParams).isNotNull() - assertThat(overlayViewParams!!.x).isEqualTo(displayWidth - boundsWidth) + assertThat(overlayViewParams!!.x).isEqualTo(displayWidth) assertThat(overlayViewParams!!.y).isEqualTo(sensorLocation.sensorLocationY) kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_90) @@ -278,7 +275,7 @@ class SideFpsOverlayViewModelTest : SysuiTestCase() { .isEqualTo( displayWidth - sensorLocation.sensorLocationY - sensorLocation.sensorRadius * 2 ) - assertThat(overlayViewParams!!.y).isEqualTo(displayHeight - boundsHeight) + assertThat(overlayViewParams!!.y).isEqualTo(displayHeight) } } 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/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java index e3a38a8d6763..37f1a3d73b0c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java @@ -443,7 +443,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { mViewMediator.onSystemReady(); TestableLooper.get(this).processAllMessages(); - mViewMediator.setShowingLocked(false); + mViewMediator.setShowingLocked(false, ""); TestableLooper.get(this).processAllMessages(); mViewMediator.onStartedGoingToSleep(OFF_BECAUSE_OF_USER); @@ -463,7 +463,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { mViewMediator.onSystemReady(); TestableLooper.get(this).processAllMessages(); - mViewMediator.setShowingLocked(false); + mViewMediator.setShowingLocked(false, ""); TestableLooper.get(this).processAllMessages(); mViewMediator.onStartedGoingToSleep(OFF_BECAUSE_OF_USER); @@ -570,7 +570,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { // When showing and provisioned mViewMediator.onSystemReady(); when(mUpdateMonitor.isDeviceProvisioned()).thenReturn(true); - mViewMediator.setShowingLocked(true); + mViewMediator.setShowingLocked(true, ""); // and a SIM becomes locked and requires a PIN mViewMediator.mUpdateCallback.onSimStateChanged( @@ -579,7 +579,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { TelephonyManager.SIM_STATE_PIN_REQUIRED); // and the keyguard goes away - mViewMediator.setShowingLocked(false); + mViewMediator.setShowingLocked(false, ""); when(mKeyguardStateController.isShowing()).thenReturn(false); mViewMediator.mUpdateCallback.onKeyguardVisibilityChanged(false); @@ -595,7 +595,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { // When showing and provisioned mViewMediator.onSystemReady(); when(mUpdateMonitor.isDeviceProvisioned()).thenReturn(true); - mViewMediator.setShowingLocked(false); + mViewMediator.setShowingLocked(false, ""); // and a SIM becomes locked and requires a PIN mViewMediator.mUpdateCallback.onSimStateChanged( @@ -604,7 +604,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { TelephonyManager.SIM_STATE_PIN_REQUIRED); // and the keyguard goes away - mViewMediator.setShowingLocked(false); + mViewMediator.setShowingLocked(false, ""); when(mKeyguardStateController.isShowing()).thenReturn(false); mViewMediator.mUpdateCallback.onKeyguardVisibilityChanged(false); @@ -843,7 +843,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { mViewMediator.onSystemReady(); TestableLooper.get(this).processAllMessages(); - mViewMediator.setShowingLocked(false); + mViewMediator.setShowingLocked(false, ""); TestableLooper.get(this).processAllMessages(); mViewMediator.onStartedGoingToSleep(OFF_BECAUSE_OF_USER); @@ -863,7 +863,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { mViewMediator.onSystemReady(); TestableLooper.get(this).processAllMessages(); - mViewMediator.setShowingLocked(false); + mViewMediator.setShowingLocked(false, ""); TestableLooper.get(this).processAllMessages(); mViewMediator.onStartedGoingToSleep(OFF_BECAUSE_OF_USER); @@ -978,7 +978,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { @Test @TestableLooper.RunWithLooper(setAsMainLooper = true) public void testDoKeyguardWhileInteractive_resets() { - mViewMediator.setShowingLocked(true); + mViewMediator.setShowingLocked(true, ""); when(mKeyguardStateController.isShowing()).thenReturn(true); TestableLooper.get(this).processAllMessages(); @@ -992,7 +992,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { @Test @TestableLooper.RunWithLooper(setAsMainLooper = true) public void testDoKeyguardWhileNotInteractive_showsInsteadOfResetting() { - mViewMediator.setShowingLocked(true); + mViewMediator.setShowingLocked(true, ""); when(mKeyguardStateController.isShowing()).thenReturn(true); TestableLooper.get(this).processAllMessages(); @@ -1051,7 +1051,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { mViewMediator.onSystemReady(); processAllMessagesAndBgExecutorMessages(); - mViewMediator.setShowingLocked(true); + mViewMediator.setShowingLocked(true, ""); RemoteAnimationTarget[] apps = new RemoteAnimationTarget[]{ mock(RemoteAnimationTarget.class) @@ -1123,7 +1123,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { @Test @TestableLooper.RunWithLooper(setAsMainLooper = true) public void testNotStartingKeyguardWhenFlagIsDisabled() { - mViewMediator.setShowingLocked(false); + mViewMediator.setShowingLocked(false, ""); when(mKeyguardStateController.isShowing()).thenReturn(false); mViewMediator.onDreamingStarted(); @@ -1133,7 +1133,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { @Test @TestableLooper.RunWithLooper(setAsMainLooper = true) public void testStartingKeyguardWhenFlagIsEnabled() { - mViewMediator.setShowingLocked(true); + mViewMediator.setShowingLocked(true, ""); when(mKeyguardStateController.isShowing()).thenReturn(true); mViewMediator.onDreamingStarted(); @@ -1174,7 +1174,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { TestableLooper.get(this).processAllMessages(); // WHEN keyguard visibility becomes FALSE - mViewMediator.setShowingLocked(false); + mViewMediator.setShowingLocked(false, ""); keyguardUpdateMonitorCallback.onKeyguardVisibilityChanged(false); TestableLooper.get(this).processAllMessages(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt index 2af4d872f6a0..bfe89de6229d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt @@ -17,9 +17,6 @@ package com.android.systemui.keyguard.data.repository import android.animation.ValueAnimator -import android.util.Log -import android.util.Log.TerribleFailure -import android.util.Log.TerribleFailureHandler import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.FlakyTest import androidx.test.filters.SmallTest @@ -53,7 +50,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest -import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -67,23 +63,14 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() { private val testScope = kosmos.testScope private lateinit var underTest: KeyguardTransitionRepository - private lateinit var oldWtfHandler: TerribleFailureHandler - private lateinit var wtfHandler: WtfHandler private lateinit var runner: KeyguardTransitionRunner @Before fun setUp() { underTest = KeyguardTransitionRepositoryImpl(Dispatchers.Main) - wtfHandler = WtfHandler() - oldWtfHandler = Log.setWtfHandler(wtfHandler) runner = KeyguardTransitionRunner(underTest) } - @After - fun tearDown() { - oldWtfHandler?.let { Log.setWtfHandler(it) } - } - @Test fun startTransitionRunsAnimatorToCompletion() = testScope.runTest { @@ -333,15 +320,17 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() { } @Test - fun attemptTomanuallyUpdateTransitionWithInvalidUUIDthrowsException() = + fun attemptTomanuallyUpdateTransitionWithInvalidUUIDEmitsNothing() = testScope.runTest { + val steps by collectValues(underTest.transitions.dropWhile { step -> step.from == OFF }) underTest.updateTransition(UUID.randomUUID(), 0f, TransitionState.RUNNING) - assertThat(wtfHandler.failed).isTrue() + assertThat(steps.size).isEqualTo(0) } @Test - fun attemptToManuallyUpdateTransitionAfterFINISHEDstateThrowsException() = + fun attemptToManuallyUpdateTransitionAfterFINISHEDstateEmitsNothing() = testScope.runTest { + val steps by collectValues(underTest.transitions.dropWhile { step -> step.from == OFF }) val uuid = underTest.startTransition( TransitionInfo( @@ -356,12 +345,19 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() { underTest.updateTransition(it, 1f, TransitionState.FINISHED) underTest.updateTransition(it, 0.5f, TransitionState.RUNNING) } - assertThat(wtfHandler.failed).isTrue() + assertThat(steps.size).isEqualTo(2) + assertThat(steps[0]) + .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 0f, TransitionState.STARTED, OWNER_NAME)) + assertThat(steps[1]) + .isEqualTo( + TransitionStep(AOD, LOCKSCREEN, 1f, TransitionState.FINISHED, OWNER_NAME) + ) } @Test - fun attemptToManuallyUpdateTransitionAfterCANCELEDstateThrowsException() = + fun attemptToManuallyUpdateTransitionAfterCANCELEDstateEmitsNothing() = testScope.runTest { + val steps by collectValues(underTest.transitions.dropWhile { step -> step.from == OFF }) val uuid = underTest.startTransition( TransitionInfo( @@ -376,7 +372,13 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() { underTest.updateTransition(it, 0.2f, TransitionState.CANCELED) underTest.updateTransition(it, 0.5f, TransitionState.RUNNING) } - assertThat(wtfHandler.failed).isTrue() + assertThat(steps.size).isEqualTo(2) + assertThat(steps[0]) + .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 0f, TransitionState.STARTED, OWNER_NAME)) + assertThat(steps[1]) + .isEqualTo( + TransitionStep(AOD, LOCKSCREEN, 0.2f, TransitionState.CANCELED, OWNER_NAME) + ) } @Test @@ -530,8 +532,6 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() { } assertThat(steps[steps.size - 1]) .isEqualTo(TransitionStep(from, to, lastValue, status, OWNER_NAME)) - - assertThat(wtfHandler.failed).isFalse() } private fun getAnimator(): ValueAnimator { @@ -541,14 +541,6 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() { } } - private class WtfHandler : TerribleFailureHandler { - var failed = false - - override fun onTerribleFailure(tag: String, what: TerribleFailure, system: Boolean) { - failed = true - } - } - companion object { private const val OWNER_NAME = "KeyguardTransitionRunner" } 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..d8e96bc23b25 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 @@ -51,14 +51,14 @@ class AlternateBouncerViewModelTest : SysuiTestCase() { @Test fun showPrimaryBouncer() = testScope.runTest { - underTest.showPrimaryBouncer() + underTest.onTapped() verify(statusBarKeyguardViewManager).showPrimaryBouncer(any()) } @Test fun hideAlternateBouncer() = testScope.runTest { - underTest.hideAlternateBouncer() + underTest.onRemovedFromWindow() verify(statusBarKeyguardViewManager).hideAlternateBouncer(any()) } @@ -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/appselector/data/ActivityTaskManagerThumbnailLoaderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/data/ActivityTaskManagerThumbnailLoaderTest.kt index a73df0767dbc..9797c8c5b538 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/data/ActivityTaskManagerThumbnailLoaderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/data/ActivityTaskManagerThumbnailLoaderTest.kt @@ -104,6 +104,7 @@ class ActivityTaskManagerThumbnailLoaderTest : SysuiTestCase() { WindowConfiguration.WINDOWING_MODE_FULLSCREEN, /* appearance= */ 0, /* isTranslucent= */ false, - /* hasImeSurface= */ false + /* hasImeSurface= */ false, + /* uiMode */ 0 ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewControllerTest.kt index b337ccfda772..8fbd3c8b7ebf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewControllerTest.kt @@ -24,7 +24,6 @@ import android.view.View import android.view.ViewGroup import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.systemui.Flags.FLAG_PSS_APP_SELECTOR_ABRUPT_EXIT_FIX import com.android.systemui.Flags.FLAG_PSS_APP_SELECTOR_RECENTS_SPLIT_SCREEN import com.android.systemui.SysuiTestCase import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorResultHandler @@ -141,27 +140,13 @@ class MediaProjectionRecentsViewControllerTest : SysuiTestCase() { } @Test - fun onRecentAppClicked_fullScreenTaskInForeground_flagOff_usesScaleUpAnimation() { - mSetFlagsRule.disableFlags(FLAG_PSS_APP_SELECTOR_ABRUPT_EXIT_FIX) - - controller.onRecentAppClicked(fullScreenTask, taskView) - - assertThat(getStartedTaskActivityOptions(fullScreenTask.taskId).animationType) - .isEqualTo(ActivityOptions.ANIM_SCALE_UP) - } - - @Test - fun onRecentAppClicked_fullScreenTaskInForeground_flagOn_usesDefaultAnimation() { - mSetFlagsRule.enableFlags(FLAG_PSS_APP_SELECTOR_ABRUPT_EXIT_FIX) + fun onRecentAppClicked_fullScreenTaskInForeground_usesDefaultAnimation() { assertForegroundTaskUsesDefaultCloseAnimation(fullScreenTask) } @Test fun onRecentAppClicked_splitScreenTaskInForeground_flagOn_usesDefaultAnimation() { - mSetFlagsRule.enableFlags( - FLAG_PSS_APP_SELECTOR_ABRUPT_EXIT_FIX, - FLAG_PSS_APP_SELECTOR_RECENTS_SPLIT_SCREEN - ) + mSetFlagsRule.enableFlags(FLAG_PSS_APP_SELECTOR_RECENTS_SPLIT_SCREEN) assertForegroundTaskUsesDefaultCloseAnimation(splitScreenTask) } 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..04d140c458e8 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; @@ -79,6 +78,7 @@ import android.view.inputmethod.InputMethodManager; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.UiEventLogger; import com.android.systemui.SysuiTestCase; @@ -215,6 +215,8 @@ public class NavigationBarTest extends SysuiTestCase { @Mock private WindowManager mWindowManager; @Mock + private ViewCaptureAwareWindowManager mViewCaptureAwareWindowManager; + @Mock private TelecomManager mTelecomManager; @Mock private InputMethodManager mInputMethodManager; @@ -512,7 +514,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 @@ -620,6 +622,7 @@ public class NavigationBarTest extends SysuiTestCase { null, context, mWindowManager, + mViewCaptureAwareWindowManager, () -> mAssistManager, mock(AccessibilityManager.class), deviceProvisionedController, diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/UserSettingObserverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/UserSettingObserverTest.kt index 90e0dd80c55c..0c2b59fed078 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/UserSettingObserverTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/UserSettingObserverTest.kt @@ -17,25 +17,34 @@ package com.android.systemui.qs import android.os.Handler +import android.platform.test.flag.junit.FlagsParameterization import android.testing.TestableLooper -import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.systemui.Flags import com.android.systemui.SysuiTestCase -import com.android.systemui.util.settings.FakeSettings +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testScope import com.android.systemui.util.settings.SecureSettings +import com.android.systemui.util.settings.fakeSettings import com.google.common.truth.Truth.assertThat import junit.framework.Assert.fail +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters private typealias Callback = (Int, Boolean) -> Unit +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest -@RunWith(AndroidJUnit4::class) +@RunWith(ParameterizedAndroidJunit4::class) @TestableLooper.RunWithLooper -class UserSettingObserverTest : SysuiTestCase() { +class UserSettingObserverTest(flags: FlagsParameterization) : SysuiTestCase() { companion object { private const val TEST_SETTING = "setting" @@ -43,8 +52,23 @@ class UserSettingObserverTest : SysuiTestCase() { private const val OTHER_USER = 1 private const val DEFAULT_VALUE = 1 private val FAIL_CALLBACK: Callback = { _, _ -> fail("Callback should not be called") } + + @JvmStatic + @Parameters(name = "{0}") + fun getParams(): List<FlagsParameterization> { + return FlagsParameterization.allCombinationsOf( + Flags.FLAG_QS_REGISTER_SETTING_OBSERVER_ON_BG_THREAD + ) + } + } + + init { + mSetFlagsRule.setFlagsParameterization(flags) } + private val kosmos = Kosmos() + private val testScope = kosmos.testScope + private lateinit var testableLooper: TestableLooper private lateinit var setting: UserSettingObserver private lateinit var secureSettings: SecureSettings @@ -54,7 +78,7 @@ class UserSettingObserverTest : SysuiTestCase() { @Before fun setUp() { testableLooper = TestableLooper.get(this) - secureSettings = FakeSettings() + secureSettings = kosmos.fakeSettings setting = object : @@ -76,92 +100,107 @@ class UserSettingObserverTest : SysuiTestCase() { @After fun tearDown() { - setting.isListening = false + setListening(false) } @Test - fun testNotListeningByDefault() { - callback = FAIL_CALLBACK + fun testNotListeningByDefault() = + testScope.runTest { + callback = FAIL_CALLBACK - assertThat(setting.isListening).isFalse() - secureSettings.putIntForUser(TEST_SETTING, 2, USER) - testableLooper.processAllMessages() - } + assertThat(setting.isListening).isFalse() + secureSettings.putIntForUser(TEST_SETTING, 2, USER) + testableLooper.processAllMessages() + } @Test - fun testChangedWhenListeningCallsCallback() { - var changed = false - callback = { _, _ -> changed = true } + fun testChangedWhenListeningCallsCallback() = + testScope.runTest { + var changed = false + callback = { _, _ -> changed = true } - setting.isListening = true - secureSettings.putIntForUser(TEST_SETTING, 2, USER) - testableLooper.processAllMessages() + setListening(true) + secureSettings.putIntForUser(TEST_SETTING, 2, USER) + testableLooper.processAllMessages() - assertThat(changed).isTrue() - } + assertThat(changed).isTrue() + } @Test - fun testListensToCorrectSetting() { - callback = FAIL_CALLBACK + fun testListensToCorrectSetting() = + testScope.runTest { + callback = FAIL_CALLBACK - setting.isListening = true - secureSettings.putIntForUser("other", 2, USER) - testableLooper.processAllMessages() - } + setListening(true) + secureSettings.putIntForUser("other", 2, USER) + testableLooper.processAllMessages() + } @Test - fun testGetCorrectValue() { - secureSettings.putIntForUser(TEST_SETTING, 2, USER) - assertThat(setting.value).isEqualTo(2) + fun testGetCorrectValue() = + testScope.runTest { + secureSettings.putIntForUser(TEST_SETTING, 2, USER) + assertThat(setting.value).isEqualTo(2) - secureSettings.putIntForUser(TEST_SETTING, 4, USER) - assertThat(setting.value).isEqualTo(4) - } + secureSettings.putIntForUser(TEST_SETTING, 4, USER) + assertThat(setting.value).isEqualTo(4) + } @Test - fun testSetValue() { - setting.value = 5 - assertThat(secureSettings.getIntForUser(TEST_SETTING, USER)).isEqualTo(5) - } + fun testSetValue() = + testScope.runTest { + setting.value = 5 + assertThat(secureSettings.getIntForUser(TEST_SETTING, USER)).isEqualTo(5) + } @Test - fun testChangeUser() { - setting.isListening = true - setting.setUserId(OTHER_USER) + fun testChangeUser() = + testScope.runTest { + setListening(true) + setting.setUserId(OTHER_USER) - setting.isListening = true - assertThat(setting.currentUser).isEqualTo(OTHER_USER) - } + setListening(true) + assertThat(setting.currentUser).isEqualTo(OTHER_USER) + } @Test - fun testDoesntListenInOtherUsers() { - callback = FAIL_CALLBACK - setting.isListening = true + fun testDoesntListenInOtherUsers() = + testScope.runTest { + callback = FAIL_CALLBACK + setListening(true) - secureSettings.putIntForUser(TEST_SETTING, 3, OTHER_USER) - testableLooper.processAllMessages() - } + secureSettings.putIntForUser(TEST_SETTING, 3, OTHER_USER) + testableLooper.processAllMessages() + } @Test - fun testListensToCorrectUserAfterChange() { - var changed = false - callback = { _, _ -> changed = true } + fun testListensToCorrectUserAfterChange() = + testScope.runTest { + var changed = false + callback = { _, _ -> changed = true } - setting.isListening = true - setting.setUserId(OTHER_USER) - secureSettings.putIntForUser(TEST_SETTING, 2, OTHER_USER) - testableLooper.processAllMessages() + setListening(true) + setting.setUserId(OTHER_USER) + testScope.runCurrent() + secureSettings.putIntForUser(TEST_SETTING, 2, OTHER_USER) + testableLooper.processAllMessages() - assertThat(changed).isTrue() - } + assertThat(changed).isTrue() + } @Test - fun testDefaultValue() { - // Check default value before listening - assertThat(setting.value).isEqualTo(DEFAULT_VALUE) - - // Check default value if setting is not set - setting.isListening = true - assertThat(setting.value).isEqualTo(DEFAULT_VALUE) + fun testDefaultValue() = + testScope.runTest { + // Check default value before listening + assertThat(setting.value).isEqualTo(DEFAULT_VALUE) + + // Check default value if setting is not set + setListening(true) + assertThat(setting.value).isEqualTo(DEFAULT_VALUE) + } + + fun setListening(listening: Boolean) { + setting.isListening = listening + testScope.runCurrent() } } 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 f866f740345e..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,9 +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).createErrorNotification(); doNothing().when(mRecordingService).showErrorToast(anyInt()); doNothing().when(mRecordingService).stopForeground(anyInt()); @@ -227,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(); @@ -234,7 +262,11 @@ public class RecordingServiceTest extends SysuiTestCase { mRecordingService.onStopped(); - verify(mRecordingService).createErrorNotification(); + 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/screenrecord/ScreenRecordPermissionDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegateTest.kt index 11b0bdf3effd..7dae5ccd05c4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegateTest.kt @@ -21,13 +21,13 @@ import android.os.UserHandle import android.testing.TestableLooper import android.view.View import android.widget.Spinner +import android.widget.TextView import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.Dependency import com.android.systemui.SysuiTestCase import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.broadcast.BroadcastDispatcher -import com.android.systemui.flags.FeatureFlags import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorActivity import com.android.systemui.mediaprojection.permission.ENTIRE_SCREEN @@ -60,7 +60,6 @@ class ScreenRecordPermissionDialogDelegateTest : SysuiTestCase() { @Mock private lateinit var starter: ActivityStarter @Mock private lateinit var controller: RecordingController @Mock private lateinit var userContextProvider: UserContextProvider - @Mock private lateinit var flags: FeatureFlags @Mock private lateinit var onStartRecordingClicked: Runnable @Mock private lateinit var mediaProjectionMetricsLogger: MediaProjectionMetricsLogger @@ -128,6 +127,32 @@ class ScreenRecordPermissionDialogDelegateTest : SysuiTestCase() { } @Test + fun startButtonText_entireScreenSelected() { + showDialog() + + onSpinnerItemSelected(ENTIRE_SCREEN) + + assertThat(getStartButton().text) + .isEqualTo( + context.getString(R.string.screenrecord_permission_dialog_continue_entire_screen) + ) + } + + @Test + fun startButtonText_singleAppSelected() { + showDialog() + + onSpinnerItemSelected(SINGLE_APP) + + assertThat(getStartButton().text) + .isEqualTo( + context.getString( + R.string.media_projection_entry_generic_permission_dialog_continue_single_app + ) + ) + } + + @Test fun startClicked_singleAppSelected_passesHostUidToAppSelector() { showDialog() onSpinnerItemSelected(SINGLE_APP) @@ -152,7 +177,8 @@ class ScreenRecordPermissionDialogDelegateTest : SysuiTestCase() { showDialog() val spinner = dialog.requireViewById<Spinner>(R.id.screen_share_mode_options) - val singleApp = context.getString(R.string.screen_share_permission_dialog_option_single_app) + val singleApp = + context.getString(R.string.screenrecord_permission_dialog_option_text_single_app) assertEquals(spinner.adapter.getItem(0), singleApp) } @@ -208,8 +234,10 @@ class ScreenRecordPermissionDialogDelegateTest : SysuiTestCase() { dialog.requireViewById<View>(android.R.id.button2).performClick() } + private fun getStartButton() = dialog.requireViewById<TextView>(android.R.id.button1) + private fun clickOnStart() { - dialog.requireViewById<View>(android.R.id.button1).performClick() + getStartButton().performClick() } private fun onSpinnerItemSelected(position: Int) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java index 2803035f1b82..8125ef55f4af 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java @@ -192,6 +192,7 @@ import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.policy.KeyguardUserSwitcherController; import com.android.systemui.statusbar.policy.KeyguardUserSwitcherView; import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController; +import com.android.systemui.statusbar.policy.SplitShadeStateController; import com.android.systemui.statusbar.policy.data.repository.FakeUserSetupRepository; import com.android.systemui.statusbar.window.StatusBarWindowStateController; import com.android.systemui.unfold.SysUIUnfoldComponent; @@ -428,6 +429,9 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { mock(DeviceEntryUdfpsInteractor.class); when(deviceEntryUdfpsInteractor.isUdfpsSupported()).thenReturn(MutableStateFlow(false)); + final SplitShadeStateController splitShadeStateController = + new ResourcesSplitShadeStateController(); + mShadeInteractor = new ShadeInteractorImpl( mTestScope.getBackgroundScope(), mKosmos.getDeviceProvisioningInteractor(), @@ -445,7 +449,8 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { new SharedNotificationContainerInteractor( new FakeConfigurationRepository(), mContext, - new ResourcesSplitShadeStateController(), + () -> splitShadeStateController, + () -> mShadeInteractor, mKeyguardInteractor, deviceEntryUdfpsInteractor, () -> mLargeScreenHeaderHelper diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt index 2c2fcbe75e1b..13bc82fa2c70 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt @@ -17,7 +17,6 @@ package com.android.systemui.shade import android.platform.test.annotations.DisableFlags -import android.platform.test.annotations.EnableFlags import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.view.View @@ -28,7 +27,6 @@ import androidx.annotation.IdRes import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.test.filters.SmallTest -import com.android.systemui.Flags.FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX import com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT import com.android.systemui.SysuiTestCase import com.android.systemui.fragments.FragmentHostManager @@ -166,31 +164,7 @@ class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() { } @Test - @DisableFlags(FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX) - fun testLargeScreen_updateResources_refactorFlagOff_splitShadeHeightIsSetBasedOnResource() { - val headerResourceHeight = 20 - val headerHelperHeight = 30 - whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()) - .thenReturn(headerHelperHeight) - overrideResource(R.bool.config_use_large_screen_shade_header, true) - overrideResource(R.dimen.qs_header_height, 10) - overrideResource(R.dimen.large_screen_shade_header_height, headerResourceHeight) - - // ensure the estimated height (would be 3 here) wouldn't impact this test case - overrideResource(R.dimen.large_screen_shade_header_min_height, 1) - overrideResource(R.dimen.new_qs_header_non_clickable_element_height, 1) - - underTest.updateResources() - - val captor = ArgumentCaptor.forClass(ConstraintSet::class.java) - verify(view).applyConstraints(capture(captor)) - assertThat(captor.value.getHeight(R.id.split_shade_status_bar)) - .isEqualTo(headerResourceHeight) - } - - @Test - @EnableFlags(FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX) - fun testLargeScreen_updateResources_refactorFlagOn_splitShadeHeightIsSetBasedOnHelper() { + fun testLargeScreen_updateResources_splitShadeHeightIsSetBasedOnHelper() { val headerResourceHeight = 20 val headerHelperHeight = 30 whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()) @@ -447,31 +421,8 @@ class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() { } @Test - @DisableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT, FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX) - fun testLargeScreenLayout_refactorFlagOff_qsAndNotifsTopMarginIsOfHeaderHeightResource() { - setLargeScreen() - val largeScreenHeaderResourceHeight = 100 - val largeScreenHeaderHelperHeight = 200 - whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()) - .thenReturn(largeScreenHeaderHelperHeight) - overrideResource(R.dimen.large_screen_shade_header_height, largeScreenHeaderResourceHeight) - - // ensure the estimated height (would be 30 here) wouldn't impact this test case - overrideResource(R.dimen.large_screen_shade_header_min_height, 10) - overrideResource(R.dimen.new_qs_header_non_clickable_element_height, 10) - - underTest.updateResources() - - assertThat(getConstraintSetLayout(R.id.qs_frame).topMargin) - .isEqualTo(largeScreenHeaderResourceHeight) - assertThat(getConstraintSetLayout(R.id.notification_stack_scroller).topMargin) - .isEqualTo(largeScreenHeaderResourceHeight) - } - - @Test @DisableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) - @EnableFlags(FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX) - fun testLargeScreenLayout_refactorFlagOn_qsAndNotifsTopMarginIsOfHeaderHeightHelper() { + fun testLargeScreenLayout_qsAndNotifsTopMarginIsOfHeaderHeightHelper() { setLargeScreen() val largeScreenHeaderResourceHeight = 100 val largeScreenHeaderHelperHeight = 200 diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt index f21def361e40..4850b0f67857 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt @@ -16,7 +16,6 @@ package com.android.systemui.shade -import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.testing.AndroidTestingRunner import android.testing.TestableLooper @@ -28,7 +27,6 @@ import androidx.annotation.IdRes import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.test.filters.SmallTest -import com.android.systemui.Flags.FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX import com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT import com.android.systemui.SysuiTestCase import com.android.systemui.fragments.FragmentHostManager @@ -164,29 +162,7 @@ class NotificationsQSContainerControllerTest : SysuiTestCase() { } @Test - @DisableFlags(FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX) - fun testLargeScreen_updateResources_refactorFlagOff_splitShadeHeightIsSet_basedOnResource() { - val helperHeight = 30 - val resourceHeight = 20 - whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()).thenReturn(helperHeight) - overrideResource(R.bool.config_use_large_screen_shade_header, true) - overrideResource(R.dimen.qs_header_height, 10) - overrideResource(R.dimen.large_screen_shade_header_height, resourceHeight) - - // ensure the estimated height (would be 3 here) wouldn't impact this test case - overrideResource(R.dimen.large_screen_shade_header_min_height, 1) - overrideResource(R.dimen.new_qs_header_non_clickable_element_height, 1) - - underTest.updateResources() - - val captor = ArgumentCaptor.forClass(ConstraintSet::class.java) - verify(view).applyConstraints(capture(captor)) - assertThat(captor.value.getHeight(R.id.split_shade_status_bar)).isEqualTo(resourceHeight) - } - - @Test - @EnableFlags(FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX) - fun testLargeScreen_updateResources_refactorFlagOn_splitShadeHeightIsSet_basedOnHelper() { + fun testLargeScreen_updateResources_splitShadeHeightIsSet_basedOnHelper() { val helperHeight = 30 val resourceHeight = 20 whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()).thenReturn(helperHeight) @@ -427,28 +403,7 @@ class NotificationsQSContainerControllerTest : SysuiTestCase() { } @Test - @DisableFlags(FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX) - fun testLargeScreenLayout_refactorFlagOff_qsAndNotifsTopMarginIsOfHeaderResourceHeight() { - setLargeScreen() - val largeScreenHeaderHelperHeight = 200 - val largeScreenHeaderResourceHeight = 100 - whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()) - .thenReturn(largeScreenHeaderHelperHeight) - overrideResource(R.dimen.large_screen_shade_header_height, largeScreenHeaderResourceHeight) - - // ensure the estimated height (would be 30 here) wouldn't impact this test case - overrideResource(R.dimen.large_screen_shade_header_min_height, 10) - overrideResource(R.dimen.new_qs_header_non_clickable_element_height, 10) - - underTest.updateResources() - - assertThat(getConstraintSetLayout(R.id.qs_frame).topMargin) - .isEqualTo(largeScreenHeaderResourceHeight) - } - - @Test - @EnableFlags(FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX) - fun testLargeScreenLayout_refactorFlagOn_qsAndNotifsTopMarginIsOfHeaderHelperHeight() { + fun testLargeScreenLayout_qsAndNotifsTopMarginIsOfHeaderHelperHeight() { setLargeScreen() val largeScreenHeaderHelperHeight = 200 val largeScreenHeaderResourceHeight = 100 diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java index e57382de2edd..505f7997ef1c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java @@ -225,7 +225,8 @@ public class QuickSettingsControllerImplBaseTest extends SysuiTestCase { new SharedNotificationContainerInteractor( configurationRepository, mContext, - splitShadeStateController, + () -> splitShadeStateController, + () -> mShadeInteractor, keyguardInteractor, deviceEntryUdfpsInteractor, () -> mLargeScreenHeaderHelper), @@ -266,6 +267,7 @@ public class QuickSettingsControllerImplBaseTest extends SysuiTestCase { when(mPanelView.getParent()).thenReturn(mPanelViewParent); when(mQs.getHeader()).thenReturn(mQsHeader); + when(mQSFragment.getHeader()).thenReturn(mQsHeader); doAnswer(invocation -> { mLockscreenShadeTransitionCallback = invocation.getArgument(0); diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplTest.java index e7db4690c2c3..2e9d6e85d0aa 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplTest.java @@ -24,34 +24,41 @@ import static android.view.MotionEvent.ACTION_UP; import static android.view.MotionEvent.BUTTON_SECONDARY; import static android.view.MotionEvent.BUTTON_STYLUS_PRIMARY; -import static com.android.systemui.Flags.FLAG_QS_UI_REFACTOR; import static com.android.systemui.Flags.FLAG_QS_UI_REFACTOR_COMPOSE_FRAGMENT; import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; import static com.android.systemui.statusbar.StatusBarState.SHADE; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.graphics.Rect; +import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.FlagsParameterization; import android.testing.TestableLooper; import android.view.MotionEvent; +import android.view.ViewGroup; import androidx.test.filters.SmallTest; import com.android.systemui.plugins.qs.QS; +import com.android.systemui.qs.flags.QSComposeFragment; import com.android.systemui.res.R; import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; import java.util.List; @@ -65,7 +72,7 @@ public class QuickSettingsControllerImplTest extends QuickSettingsControllerImpl @Parameters(name = "{0}") public static List<FlagsParameterization> getParams() { - return progressionOf(FLAG_QS_UI_REFACTOR, FLAG_QS_UI_REFACTOR_COMPOSE_FRAGMENT); + return progressionOf(FLAG_QS_UI_REFACTOR_COMPOSE_FRAGMENT); } public QuickSettingsControllerImplTest(FlagsParameterization flags) { @@ -244,6 +251,61 @@ public class QuickSettingsControllerImplTest extends QuickSettingsControllerImpl } @Test + @DisableFlags(QSComposeFragment.FLAG_NAME) + public void onQsFragmentAttached_qsComposeFragmentDisabled_setHeaderInNSSL() { + mFragmentListener.onFragmentViewCreated(QS.TAG, mQSFragment); + + verify(mNotificationStackScrollLayoutController) + .setQsHeader((ViewGroup) mQSFragment.getHeader()); + verify(mNotificationStackScrollLayoutController, never()).setQsHeaderBoundsProvider(any()); + } + + @Test + @EnableFlags(QSComposeFragment.FLAG_NAME) + public void onQsFragmentAttached_qsComposeFragmentEnabled_setQsHeaderBoundsProviderInNSSL() { + mFragmentListener.onFragmentViewCreated(QS.TAG, mQSFragment); + + verify(mNotificationStackScrollLayoutController, never()) + .setQsHeader((ViewGroup) mQSFragment.getHeader()); + ArgumentCaptor<QSHeaderBoundsProvider> argumentCaptor = + ArgumentCaptor.forClass(QSHeaderBoundsProvider.class); + + verify(mNotificationStackScrollLayoutController) + .setQsHeaderBoundsProvider(argumentCaptor.capture()); + + argumentCaptor.getValue().getLeftProvider().invoke(); + argumentCaptor.getValue().getHeightProvider().invoke(); + argumentCaptor.getValue().getBoundsOnScreenProvider().invoke(new Rect()); + InOrder inOrderVerifier = inOrder(mQSFragment); + + inOrderVerifier.verify(mQSFragment).getHeaderLeft(); + inOrderVerifier.verify(mQSFragment).getHeaderHeight(); + inOrderVerifier.verify(mQSFragment).getHeaderBoundsOnScreen(new Rect()); + } + + @Test + @DisableFlags(QSComposeFragment.FLAG_NAME) + public void onQSFragmentDetached_qsComposeFragmentFlagDisabled_setViewToNullInNSSL() { + mFragmentListener.onFragmentViewCreated(QS.TAG, mQSFragment); + + mFragmentListener.onFragmentViewDestroyed(QS.TAG, mQSFragment); + + verify(mNotificationStackScrollLayoutController).setQsHeader(null); + verify(mNotificationStackScrollLayoutController, never()).setQsHeaderBoundsProvider(null); + } + + @Test + @EnableFlags(QSComposeFragment.FLAG_NAME) + public void onQSFragmentDetached_qsComposeFragmentFlagEnabled_setBoundsProviderToNullInNSSL() { + mFragmentListener.onFragmentViewCreated(QS.TAG, mQSFragment); + + mFragmentListener.onFragmentViewDestroyed(QS.TAG, mQSFragment); + + verify(mNotificationStackScrollLayoutController, never()).setQsHeader(null); + verify(mNotificationStackScrollLayoutController).setQsHeaderBoundsProvider(null); + } + + @Test public void onQsFragmentAttached_notFullWidth_setsFullWidthFalseOnQS() { setIsFullWidth(false); mFragmentListener.onFragmentViewCreated(QS.TAG, mQSFragment); 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/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt index e68fa0bc6eb3..804eb5cf597c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt @@ -231,6 +231,34 @@ class ScreenRecordChipViewModelTest : SysuiTestCase() { assertThat((latest as OngoingActivityChipModel.Shown.Timer).startTimeMs).isEqualTo(5678) } + /** Regression test for b/349620526. */ + @Test + fun chip_recordingState_thenGetsTaskInfo_startTimeDoesNotChange() = + testScope.runTest { + val latest by collectLastValue(underTest.chip) + + // Start recording, but without any task info + systemClock.setElapsedRealtime(1234) + screenRecordRepo.screenRecordState.value = ScreenRecordModel.Recording + mediaProjectionRepo.mediaProjectionState.value = MediaProjectionState.NotProjecting + + assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java) + assertThat((latest as OngoingActivityChipModel.Shown.Timer).startTimeMs).isEqualTo(1234) + + // WHEN we receive the recording task info a few milliseconds later + systemClock.setElapsedRealtime(1240) + mediaProjectionRepo.mediaProjectionState.value = + MediaProjectionState.Projecting.SingleTask( + "host.package", + hostDeviceName = null, + FakeActivityTaskManager.createTask(taskId = 1) + ) + + // THEN the start time is still the old start time + assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java) + assertThat((latest as OngoingActivityChipModel.Shown.Timer).startTimeMs).isEqualTo(1234) + } + @Test fun chip_notProjecting_clickListenerShowsDialog() = testScope.runTest { 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/statusbar/phone/KeyguardClockPositionAlgorithmTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithmTest.java index 7f33c23e8abc..eb1e28b891f7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithmTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithmTest.java @@ -26,13 +26,10 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.when; import android.content.res.Resources; -import android.platform.test.annotations.DisableFlags; -import android.platform.test.annotations.EnableFlags; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import com.android.systemui.Flags; import com.android.systemui.SysuiTestCase; import com.android.systemui.doze.util.BurnInHelperKt; import com.android.systemui.log.LogBuffer; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java index af5e60e9cd01..9b611057c059 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java @@ -1068,7 +1068,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { public void testShowBouncerOrKeyguard_needsFullScreen() { when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn( KeyguardSecurityModel.SecurityMode.SimPin); - mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false); + mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, false); verify(mCentralSurfaces).hideKeyguard(); verify(mPrimaryBouncerInteractor).show(true); } @@ -1084,7 +1084,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { .thenReturn(KeyguardState.LOCKSCREEN); reset(mCentralSurfaces); - mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false); + mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, false); verify(mPrimaryBouncerInteractor).show(true); verify(mCentralSurfaces).showKeyguard(); } @@ -1092,11 +1092,26 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { @Test @DisableSceneContainer public void testShowBouncerOrKeyguard_needsFullScreen_bouncerAlreadyShowing() { + boolean isFalsingReset = false; when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn( KeyguardSecurityModel.SecurityMode.SimPin); when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(true); - mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false); + mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, isFalsingReset); verify(mCentralSurfaces, never()).hideKeyguard(); + verify(mPrimaryBouncerInteractor).show(true); + } + + @Test + @DisableSceneContainer + public void testShowBouncerOrKeyguard_needsFullScreen_bouncerAlreadyShowing_onFalsing() { + boolean isFalsingReset = true; + when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn( + KeyguardSecurityModel.SecurityMode.SimPin); + when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(true); + mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, isFalsingReset); + verify(mCentralSurfaces, never()).hideKeyguard(); + + // Do not refresh the full screen bouncer if the call is from falsing verify(mPrimaryBouncerInteractor, never()).show(true); } 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/android/content/ContextKosmos.kt b/packages/SystemUI/tests/utils/src/android/content/ContextKosmos.kt index 185deea31747..a61233ad18b9 100644 --- a/packages/SystemUI/tests/utils/src/android/content/ContextKosmos.kt +++ b/packages/SystemUI/tests/utils/src/android/content/ContextKosmos.kt @@ -16,10 +16,12 @@ package android.content +import com.android.systemui.SysuiTestableContext import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testCase import com.android.systemui.util.mockito.mock -var Kosmos.applicationContext: Context by +var Kosmos.testableContext: SysuiTestableContext by Kosmos.Fixture { testCase.context.apply { ensureTestableResources() } } +var Kosmos.applicationContext: Context by Kosmos.Fixture { testableContext } val Kosmos.mockedContext: Context by Kosmos.Fixture { mock<Context>() } 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 edf4bcc238c0..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(fakeEduClock) } + 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/FakeContextualEducationRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/FakeContextualEducationRepository.kt index 3816e1b604ce..aa1968afba7d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/FakeContextualEducationRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/FakeContextualEducationRepository.kt @@ -18,12 +18,11 @@ package com.android.systemui.education.data.repository import com.android.systemui.contextualeducation.GestureType import com.android.systemui.education.data.model.GestureEduModel -import java.time.Clock import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -class FakeContextualEducationRepository(private val clock: Clock) : ContextualEducationRepository { +class FakeContextualEducationRepository : ContextualEducationRepository { private val userGestureMap = mutableMapOf<Int, GestureEduModel>() private val _gestureEduModels = MutableStateFlow(GestureEduModel()) @@ -44,16 +43,11 @@ class FakeContextualEducationRepository(private val clock: Clock) : ContextualEd return gestureEduModelsFlow } - override suspend fun incrementSignalCount(gestureType: GestureType) { - val originalModel = _gestureEduModels.value - _gestureEduModels.value = - originalModel.copy( - signalCount = _gestureEduModels.value.signalCount + 1, - ) - } - - override suspend fun updateShortcutTriggerTime(gestureType: GestureType) { - val originalModel = _gestureEduModels.value - _gestureEduModels.value = originalModel.copy(lastShortcutTriggeredTime = clock.instant()) + override suspend fun updateGestureEduModel( + gestureType: GestureType, + transform: (GestureEduModel) -> GestureEduModel + ) { + val currentModel = _gestureEduModels.value + _gestureEduModels.value = transform(currentModel) } } 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/ContextualEducationInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractorKosmos.kt index a7b322b5a86d..5c99a7faf13c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractorKosmos.kt @@ -17,6 +17,7 @@ package com.android.systemui.education.domain.interactor import com.android.systemui.education.data.repository.contextualEducationRepository +import com.android.systemui.education.data.repository.fakeEduClock import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope @@ -28,6 +29,7 @@ val Kosmos.contextualEducationInteractor by backgroundScope = testScope.backgroundScope, backgroundDispatcher = testDispatcher, repository = contextualEducationRepository, - selectedUserInteractor = selectedUserInteractor + selectedUserInteractor = selectedUserInteractor, + clock = fakeEduClock ) } 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/domain/interactor/KeyguardTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorKosmos.kt index c5da10e59369..b68d6a0510d5 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorKosmos.kt @@ -33,6 +33,7 @@ val Kosmos.keyguardTransitionInteractor: KeyguardTransitionInteractor by fromAodTransitionInteractor = { fromAodTransitionInteractor }, fromAlternateBouncerTransitionInteractor = { fromAlternateBouncerTransitionInteractor }, fromDozingTransitionInteractor = { fromDozingTransitionInteractor }, + fromOccludedTransitionInteractor = { fromOccludedTransitionInteractor }, sceneInteractor = sceneInteractor ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinderKosmos.kt index 2919d3f575c6..1e95fc12bdb5 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinderKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinderKosmos.kt @@ -17,7 +17,6 @@ package com.android.systemui.keyguard.ui.binder import android.content.applicationContext -import android.view.layoutInflater import android.view.mockedLayoutInflater import android.view.windowManager import com.android.systemui.biometrics.domain.interactor.fingerprintPropertyInteractor @@ -25,7 +24,6 @@ import com.android.systemui.biometrics.domain.interactor.udfpsOverlayInteractor import com.android.systemui.common.ui.domain.interactor.configurationInteractor import com.android.systemui.deviceentry.domain.interactor.deviceEntryUdfpsInteractor import com.android.systemui.deviceentry.ui.viewmodel.AlternateBouncerUdfpsAccessibilityOverlayViewModel -import com.android.systemui.keyguard.dismissCallbackRegistry import com.android.systemui.keyguard.ui.SwipeUpAnywhereGestureHandler import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerDependencies import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerMessageAreaViewModel @@ -50,10 +48,10 @@ val Kosmos.alternateBouncerViewBinder by alternateBouncerDependencies = { alternateBouncerDependencies }, windowManager = { windowManager }, layoutInflater = { mockedLayoutInflater }, - dismissCallbackRegistry = dismissCallbackRegistry, ) } +@ExperimentalCoroutinesApi private val Kosmos.alternateBouncerDependencies by Kosmos.Fixture { AlternateBouncerDependencies( @@ -69,6 +67,7 @@ private val Kosmos.alternateBouncerDependencies by ) } +@ExperimentalCoroutinesApi private val Kosmos.alternateBouncerUdfpsIconViewModel by Kosmos.Fixture { AlternateBouncerUdfpsIconViewModel( diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelKosmos.kt index bdd4afa3da7d..29583153ccc6 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelKosmos.kt @@ -18,6 +18,8 @@ package com.android.systemui.keyguard.ui.viewmodel +import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor +import com.android.systemui.keyguard.dismissCallbackRegistry import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture @@ -28,5 +30,7 @@ val Kosmos.alternateBouncerViewModel by Fixture { AlternateBouncerViewModel( statusBarKeyguardViewManager = statusBarKeyguardViewManager, keyguardTransitionInteractor = keyguardTransitionInteractor, + dismissCallbackRegistry = dismissCallbackRegistry, + alternateBouncerInteractor = { alternateBouncerInteractor }, ) } 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/tiles/impl/airplane/AirplaneModeTileKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/airplane/AirplaneModeTileKosmos.kt new file mode 100644 index 000000000000..73b1859acc3d --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/airplane/AirplaneModeTileKosmos.kt @@ -0,0 +1,24 @@ +/* + * 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.tiles.impl.airplane + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.qs.qsEventLogger +import com.android.systemui.statusbar.connectivity.ConnectivityModule + +val Kosmos.qsAirplaneModeTileConfig by + Kosmos.Fixture { ConnectivityModule.provideAirplaneModeTileConfig(qsEventLogger) } 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/scene/domain/startable/SceneContainerStartableKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt index 8e76a0bf5a13..53b6a2ee226b 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt @@ -18,6 +18,7 @@ package com.android.systemui.scene.domain.startable import com.android.internal.logging.uiEventLogger import com.android.systemui.authentication.domain.interactor.authenticationInteractor +import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor import com.android.systemui.bouncer.domain.interactor.bouncerInteractor import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor import com.android.systemui.classifier.falsingCollector @@ -80,5 +81,6 @@ val Kosmos.sceneContainerStartable by Fixture { keyguardEnabledInteractor = keyguardEnabledInteractor, dismissCallbackRegistry = dismissCallbackRegistry, statusBarStateController = sysuiStatusBarStateController, + alternateBouncerInteractor = alternateBouncerInteractor, ) } 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/ShadeTestUtil.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt index ea02d0c7ac9a..6d488d21301e 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt @@ -18,10 +18,13 @@ package com.android.systemui.shade import com.android.compose.animation.scene.ObservableTransitionState import com.android.compose.animation.scene.SceneKey +import com.android.systemui.SysuiTestableContext +import com.android.systemui.res.R import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.data.repository.FakeShadeRepository +import com.android.systemui.shade.data.repository.ShadeRepository import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.TestScope @@ -86,6 +89,11 @@ class ShadeTestUtil constructor(val delegate: ShadeTestUtilDelegate) { delegate.assertFlagValid() delegate.setLegacyExpandedOrAwaitingInputTransfer(legacyExpandedOrAwaitingInputTransfer) } + + fun setSplitShade(splitShade: Boolean) { + delegate.assertFlagValid() + delegate.setSplitShade(splitShade) + } } /** Sets up shade state for tests for a specific value of the scene container flag. */ @@ -117,11 +125,16 @@ interface ShadeTestUtilDelegate { fun setQsFullscreen(qsFullscreen: Boolean) fun setLegacyExpandedOrAwaitingInputTransfer(legacyExpandedOrAwaitingInputTransfer: Boolean) + + fun setSplitShade(splitShade: Boolean) } /** Sets up shade state for tests when the scene container flag is disabled. */ -class ShadeTestUtilLegacyImpl(val testScope: TestScope, val shadeRepository: FakeShadeRepository) : - ShadeTestUtilDelegate { +class ShadeTestUtilLegacyImpl( + val testScope: TestScope, + val shadeRepository: FakeShadeRepository, + val context: SysuiTestableContext +) : ShadeTestUtilDelegate { override fun setShadeAndQsExpansion(shadeExpansion: Float, qsExpansion: Float) { shadeRepository.setLegacyShadeExpansion(shadeExpansion) shadeRepository.setQsExpansion(qsExpansion) @@ -168,11 +181,22 @@ class ShadeTestUtilLegacyImpl(val testScope: TestScope, val shadeRepository: Fak override fun setLegacyExpandedOrAwaitingInputTransfer(expanded: Boolean) { shadeRepository.setLegacyExpandedOrAwaitingInputTransfer(expanded) } + + override fun setSplitShade(splitShade: Boolean) { + context + .getOrCreateTestableResources() + .addOverride(R.bool.config_use_split_notification_shade, splitShade) + testScope.runCurrent() + } } /** Sets up shade state for tests when the scene container flag is enabled. */ -class ShadeTestUtilSceneImpl(val testScope: TestScope, val sceneInteractor: SceneInteractor) : - ShadeTestUtilDelegate { +class ShadeTestUtilSceneImpl( + val testScope: TestScope, + val sceneInteractor: SceneInteractor, + val shadeRepository: ShadeRepository, + val context: SysuiTestableContext, +) : ShadeTestUtilDelegate { val isUserInputOngoing = MutableStateFlow(true) override fun setShadeAndQsExpansion(shadeExpansion: Float, qsExpansion: Float) { @@ -263,6 +287,14 @@ class ShadeTestUtilSceneImpl(val testScope: TestScope, val sceneInteractor: Scen testScope.runCurrent() } + override fun setSplitShade(splitShade: Boolean) { + context + .getOrCreateTestableResources() + .addOverride(R.bool.config_use_split_notification_shade, splitShade) + shadeRepository.setShadeLayoutWide(splitShade) + testScope.runCurrent() + } + override fun assertFlagValid() { Assert.assertTrue(SceneContainerFlag.isEnabled) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtilKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtilKosmos.kt index 9eeb345bde0a..a1551e095f24 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtilKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtilKosmos.kt @@ -16,6 +16,7 @@ package com.android.systemui.shade +import android.content.testableContext import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope import com.android.systemui.scene.domain.interactor.sceneInteractor @@ -26,9 +27,14 @@ var Kosmos.shadeTestUtil: ShadeTestUtil by Kosmos.Fixture { ShadeTestUtil( if (SceneContainerFlag.isEnabled) { - ShadeTestUtilSceneImpl(testScope, sceneInteractor) + ShadeTestUtilSceneImpl( + testScope, + sceneInteractor, + fakeShadeRepository, + testableContext + ) } else { - ShadeTestUtilLegacyImpl(testScope, fakeShadeRepository) + ShadeTestUtilLegacyImpl(testScope, fakeShadeRepository, testableContext) } ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt index bfd6614a2272..54208b9cdaef 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt @@ -44,7 +44,7 @@ val Kosmos.shadeInteractorSceneContainerImpl by ShadeInteractorSceneContainerImpl( scope = applicationCoroutineScope, sceneInteractor = sceneInteractor, - sharedNotificationContainerInteractor = sharedNotificationContainerInteractor, + shadeRepository = shadeRepository, ) } val Kosmos.shadeInteractorLegacyImpl by 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/domain/interactor/SharedNotificationContainerInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractorKosmos.kt index 8909d751227a..3234e66024a8 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractorKosmos.kt @@ -21,6 +21,7 @@ import com.android.systemui.common.ui.data.repository.configurationRepository import com.android.systemui.deviceentry.domain.interactor.deviceEntryUdfpsInteractor import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.kosmos.Kosmos +import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.shade.largeScreenHeaderHelper import com.android.systemui.statusbar.policy.splitShadeStateController @@ -29,7 +30,8 @@ val Kosmos.sharedNotificationContainerInteractor by SharedNotificationContainerInteractor( configurationRepository = configurationRepository, context = applicationContext, - splitShadeStateController = splitShadeStateController, + splitShadeStateController = { splitShadeStateController }, + shadeInteractor = { shadeInteractor }, keyguardInteractor = keyguardInteractor, deviceEntryUdfpsInteractor = deviceEntryUdfpsInteractor, largeScreenHeaderHelperLazy = { largeScreenHeaderHelper } 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/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/data/repository/VolumePanelGlobalStateRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/data/repository/VolumePanelGlobalStateRepositoryKosmos.kt index 2ba1211a9bdb..0b438d183544 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/data/repository/VolumePanelGlobalStateRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/data/repository/VolumePanelGlobalStateRepositoryKosmos.kt @@ -18,6 +18,7 @@ package com.android.systemui.volume.panel.data.repository import com.android.systemui.dump.dumpManager import com.android.systemui.kosmos.Kosmos +import com.android.systemui.volume.shared.volumePanelLogger val Kosmos.volumePanelGlobalStateRepository by - Kosmos.Fixture { VolumePanelGlobalStateRepository(dumpManager) } + Kosmos.Fixture { VolumePanelGlobalStateRepository(dumpManager, volumePanelLogger) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractorKosmos.kt index a18f498e5441..3804a9f21080 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractorKosmos.kt @@ -28,6 +28,7 @@ import com.android.systemui.volume.panel.domain.ComponentAvailabilityCriteria import com.android.systemui.volume.panel.domain.defaultCriteria import com.android.systemui.volume.panel.shared.model.VolumePanelComponentKey import com.android.systemui.volume.panel.ui.composable.enabledComponents +import com.android.systemui.volume.shared.volumePanelLogger import javax.inject.Provider var Kosmos.criteriaByKey: Map<VolumePanelComponentKey, Provider<ComponentAvailabilityCriteria>> by @@ -50,6 +51,7 @@ var Kosmos.componentsInteractor: ComponentsInteractor by enabledComponents, { defaultCriteria }, testScope.backgroundScope, + volumePanelLogger, criteriaByKey, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModelKosmos.kt index 34a008f92518..c4fb9e486c4d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModelKosmos.kt @@ -18,17 +18,19 @@ package com.android.systemui.volume.panel.ui.viewmodel import android.content.applicationContext import com.android.systemui.broadcast.broadcastDispatcher +import com.android.systemui.dump.dumpManager import com.android.systemui.kosmos.Kosmos -import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.statusbar.policy.configurationController import com.android.systemui.volume.panel.dagger.factory.volumePanelComponentFactory import com.android.systemui.volume.panel.domain.VolumePanelStartable import com.android.systemui.volume.panel.domain.interactor.volumePanelGlobalStateInteractor +import com.android.systemui.volume.shared.volumePanelLogger var Kosmos.volumePanelStartables: Set<VolumePanelStartable> by Kosmos.Fixture { emptySet() } var Kosmos.volumePanelViewModel: VolumePanelViewModel by - Kosmos.Fixture { volumePanelViewModelFactory.create(testScope.backgroundScope) } + Kosmos.Fixture { volumePanelViewModelFactory.create(applicationCoroutineScope) } val Kosmos.volumePanelViewModelFactory: VolumePanelViewModel.Factory by Kosmos.Fixture { @@ -37,6 +39,8 @@ val Kosmos.volumePanelViewModelFactory: VolumePanelViewModel.Factory by volumePanelComponentFactory, configurationController, broadcastDispatcher, + dumpManager, + volumePanelLogger, volumePanelGlobalStateInteractor, ) } diff --git a/packages/overlays/HsumConfigOverlay/Android.bp b/packages/overlays/HsumDefaultConfigOverlay/Android.bp index 050b1f056038..bff2f9bc326a 100644 --- a/packages/overlays/HsumConfigOverlay/Android.bp +++ b/packages/overlays/HsumDefaultConfigOverlay/Android.bp @@ -8,7 +8,7 @@ package { } runtime_resource_overlay { - name: "HsumConfigOverlay", + name: "HsumDefaultConfigOverlay", certificate: "platform", product_specific: true, diff --git a/packages/overlays/HsumConfigOverlay/AndroidManifest.xml b/packages/overlays/HsumDefaultConfigOverlay/AndroidManifest.xml index cd7a8796985e..dcd1741ec3f2 100644 --- a/packages/overlays/HsumConfigOverlay/AndroidManifest.xml +++ b/packages/overlays/HsumDefaultConfigOverlay/AndroidManifest.xml @@ -15,7 +15,7 @@ --> <manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="com.android.internal.overlay.hsumconfig" + package="com.android.internal.overlay.hsum.defaultconfig" android:versionCode="1" android:versionName="1.0"> <overlay android:targetPackage="android" android:priority="2" android:isStatic="true" /> diff --git a/packages/overlays/HsumConfigOverlay/OWNERS b/packages/overlays/HsumDefaultConfigOverlay/OWNERS index 79dd1c967829..79dd1c967829 100644 --- a/packages/overlays/HsumConfigOverlay/OWNERS +++ b/packages/overlays/HsumDefaultConfigOverlay/OWNERS diff --git a/packages/overlays/HsumConfigOverlay/res/values/config.xml b/packages/overlays/HsumDefaultConfigOverlay/res/values/config.xml index 7dbdfc71db93..7dbdfc71db93 100644 --- a/packages/overlays/HsumConfigOverlay/res/values/config.xml +++ b/packages/overlays/HsumDefaultConfigOverlay/res/values/config.xml diff --git a/proto/src/windowmanager.proto b/proto/src/windowmanager.proto index da4dfa98401e..6c8a4864ded2 100644 --- a/proto/src/windowmanager.proto +++ b/proto/src/windowmanager.proto @@ -45,6 +45,7 @@ message TaskSnapshotProto { int32 letterbox_inset_top = 18; int32 letterbox_inset_right = 19; int32 letterbox_inset_bottom = 20; + int32 ui_mode = 21; } // Persistent letterboxing configurations 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/ravenwood/texts/ravenwood-annotation-allowed-classes.txt b/ravenwood/texts/ravenwood-annotation-allowed-classes.txt index 68f185eba42c..cc9b70e387e8 100644 --- a/ravenwood/texts/ravenwood-annotation-allowed-classes.txt +++ b/ravenwood/texts/ravenwood-annotation-allowed-classes.txt @@ -281,6 +281,12 @@ android.os.connectivity.WifiBatteryStats com.android.server.LocalServices +com.android.internal.graphics.cam.Cam +com.android.internal.graphics.cam.CamUtils +com.android.internal.graphics.cam.Frame +com.android.internal.graphics.cam.HctSolver +com.android.internal.graphics.ColorUtils + com.android.internal.util.BitUtils com.android.internal.util.BitwiseInputStream com.android.internal.util.BitwiseOutputStream diff --git a/services/Android.bp b/services/Android.bp index ded7379ad487..0006455f41b0 100644 --- a/services/Android.bp +++ b/services/Android.bp @@ -120,6 +120,7 @@ filegroup { ":services.backup-sources", ":services.companion-sources", ":services.contentcapture-sources", + ":services.appfunctions-sources", ":services.contentsuggestions-sources", ":services.contextualsearch-sources", ":services.coverage-sources", @@ -217,6 +218,7 @@ system_java_library { "services.autofill", "services.backup", "services.companion", + "services.appfunctions", "services.contentcapture", "services.contentsuggestions", "services.contextualsearch", 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/appfunctions/Android.bp b/services/appfunctions/Android.bp new file mode 100644 index 000000000000..f8ee823ef0c9 --- /dev/null +++ b/services/appfunctions/Android.bp @@ -0,0 +1,25 @@ +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +filegroup { + name: "services.appfunctions-sources", + srcs: ["java/**/*.java"], + path: "java", + visibility: ["//frameworks/base/services"], +} + +java_library_static { + name: "services.appfunctions", + defaults: ["platform_service_defaults"], + srcs: [ + ":services.appfunctions-sources", + "java/**/*.logtags", + ], + libs: ["services.core"], +} diff --git a/services/appfunctions/OWNERS b/services/appfunctions/OWNERS new file mode 100644 index 000000000000..b3108944a3ce --- /dev/null +++ b/services/appfunctions/OWNERS @@ -0,0 +1 @@ +include /core/java/android/app/appfunctions/OWNERS diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerService.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerService.java new file mode 100644 index 000000000000..f30e770be32b --- /dev/null +++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerService.java @@ -0,0 +1,45 @@ +/* + * 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.appfunctions; + +import static android.app.appfunctions.flags.Flags.enableAppFunctionManager; + +import android.app.appfunctions.IAppFunctionManager; +import android.content.Context; + +import com.android.server.SystemService; + +/** + * Service that manages app functions. + */ +public class AppFunctionManagerService extends SystemService { + + public AppFunctionManagerService(Context context) { + super(context); + } + + @Override + public void onStart() { + if (enableAppFunctionManager()) { + publishBinderService(Context.APP_FUNCTION_SERVICE, new AppFunctionManagerStub()); + } + } + + private static class AppFunctionManagerStub extends IAppFunctionManager.Stub { + + } +} diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java index 0ab6bbc3e0d3..42f69e9ae02f 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java @@ -223,8 +223,8 @@ public class CompanionDeviceManagerService extends SystemService { // delays (even in case of the Main Thread). It may be fine overall, but would require // updating the tests (adding a delay there). mPackageMonitor.register(context, FgThread.get().getLooper(), UserHandle.ALL, true); - mDevicePresenceProcessor.init(context); } else if (phase == PHASE_BOOT_COMPLETED) { + mDevicePresenceProcessor.init(context); // Run the Inactive Association Removal job service daily. InactiveAssociationsRemovalService.schedule(getContext()); mCrossDeviceSyncController.onBootCompleted(); diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index 2e1416b2887b..d4f729cfbaf6 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -6962,7 +6962,8 @@ public final class ActiveServices { } private boolean collectPackageServicesLocked(String packageName, Set<String> filterByClasses, - boolean evenPersistent, boolean doit, ArrayMap<ComponentName, ServiceRecord> services) { + boolean evenPersistent, boolean doit, int minOomAdj, + ArrayMap<ComponentName, ServiceRecord> services) { boolean didSomething = false; for (int i = services.size() - 1; i >= 0; i--) { ServiceRecord service = services.valueAt(i); @@ -6970,6 +6971,11 @@ public final class ActiveServices { || (service.packageName.equals(packageName) && (filterByClasses == null || filterByClasses.contains(service.name.getClassName()))); + if (service.app != null && service.app.mState.getCurAdj() < minOomAdj) { + Slog.i(TAG, "Skip force stopping service " + service + + ": below minimum oom adj level"); + continue; + } if (sameComponent && (service.app == null || evenPersistent || !service.app.isPersistent())) { if (!doit) { @@ -6993,6 +6999,12 @@ public final class ActiveServices { boolean bringDownDisabledPackageServicesLocked(String packageName, Set<String> filterByClasses, int userId, boolean evenPersistent, boolean fullStop, boolean doit) { + return bringDownDisabledPackageServicesLocked(packageName, filterByClasses, userId, + evenPersistent, fullStop, doit, ProcessList.INVALID_ADJ); + } + + boolean bringDownDisabledPackageServicesLocked(String packageName, Set<String> filterByClasses, + int userId, boolean evenPersistent, boolean fullStop, boolean doit, int minOomAdj) { boolean didSomething = false; if (mTmpCollectionResults != null) { @@ -7002,7 +7014,8 @@ public final class ActiveServices { if (userId == UserHandle.USER_ALL) { for (int i = mServiceMap.size() - 1; i >= 0; i--) { didSomething |= collectPackageServicesLocked(packageName, filterByClasses, - evenPersistent, doit, mServiceMap.valueAt(i).mServicesByInstanceName); + evenPersistent, doit, minOomAdj, + mServiceMap.valueAt(i).mServicesByInstanceName); if (!doit && didSomething) { return true; } @@ -7015,7 +7028,7 @@ public final class ActiveServices { if (smap != null) { ArrayMap<ComponentName, ServiceRecord> items = smap.mServicesByInstanceName; didSomething = collectPackageServicesLocked(packageName, filterByClasses, - evenPersistent, doit, items); + evenPersistent, doit, minOomAdj, items); } if (doit && filterByClasses == null) { forceStopPackageLocked(packageName, userId); diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 9d8f3374d6ad..4a18cb1f5ed8 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -4377,6 +4377,16 @@ public class ActivityManagerService extends IActivityManager.Stub } @GuardedBy("this") + final boolean forceStopUserPackagesLocked(int userId, String reasonString, + boolean evenImportantServices) { + int minOomAdj = evenImportantServices ? ProcessList.INVALID_ADJ + : ProcessList.FOREGROUND_APP_ADJ; + return forceStopPackageInternalLocked(null, -1, false, false, + true, false, false, false, userId, reasonString, + ApplicationExitInfo.REASON_USER_STOPPED, minOomAdj); + } + + @GuardedBy("this") final boolean forceStopPackageLocked(String packageName, int appId, boolean callerWillRestart, boolean purgeCache, boolean doit, boolean evenPersistent, boolean uninstalling, boolean packageStateStopped, @@ -4385,7 +4395,6 @@ public class ActivityManagerService extends IActivityManager.Stub : ApplicationExitInfo.REASON_USER_REQUESTED; return forceStopPackageLocked(packageName, appId, callerWillRestart, purgeCache, doit, evenPersistent, uninstalling, packageStateStopped, userId, reasonString, reason); - } @GuardedBy("this") @@ -4393,6 +4402,16 @@ public class ActivityManagerService extends IActivityManager.Stub boolean callerWillRestart, boolean purgeCache, boolean doit, boolean evenPersistent, boolean uninstalling, boolean packageStateStopped, int userId, String reasonString, int reason) { + return forceStopPackageInternalLocked(packageName, appId, callerWillRestart, purgeCache, + doit, evenPersistent, uninstalling, packageStateStopped, userId, reasonString, + reason, ProcessList.INVALID_ADJ); + } + + @GuardedBy("this") + private boolean forceStopPackageInternalLocked(String packageName, int appId, + boolean callerWillRestart, boolean purgeCache, boolean doit, + boolean evenPersistent, boolean uninstalling, boolean packageStateStopped, + int userId, String reasonString, int reason, int minOomAdj) { int i; if (userId == UserHandle.USER_ALL && packageName == null) { @@ -4431,7 +4450,7 @@ public class ActivityManagerService extends IActivityManager.Stub } didSomething |= mProcessList.killPackageProcessesLSP(packageName, appId, userId, - ProcessList.INVALID_ADJ, callerWillRestart, false /* allowRestart */, doit, + minOomAdj, callerWillRestart, false /* allowRestart */, doit, evenPersistent, true /* setRemoved */, uninstalling, reason, subReason, @@ -4440,7 +4459,8 @@ public class ActivityManagerService extends IActivityManager.Stub } if (mServices.bringDownDisabledPackageServicesLocked( - packageName, null /* filterByClasses */, userId, evenPersistent, true, doit)) { + packageName, null /* filterByClasses */, userId, evenPersistent, + true, doit, minOomAdj)) { if (!doit) { return true; } @@ -19872,6 +19892,11 @@ public class ActivityManagerService extends IActivityManager.Stub } @Override + public boolean isEarlyPackageKillEnabledForUserSwitch(int fromUserId, int toUserId) { + return mUserController.isEarlyPackageKillEnabledForUserSwitch(fromUserId, toUserId); + } + + @Override public void setStopUserOnSwitch(int value) { ActivityManagerService.this.setStopUserOnSwitch(value); } diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java index 31232687418f..0b6d1358bd57 100644 --- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java +++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java @@ -51,6 +51,7 @@ import java.util.HashMap; import java.util.Map; import java.util.List; import java.util.ArrayList; +import static java.util.concurrent.Executors.newSingleThreadScheduledExecutor; /** * Maps system settings to system properties. @@ -345,7 +346,7 @@ public class SettingsToPropertiesMapper { // add sys prop sync callback for staged flag values DeviceConfig.addOnPropertiesChangedListener( NAMESPACE_REBOOT_STAGING, - AsyncTask.THREAD_POOL_EXECUTOR, + newSingleThreadScheduledExecutor(), (DeviceConfig.Properties properties) -> { for (String flagName : properties.getKeyset()) { diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java index 30efa3e87fc6..e57fe133eac8 100644 --- a/services/core/java/com/android/server/am/UserController.java +++ b/services/core/java/com/android/server/am/UserController.java @@ -439,6 +439,15 @@ class UserController implements Handler.Callback { @GuardedBy("mLock") private final List<PendingUserStart> mPendingUserStarts = new ArrayList<>(); + /** + * Contains users which cannot abort the shutdown process. + * + * <p> For example, we don't abort shutdown for users whose processes have already been stopped + * due to {@link #isEarlyPackageKillEnabledForUserSwitch(int, int)}. + */ + @GuardedBy("mLock") + private final ArraySet<Integer> mDoNotAbortShutdownUserIds = new ArraySet<>(); + private final UserLifecycleListener mUserLifecycleListener = new UserLifecycleListener() { @Override public void onUserCreated(UserInfo user, Object token) { @@ -509,11 +518,11 @@ class UserController implements Handler.Callback { } } - private boolean shouldStopUserOnSwitch() { + private boolean isStopUserOnSwitchEnabled() { synchronized (mLock) { if (mStopUserOnSwitch != STOP_USER_ON_SWITCH_DEFAULT) { final boolean value = mStopUserOnSwitch == STOP_USER_ON_SWITCH_TRUE; - Slogf.i(TAG, "shouldStopUserOnSwitch(): returning overridden value (%b)", value); + Slogf.i(TAG, "isStopUserOnSwitchEnabled(): returning overridden value (%b)", value); return value; } } @@ -521,6 +530,26 @@ class UserController implements Handler.Callback { return property == -1 ? mDelayUserDataLocking : property == 1; } + /** + * Get whether or not the previous user's packages will be killed before the user is + * stopped during a user switch. + * + * <p> The primary use case of this method is for {@link com.android.server.SystemService} + * classes to call this API in their + * {@link com.android.server.SystemService#onUserSwitching} method implementation to prevent + * restarting any of the previous user's processes that will be killed during the user switch. + */ + boolean isEarlyPackageKillEnabledForUserSwitch(int fromUserId, int toUserId) { + // NOTE: The logic in this method could be extended to cover other cases where + // the previous user is also stopped like: guest users, ephemeral users, + // and users with DISALLOW_RUN_IN_BACKGROUND. Currently, this is not done + // because early killing is not enabled for these cases by default. + if (fromUserId == UserHandle.USER_SYSTEM) { + return false; + } + return isStopUserOnSwitchEnabled(); + } + void finishUserSwitch(UserState uss) { // This call holds the AM lock so we post to the handler. mHandler.post(() -> { @@ -1247,6 +1276,7 @@ class UserController implements Handler.Callback { return; } uss.setState(UserState.STATE_SHUTDOWN); + mDoNotAbortShutdownUserIds.remove(userId); } TimingsTraceAndSlog t = new TimingsTraceAndSlog(); t.traceBegin("setUserState-STATE_SHUTDOWN-" + userId + "-[stopUser]"); @@ -1555,7 +1585,8 @@ class UserController implements Handler.Callback { private void stopPackagesOfStoppedUser(@UserIdInt int userId, String reason) { if (DEBUG_MU) Slogf.i(TAG, "stopPackagesOfStoppedUser(%d): %s", userId, reason); - mInjector.activityManagerForceStopPackage(userId, reason); + mInjector.activityManagerForceStopUserPackages(userId, reason, + /* evenImportantServices= */ true); if (mInjector.getUserManager().isPreCreated(userId)) { // Don't fire intent for precreated. return; @@ -1608,6 +1639,21 @@ class UserController implements Handler.Callback { } } + private void stopPreviousUserPackagesIfEnabled(int fromUserId, int toUserId) { + if (!android.multiuser.Flags.stopPreviousUserApps() + || !isEarlyPackageKillEnabledForUserSwitch(fromUserId, toUserId)) { + return; + } + // Stop the previous user's packages early to reduce resource usage + // during user switching. Only do this when the previous user will + // be stopped regardless. + synchronized (mLock) { + mDoNotAbortShutdownUserIds.add(fromUserId); + } + mInjector.activityManagerForceStopUserPackages(fromUserId, + "early stop user packages", /* evenImportantServices= */ false); + } + void scheduleStartProfiles() { // Parent user transition to RUNNING_UNLOCKING happens on FgThread, so it is busy, there is // a chance the profile will reach RUNNING_LOCKED while parent is still locked, so no @@ -1889,7 +1935,8 @@ class UserController implements Handler.Callback { updateStartedUserArrayLU(); needStart = true; updateUmState = true; - } else if (uss.state == UserState.STATE_SHUTDOWN) { + } else if (uss.state == UserState.STATE_SHUTDOWN + || mDoNotAbortShutdownUserIds.contains(userId)) { Slogf.i(TAG, "User #" + userId + " is shutting down - will start after full shutdown"); mPendingUserStarts.add(new PendingUserStart(userId, userStartMode, @@ -2293,7 +2340,7 @@ class UserController implements Handler.Callback { hasUserRestriction(UserManager.DISALLOW_RUN_IN_BACKGROUND, oldUserId); synchronized (mLock) { // If running in background is disabled or mStopUserOnSwitch mode, stop the user. - if (hasRestriction || shouldStopUserOnSwitch()) { + if (hasRestriction || isStopUserOnSwitchEnabled()) { Slogf.i(TAG, "Stopping user %d and its profiles on user switch", oldUserId); stopUsersLU(oldUserId, /* allowDelayedLocking= */ false, null, null); return; @@ -3425,7 +3472,7 @@ class UserController implements Handler.Callback { pw.println(" mLastActiveUsersForDelayedLocking:" + mLastActiveUsersForDelayedLocking); pw.println(" mDelayUserDataLocking:" + mDelayUserDataLocking); pw.println(" mAllowUserUnlocking:" + mAllowUserUnlocking); - pw.println(" shouldStopUserOnSwitch():" + shouldStopUserOnSwitch()); + pw.println(" isStopUserOnSwitchEnabled():" + isStopUserOnSwitchEnabled()); pw.println(" mStopUserOnSwitch:" + mStopUserOnSwitch); pw.println(" mMaxRunningUsers:" + mMaxRunningUsers); pw.println(" mBackgroundUserScheduledStopTimeSecs:" @@ -3522,6 +3569,7 @@ class UserController implements Handler.Callback { Integer.toString(msg.arg1), msg.arg1); mInjector.getSystemServiceManager().onUserSwitching(msg.arg2, msg.arg1); + stopPreviousUserPackagesIfEnabled(msg.arg2, msg.arg1); scheduleOnUserCompletedEvent(msg.arg1, UserCompletedEventType.EVENT_TYPE_USER_SWITCHING, USER_COMPLETED_EVENT_DELAY_MS); @@ -3896,10 +3944,10 @@ class UserController implements Handler.Callback { }.sendNext(); } - void activityManagerForceStopPackage(@UserIdInt int userId, String reason) { + void activityManagerForceStopUserPackages(@UserIdInt int userId, String reason, + boolean evenImportantServices) { synchronized (mService) { - mService.forceStopPackageLocked(null, -1, false, false, true, false, false, false, - userId, reason); + mService.forceStopUserPackagesLocked(userId, reason, evenImportantServices); } }; diff --git a/services/core/java/com/android/server/appop/DiscreteRegistry.java b/services/core/java/com/android/server/appop/DiscreteRegistry.java index 539dbca30d6b..2ce4623a19b4 100644 --- a/services/core/java/com/android/server/appop/DiscreteRegistry.java +++ b/services/core/java/com/android/server/appop/DiscreteRegistry.java @@ -1413,11 +1413,11 @@ final class DiscreteRegistry { pw.print("-"); pw.print(flagsToString(mOpFlag)); pw.print("] at "); - date.setTime(discretizeTimeStamp(mNoteTime)); + date.setTime(mNoteTime); pw.print(sdf.format(date)); if (mNoteDuration != -1) { pw.print(" for "); - pw.print(discretizeDuration(mNoteDuration)); + pw.print(mNoteDuration); pw.print(" milliseconds "); } if (mAttributionFlags != AppOpsManager.ATTRIBUTION_FLAGS_NONE) { diff --git a/services/core/java/com/android/server/biometrics/BiometricService.java b/services/core/java/com/android/server/biometrics/BiometricService.java index fe73bfe178f0..feef5409d14f 100644 --- a/services/core/java/com/android/server/biometrics/BiometricService.java +++ b/services/core/java/com/android/server/biometrics/BiometricService.java @@ -385,9 +385,9 @@ public class BiometricService extends SystemService { DEFAULT_APP_ENABLED ? 1 : 0 /* default */, userId) != 0); } else if (MANDATORY_BIOMETRICS_ENABLED.equals(uri)) { - updateMandatoryBiometricsForAllProfiles(); + updateMandatoryBiometricsForAllProfiles(userId); } else if (MANDATORY_BIOMETRICS_REQUIREMENTS_SATISFIED.equals(uri)) { - updateMandatoryBiometricsRequirementsForAllProfiles(); + updateMandatoryBiometricsRequirementsForAllProfiles(userId); } } @@ -431,16 +431,15 @@ public class BiometricService extends SystemService { public boolean getMandatoryBiometricsEnabledAndRequirementsSatisfiedForUser(int userId) { if (!mMandatoryBiometricsEnabled.containsKey(userId)) { - updateMandatoryBiometricsForAllProfiles(); + updateMandatoryBiometricsForAllProfiles(userId); } if (!mMandatoryBiometricsRequirementsSatisfied.containsKey(userId)) { - updateMandatoryBiometricsRequirementsForAllProfiles(); + updateMandatoryBiometricsRequirementsForAllProfiles(userId); } return mMandatoryBiometricsEnabled.getOrDefault(userId, DEFAULT_MANDATORY_BIOMETRICS_STATUS) && mMandatoryBiometricsRequirementsSatisfied.getOrDefault(userId, DEFAULT_MANDATORY_BIOMETRICS_REQUIREMENTS_SATISFIED_STATUS) - && mBiometricEnabledForApps.getOrDefault(userId, DEFAULT_APP_ENABLED) && getEnabledForApps(userId) && (mFingerprintEnrolledForUser.getOrDefault(userId, false /* default */) || mFaceEnrolledForUser.getOrDefault(userId, false /* default */)); @@ -455,25 +454,31 @@ public class BiometricService extends SystemService { } } - private void updateMandatoryBiometricsForAllProfiles() { - final int mainUserId = mUserManager.getMainUser().getIdentifier(); - for (UserHandle userHandle: mUserManager.getUserProfiles()) { - mMandatoryBiometricsEnabled.put(userHandle.getIdentifier(), + private void updateMandatoryBiometricsForAllProfiles(int userId) { + int effectiveUserId = userId; + if (mUserManager.getMainUser() != null) { + effectiveUserId = mUserManager.getMainUser().getIdentifier(); + } + for (int profileUserId: mUserManager.getEnabledProfileIds(effectiveUserId)) { + mMandatoryBiometricsEnabled.put(profileUserId, Settings.Secure.getIntForUser( mContentResolver, Settings.Secure.MANDATORY_BIOMETRICS, DEFAULT_MANDATORY_BIOMETRICS_STATUS ? 1 : 0, - mainUserId) != 0); + effectiveUserId) != 0); } } - private void updateMandatoryBiometricsRequirementsForAllProfiles() { - final int mainUserId = mUserManager.getMainUser().getIdentifier(); - for (UserHandle userHandle: mUserManager.getUserProfiles()) { - mMandatoryBiometricsRequirementsSatisfied.put(userHandle.getIdentifier(), + private void updateMandatoryBiometricsRequirementsForAllProfiles(int userId) { + int effectiveUserId = userId; + if (mUserManager.getMainUser() != null) { + effectiveUserId = mUserManager.getMainUser().getIdentifier(); + } + for (int profileUserId: mUserManager.getEnabledProfileIds(effectiveUserId)) { + mMandatoryBiometricsRequirementsSatisfied.put(profileUserId, Settings.Secure.getIntForUser(mContentResolver, Settings.Secure.MANDATORY_BIOMETRICS_REQUIREMENTS_SATISFIED, DEFAULT_MANDATORY_BIOMETRICS_REQUIREMENTS_SATISFIED_STATUS ? 1 : 0, - mainUserId) != 0); + effectiveUserId) != 0); } } diff --git a/services/core/java/com/android/server/biometrics/Utils.java b/services/core/java/com/android/server/biometrics/Utils.java index df29ca45930e..fb8a81be4b89 100644 --- a/services/core/java/com/android/server/biometrics/Utils.java +++ b/services/core/java/com/android/server/biometrics/Utils.java @@ -586,6 +586,8 @@ public class Utils { } } + // LINT.IfChange + /** * Checks if a client package is running in the background. * @@ -618,4 +620,6 @@ public class Utils { return true; } + // LINT.ThenChange(frameworks/base/packages/SystemUI/shared/biometrics/src/com/android + // /systemui/biometrics/Utils.kt) } diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/BiometricTestSessionImpl.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/BiometricTestSessionImpl.java index 0fdd57d64d8d..dca14914a572 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/BiometricTestSessionImpl.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/BiometricTestSessionImpl.java @@ -264,4 +264,11 @@ public class BiometricTestSessionImpl extends ITestSession.Stub { } }); } + + @android.annotation.EnforcePermission(android.Manifest.permission.TEST_BIOMETRIC) + @Override + public int getSensorId() { + super.getSensorId_enforcePermission(); + return mSensorId; + } } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/BiometricTestSessionImpl.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/BiometricTestSessionImpl.java index 8dc560b0e0b5..caa2c1c34ff7 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/BiometricTestSessionImpl.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/BiometricTestSessionImpl.java @@ -293,4 +293,11 @@ class BiometricTestSessionImpl extends ITestSession.Stub { } }); } + + @android.annotation.EnforcePermission(android.Manifest.permission.TEST_BIOMETRIC) + @Override + public int getSensorId() { + super.getSensorId_enforcePermission(); + return mSensorId; + } }
\ No newline at end of file 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/InputMethodManagerInternal.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java index 4d06f50d4d45..e36d5bbbd8d2 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java @@ -16,7 +16,7 @@ package com.android.server.inputmethod; -import static com.android.server.inputmethod.InputMethodUtils.NOT_A_SUBTYPE_ID; +import static com.android.server.inputmethod.InputMethodUtils.NOT_A_SUBTYPE_INDEX; import static java.lang.annotation.RetentionPolicy.SOURCE; @@ -140,23 +140,26 @@ public abstract class InputMethodManagerInternal { * to be switched. */ public boolean switchToInputMethod(@NonNull String imeId, @UserIdInt int userId) { - return switchToInputMethod(imeId, NOT_A_SUBTYPE_ID, userId); + return switchToInputMethod(imeId, NOT_A_SUBTYPE_INDEX, userId); } /** * Force switch to the enabled input method by {@code imeId} for the current user. If the input - * method with {@code imeId} is not enabled or not installed, do nothing. If {@code subtypeId} - * is also supplied (not {@link InputMethodUtils#NOT_A_SUBTYPE_ID}) and valid, also switches to - * it, otherwise the system decides the most sensible default subtype to use. + * method with {@code imeId} is not enabled or not installed, do nothing. If + * {@code subtypeIndex} is also supplied (not {@link InputMethodUtils#NOT_A_SUBTYPE_INDEX}) and + * valid, also switches to it, otherwise the system decides the most sensible default subtype to + * use. * - * @param imeId the input method ID to be switched to - * @param subtypeId the input method subtype ID to be switched to - * @param userId the user ID to be queried + * @param imeId the input method ID to be switched to + * @param subtypeIndex the subtype to be switched to, as an index in the input method's array of + * subtypes, or {@link InputMethodUtils#NOT_A_SUBTYPE_INDEX} if the system + * should decide the most sensible subtype + * @param userId the user ID to be queried * @return {@code true} if the current input method was successfully switched to the input * method by {@code imeId}; {@code false} the input method with {@code imeId} is not available * to be switched. */ - public abstract boolean switchToInputMethod(@NonNull String imeId, int subtypeId, + public abstract boolean switchToInputMethod(@NonNull String imeId, int subtypeIndex, @UserIdInt int userId); /** @@ -376,7 +379,7 @@ public abstract class InputMethodManagerInternal { } @Override - public boolean switchToInputMethod(@NonNull String imeId, int subtypeId, + public boolean switchToInputMethod(@NonNull String imeId, int subtypeIndex, @UserIdInt int userId) { return false; } diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index ea3d7d10f3f8..8afbd56728e4 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -49,10 +49,12 @@ import static android.view.inputmethod.ConnectionlessHandwritingCallback.CONNECT import static com.android.server.inputmethod.ImeVisibilityStateComputer.ImeTargetWindowState; import static com.android.server.inputmethod.ImeVisibilityStateComputer.ImeVisibilityResult; -import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_SHOW_IME; import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_HIDE_IME; +import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_SHOW_IME; import static com.android.server.inputmethod.InputMethodBindingController.TIME_TO_RECONNECT; +import static com.android.server.inputmethod.InputMethodSettings.INVALID_SUBTYPE_HASHCODE; import static com.android.server.inputmethod.InputMethodSubtypeSwitchingController.MODE_AUTO; +import static com.android.server.inputmethod.InputMethodUtils.NOT_A_SUBTYPE_INDEX; import static com.android.server.inputmethod.InputMethodUtils.isSoftInputModeStateVisibleAllowed; import static java.lang.annotation.RetentionPolicy.SOURCE; @@ -81,7 +83,6 @@ import android.content.pm.PackageManagerInternal; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.content.pm.UserInfo; -import android.content.res.Configuration; import android.content.res.Resources; import android.hardware.input.InputManager; import android.inputmethodservice.InputMethodService; @@ -273,11 +274,6 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. private static final int MSG_NOTIFY_IME_UID_TO_AUDIO_SERVICE = 7000; - private static final int NOT_A_SUBTYPE_ID = InputMethodUtils.NOT_A_SUBTYPE_ID; - - private static final int INVALID_SUBTYPE_HASHCODE = - InputMethodSettings.INVALID_SUBTYPE_HASHCODE; - private static final String TAG_TRY_SUPPRESSING_IME_SWITCHER = "TrySuppressingImeSwitcher"; private static final String HANDLER_THREAD_NAME = "android.imms"; private static final String PACKAGE_MONITOR_THREAD_NAME = "android.imms2"; @@ -303,28 +299,6 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. private final String[] mNonPreemptibleInputMethods; /** - * Whether the new Input Method Switcher menu is enabled. - * - * @see #shouldEnableNewInputMethodSwitcherMenu - */ - @SharedByAllUsersField - private final boolean mNewInputMethodSwitcherMenuEnabled; - - /** - * Returns {@code true} if the new Input Method Switcher menu is enabled. This will be - * {@code false} for watches and small screen devices. - * - * @param context the context to check the device configuration for. - */ - private static boolean shouldEnableNewInputMethodSwitcherMenu(@NonNull Context context) { - final boolean isWatch = context.getPackageManager() - .hasSystemFeature(PackageManager.FEATURE_WATCH); - final boolean isSmallScreen = (context.getResources().getConfiguration().screenLayout - & Configuration.SCREENLAYOUT_SIZE_MASK) == Configuration.SCREENLAYOUT_SIZE_SMALL; - return Flags.imeSwitcherRevamp() && !isWatch && !isSmallScreen; - } - - /** * See {@link #shouldEnableConcurrentMultiUserMode(Context)} about when set to be {@code true}. */ @SharedByAllUsersField @@ -356,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; } /** @@ -369,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; } /** @@ -385,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; @@ -396,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 @@ -566,7 +553,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @GuardedBy("ImfLock.class") @Nullable IInputMethodInvoker getCurMethodLocked() { - return getInputMethodBindingController(mCurrentUserId).getCurMethod(); + return getInputMethodBindingController(mCurrentImeUserId).getCurMethod(); } /** @@ -612,8 +599,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. private void onSecureSettingsChangedLocked(@NonNull String key, @UserIdInt int userId) { switch (key) { case Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD: { - if (!mNewInputMethodSwitcherMenuEnabled) { - if (userId == mCurrentUserId) { + if (!Flags.imeSwitcherRevamp()) { + if (userId == mCurrentImeUserId) { mMenuController.updateKeyboardFromSettingsLocked(userId); } } @@ -675,12 +662,12 @@ 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; - if (mNewInputMethodSwitcherMenuEnabled) { + final int userId = mCurrentImeUserId; + if (Flags.imeSwitcherRevamp()) { final var bindingController = getInputMethodBindingController(userId); mMenuControllerNew.hide(bindingController.getCurTokenDisplayId(), userId); } else { @@ -718,7 +705,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. userData.mRawInputMethodMap.set(rawMethodMap); final var methodMap = rawMethodMap.toInputMethodMap(additionalSubtypeMap, DirectBootAwareness.AUTO, - mUserManagerInternal.isUserUnlockingOrUnlocked(userId)); + userData.mIsUnlockingOrUnlocked.get()); final var settings = InputMethodSettings.create(methodMap, userId); InputMethodSettingsRepository.put(userId, settings); } @@ -842,7 +829,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. final var newMethodMap = userData.mRawInputMethodMap.get().toInputMethodMap( newAdditionalSubtypeMap, DirectBootAwareness.AUTO, - mUserManagerInternal.isUserUnlockingOrUnlocked(userId)); + userData.mIsUnlockingOrUnlocked.get()); final boolean noUpdate = InputMethodMap.areSame(settings.getMethodMap(), newMethodMap); if (noUpdate && imesToBeDisabled.isEmpty()) { @@ -1072,6 +1059,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. final int userId = user.getUserIdentifier(); final var userData = mService.getUserData(userId); final boolean userUnlocked = true; + userData.mIsUnlockingOrUnlocked.set(userUnlocked); SecureSettingsWrapper.onUserUnlocking(userId); final var methodMap = userData.mRawInputMethodMap.get().toInputMethodMap( AdditionalSubtypeMapRepository.get(userId), DirectBootAwareness.AUTO, @@ -1122,9 +1110,12 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. final var additionalSubtypeMap = AdditionalSubtypeMapRepository.get(userId); final var rawMethodMap = queryRawInputMethodServiceMap(context, userId); userData.mRawInputMethodMap.set(rawMethodMap); + + final boolean unlocked = userManagerInternal.isUserUnlockingOrUnlocked(userId); + userData.mIsUnlockingOrUnlocked.set(unlocked); final var methodMap = rawMethodMap.toInputMethodMap(additionalSubtypeMap, - DirectBootAwareness.AUTO, - userManagerInternal.isUserUnlockingOrUnlocked(userId)); + DirectBootAwareness.AUTO, unlocked); + final var settings = InputMethodSettings.create(methodMap, userId); InputMethodSettingsRepository.put(userId, settings); @@ -1152,6 +1143,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. final var additionalSubtypeMap = AdditionalSubtypeMapRepository.get(userId); final var rawMethodMap = userData.mRawInputMethodMap.get(); final boolean userUnlocked = false; // Stopping a user also locks their storage. + userData.mIsUnlockingOrUnlocked.set(userUnlocked); final var methodMap = rawMethodMap.toInputMethodMap(additionalSubtypeMap, DirectBootAwareness.AUTO, userUnlocked); InputMethodSettingsRepository.put(userId, @@ -1205,11 +1197,10 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. mUserManagerInternal = LocalServices.getService(UserManagerInternal.class); mSlotIme = mContext.getString(com.android.internal.R.string.status_bar_ime); - mNewInputMethodSwitcherMenuEnabled = shouldEnableNewInputMethodSwitcherMenu(mContext); mShowOngoingImeSwitcherForPhones = false; - mCurrentUserId = mActivityManagerInternal.getCurrentUserId(); + mCurrentImeUserId = mActivityManagerInternal.getCurrentUserId(); final IntFunction<InputMethodBindingController> bindingControllerFactory = userId -> new InputMethodBindingController(userId, InputMethodManagerService.this); @@ -1221,7 +1212,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. : bindingControllerFactory, visibilityStateComputerFactory); mMenuController = new InputMethodMenuController(this); - mMenuControllerNew = mNewInputMethodSwitcherMenuEnabled + mMenuControllerNew = Flags.imeSwitcherRevamp() ? new InputMethodMenuControllerNew() : null; mVisibilityApplier = new DefaultImeVisibilityApplier(this); @@ -1289,7 +1280,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. if (DEBUG) { Slog.i(TAG, "Default found, using " + defIm.getId()); } - setSelectedInputMethodAndSubtypeLocked(defIm, NOT_A_SUBTYPE_ID, false, userId); + setSelectedInputMethodAndSubtypeLocked(defIm, NOT_A_SUBTYPE_INDEX, false, userId); } @NonNull @@ -1304,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); @@ -1329,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); @@ -1355,6 +1346,17 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } updateFromSettingsLocked(true, newUserId); + // Special workaround for b/356879517. + // KeyboardLayoutManager still expects onInputMethodSubtypeChangedForKeyboardLayoutMapping + // to be called back upon IME user switching, while we are actively deprecating the concept + // of "current IME user" at b/350386877. + // TODO(b/356879517): Come up with a way to avoid this special handling. + if (newUserData.mSubtypeForKeyboardLayoutMapping != null) { + final var subtypeHandleAndSubtype = newUserData.mSubtypeForKeyboardLayoutMapping; + mInputManagerInternal.onInputMethodSubtypeChangedForKeyboardLayoutMapping( + newUserId, subtypeHandleAndSubtype.first, subtypeHandleAndSubtype.second); + } + if (initialUserSwitch) { InputMethodUtils.setNonSelectedSystemImesDisabledUntilUsed( getPackageManagerForUser(mContext, newUserId), @@ -1418,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) { @@ -1604,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 @@ -1635,7 +1637,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. final var userData = getUserData(userId); final var methodMap = userData.mRawInputMethodMap.get().toInputMethodMap( AdditionalSubtypeMapRepository.get(userId), directBootAwareness, - mUserManagerInternal.isUserUnlockingOrUnlocked(userId)); + userData.mIsUnlockingOrUnlocked.get()); final var settings = InputMethodSettings.create(methodMap, userId); // Create a copy. final ArrayList<InputMethodInfo> methodList = new ArrayList<>(settings.getMethodList()); @@ -1810,7 +1812,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. ImeTracker.PHASE_SERVER_WAIT_IME); userData.mCurStatsToken = null; // TODO: Make mMenuController multi-user aware - if (mNewInputMethodSwitcherMenuEnabled) { + if (Flags.imeSwitcherRevamp()) { mMenuControllerNew.hide(bindingController.getCurTokenDisplayId(), userId); } else { mMenuController.hideInputMethodMenuLocked(userId); @@ -2018,7 +2020,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. if (deviceMethodId == null) { visibilityStateComputer.getImePolicy().setImeHiddenByDisplayPolicy(true); } else if (!Objects.equals(deviceMethodId, selectedMethodId)) { - setInputMethodLocked(deviceMethodId, NOT_A_SUBTYPE_ID, + setInputMethodLocked(deviceMethodId, NOT_A_SUBTYPE_INDEX, bindingController.getDeviceIdToShowIme(), userId); selectedMethodId = deviceMethodId; } @@ -2556,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) { @@ -2588,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) { @@ -2620,7 +2622,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. if (!mShowOngoingImeSwitcherForPhones) return false; // When the IME switcher dialog is shown, the IME switcher button should be hidden. // TODO(b/305849394): Make mMenuController multi-user aware. - final boolean switcherMenuShowing = mNewInputMethodSwitcherMenuEnabled + final boolean switcherMenuShowing = Flags.imeSwitcherRevamp() ? mMenuControllerNew.isShowing() : mMenuController.getSwitchingDialogLocked() != null; if (switcherMenuShowing) { @@ -2636,12 +2638,10 @@ 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() - && !mNewInputMethodSwitcherMenuEnabled) { + if (mWindowManagerInternal.isHardKeyboardAvailable() && !Flags.imeSwitcherRevamp()) { // When physical keyboard is attached, we show the ime switcher (or notification if // NavBar is not available) because SHOW_IME_WITH_HARD_KEYBOARD settings currently // exists in the IME switcher dialog. Might be OK to remove this condition once @@ -2652,7 +2652,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); - if (mNewInputMethodSwitcherMenuEnabled) { + if (Flags.imeSwitcherRevamp()) { // The IME switcher button should be shown when the current IME specified a // language settings activity. final var curImi = settings.getMethodMap().get(settings.getSelectedInputMethod()); @@ -2790,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); @@ -2803,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 @@ -2825,7 +2825,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } final var curId = bindingController.getCurId(); // TODO(b/305849394): Make mMenuController multi-user aware. - final boolean switcherMenuShowing = mNewInputMethodSwitcherMenuEnabled + final boolean switcherMenuShowing = Flags.imeSwitcherRevamp() ? mMenuControllerNew.isShowing() : mMenuController.getSwitchingDialogLocked() != null; if (switcherMenuShowing @@ -2847,7 +2847,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @GuardedBy("ImfLock.class") void updateFromSettingsLocked(boolean enabledMayChange, @UserIdInt int userId) { updateInputMethodsFromSettingsLocked(enabledMayChange, userId); - if (!mNewInputMethodSwitcherMenuEnabled) { + if (!Flags.imeSwitcherRevamp()) { mMenuController.updateKeyboardFromSettingsLocked(userId); } } @@ -2915,7 +2915,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } if (!TextUtils.isEmpty(id)) { try { - setInputMethodLocked(id, settings.getSelectedInputMethodSubtypeId(id), userId); + setInputMethodLocked(id, settings.getSelectedInputMethodSubtypeIndex(id), userId); } catch (IllegalArgumentException e) { Slog.w(TAG, "Unknown input method from prefs: " + id, e); resetCurrentMethodAndClientLocked(UnbindReason.SWITCH_IME_FAILED, userId); @@ -2938,17 +2938,28 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. ? subtype : null; final InputMethodSubtypeHandle newSubtypeHandle = normalizedSubtype != null ? InputMethodSubtypeHandle.of(imi, normalizedSubtype) : null; + + final var userData = getUserData(userId); + + // A workaround for b/356879517. KeyboardLayoutManager has relied on an implementation + // detail that IMMS triggers this callback only for the current IME user. + // TODO(b/357663774): Figure out how to better handle this scenario. + userData.mSubtypeForKeyboardLayoutMapping = + Pair.create(newSubtypeHandle, normalizedSubtype); + if (userId != mCurrentImeUserId) { + return; + } mInputManagerInternal.onInputMethodSubtypeChangedForKeyboardLayoutMapping( userId, newSubtypeHandle, normalizedSubtype); } @GuardedBy("ImfLock.class") - void setInputMethodLocked(String id, int subtypeId, @UserIdInt int userId) { - setInputMethodLocked(id, subtypeId, DEVICE_ID_DEFAULT, userId); + void setInputMethodLocked(String id, int subtypeIndex, @UserIdInt int userId) { + setInputMethodLocked(id, subtypeIndex, DEVICE_ID_DEFAULT, userId); } @GuardedBy("ImfLock.class") - void setInputMethodLocked(String id, int subtypeId, int deviceId, @UserIdInt int userId) { + void setInputMethodLocked(String id, int subtypeIndex, int deviceId, @UserIdInt int userId) { final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); InputMethodInfo info = settings.getMethodMap().get(id); if (info == null) { @@ -2965,25 +2976,25 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } final InputMethodSubtype oldSubtype = bindingController.getCurrentSubtype(); final InputMethodSubtype newSubtype; - if (subtypeId >= 0 && subtypeId < subtypeCount) { - newSubtype = info.getSubtypeAt(subtypeId); + if (subtypeIndex >= 0 && subtypeIndex < subtypeCount) { + newSubtype = info.getSubtypeAt(subtypeIndex); } else { // If subtype is null, try to find the most applicable one from // getCurrentInputMethodSubtype. - subtypeId = NOT_A_SUBTYPE_ID; + subtypeIndex = NOT_A_SUBTYPE_INDEX; // TODO(b/347083680): The method below has questionable behaviors. newSubtype = bindingController.getCurrentInputMethodSubtype(); if (newSubtype != null) { for (int i = 0; i < subtypeCount; ++i) { if (Objects.equals(newSubtype, info.getSubtypeAt(i))) { - subtypeId = i; + subtypeIndex = i; break; } } } } if (!Objects.equals(newSubtype, oldSubtype)) { - setSelectedInputMethodAndSubtypeLocked(info, subtypeId, true, userId); + setSelectedInputMethodAndSubtypeLocked(info, subtypeIndex, true, userId); IInputMethodInvoker curMethod = bindingController.getCurMethod(); if (curMethod != null) { updateSystemUiLocked(bindingController.getImeWindowVis(), @@ -3010,9 +3021,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } final long ident = Binder.clearCallingIdentity(); try { - // Set a subtype to this input method. - // subtypeId the name of a subtype which will be set. - setSelectedInputMethodAndSubtypeLocked(info, subtypeId, false, userId); + setSelectedInputMethodAndSubtypeLocked(info, subtypeIndex, false, userId); // mCurMethodId should be updated after setSelectedInputMethodAndSubtypeLocked() // because mCurMethodId is stored as a history in // setSelectedInputMethodAndSubtypeLocked(). @@ -3707,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); @@ -3754,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. @@ -4016,7 +4025,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @IInputMethodManagerImpl.PermissionVerified(Manifest.permission.TEST_INPUT_METHOD) public boolean isInputMethodPickerShownForTest() { synchronized (ImfLock.class) { - return mNewInputMethodSwitcherMenuEnabled + return Flags.imeSwitcherRevamp() ? mMenuControllerNew.isShowing() : mMenuController.isisInputMethodPickerShownForTestLocked(); } @@ -4025,11 +4034,12 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. /** * Gets the list of Input Method Switcher Menu items and the index of the selected item. * - * @param items the list of input method and subtype items. - * @param selectedImeId the ID of the selected input method. - * @param selectedSubtypeId the ID of the selected input method subtype, - * or {@link #NOT_A_SUBTYPE_ID} if no subtype is selected. - * @param userId the ID of the user for which to get the menu items. + * @param items the list of input method and subtype items. + * @param selectedImeId the ID of the selected input method. + * @param selectedSubtypeIndex the index of the selected subtype in the input method's array of + * subtypes, or {@link InputMethodUtils#NOT_A_SUBTYPE_INDEX} if no + * subtype is selected. + * @param userId the ID of the user for which to get the menu items. * @return the list of menu items, and the index of the selected item, * or {@code -1} if no item is selected. */ @@ -4037,17 +4047,17 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @NonNull private Pair<List<MenuItem>, Integer> getInputMethodPickerItems( @NonNull List<ImeSubtypeListItem> items, @Nullable String selectedImeId, - int selectedSubtypeId, @UserIdInt int userId) { + int selectedSubtypeIndex, @UserIdInt int userId) { final var bindingController = getInputMethodBindingController(userId); final var settings = InputMethodSettingsRepository.get(userId); - if (selectedSubtypeId == NOT_A_SUBTYPE_ID) { + if (selectedSubtypeIndex == NOT_A_SUBTYPE_INDEX) { // TODO(b/351124299): Check if this fallback logic is still necessary. final var curSubtype = bindingController.getCurrentInputMethodSubtype(); if (curSubtype != null) { final var curMethodId = bindingController.getSelectedMethodId(); final var curImi = settings.getMethodMap().get(curMethodId); - selectedSubtypeId = SubtypeUtils.getSubtypeIdFromHashCode( + selectedSubtypeIndex = SubtypeUtils.getSubtypeIndexFromHashCode( curImi, curSubtype.hashCode()); } } @@ -4062,19 +4072,19 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. final var item = items.get(i); final var imeId = item.mImi.getId(); if (imeId.equals(selectedImeId)) { - final int subtypeId = item.mSubtypeId; + final int subtypeIndex = item.mSubtypeIndex; // Check if this is the selected IME-subtype pair. - if ((subtypeId == 0 && selectedSubtypeId == NOT_A_SUBTYPE_ID) - || subtypeId == NOT_A_SUBTYPE_ID - || subtypeId == selectedSubtypeId) { + if ((subtypeIndex == 0 && selectedSubtypeIndex == NOT_A_SUBTYPE_INDEX) + || subtypeIndex == NOT_A_SUBTYPE_INDEX + || subtypeIndex == selectedSubtypeIndex) { selectedIndex = i; } } final boolean hasHeader = !imeId.equals(prevImeId); final boolean hasDivider = hasHeader && prevImeId != null; prevImeId = imeId; - menuItems.add(new MenuItem(item.mImeName, item.mSubtypeName, item.mImi, item.mSubtypeId, - hasHeader, hasDivider)); + menuItems.add(new MenuItem(item.mImeName, item.mSubtypeName, item.mImi, + item.mSubtypeIndex, hasHeader, hasDivider)); } return new Pair<>(menuItems, selectedIndex); @@ -4131,10 +4141,10 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. imi.getPackageName(), callingUid, userId, settings)) { throw getExceptionForUnknownImeId(id); } - final int subtypeId = subtype != null - ? SubtypeUtils.getSubtypeIdFromHashCode(imi, subtype.hashCode()) - : NOT_A_SUBTYPE_ID; - setInputMethodWithSubtypeIdLocked(id, subtypeId, userId); + final int subtypeIndex = subtype != null + ? SubtypeUtils.getSubtypeIndexFromHashCode(imi, subtype.hashCode()) + : NOT_A_SUBTYPE_INDEX; + setInputMethodWithSubtypeIndexLocked(id, subtypeIndex, userId); } @BinderThread @@ -4152,18 +4162,18 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } final var currentSubtype = bindingController.getCurrentSubtype(); String targetLastImiId = null; - int subtypeId = NOT_A_SUBTYPE_ID; + int subtypeIndex = NOT_A_SUBTYPE_INDEX; if (lastIme != null && lastImi != null) { final boolean imiIdIsSame = lastImi.getId().equals( bindingController.getSelectedMethodId()); final int lastSubtypeHash = Integer.parseInt(lastIme.second); - final int currentSubtypeHash = currentSubtype == null ? NOT_A_SUBTYPE_ID + final int currentSubtypeHash = currentSubtype == null ? NOT_A_SUBTYPE_INDEX : currentSubtype.hashCode(); // If the last IME is the same as the current IME and the last subtype is not // defined, there is no need to switch to the last IME. if (!imiIdIsSame || lastSubtypeHash != currentSubtypeHash) { targetLastImiId = lastIme.first; - subtypeId = SubtypeUtils.getSubtypeIdFromHashCode(lastImi, lastSubtypeHash); + subtypeIndex = SubtypeUtils.getSubtypeIndexFromHashCode(lastImi, lastSubtypeHash); } } @@ -4173,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(); - subtypeId = SubtypeUtils.getSubtypeIdFromHashCode(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; } } } @@ -4206,9 +4214,9 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. if (DEBUG) { Slog.d(TAG, "Switch to: " + lastImi.getId() + ", " + lastIme.second + ", from: " + bindingController.getSelectedMethodId() + ", " - + subtypeId); + + subtypeIndex); } - setInputMethodWithSubtypeIdLocked(targetLastImiId, subtypeId, userId); + setInputMethodWithSubtypeIndexLocked(targetLastImiId, subtypeIndex, userId); return true; } else { return false; @@ -4228,7 +4236,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. if (nextSubtype == null) { return false; } - setInputMethodWithSubtypeIdLocked(nextSubtype.mImi.getId(), nextSubtype.mSubtypeId, + setInputMethodWithSubtypeIndexLocked(nextSubtype.mImi.getId(), nextSubtype.mSubtypeIndex, userData.mUserId); return true; } @@ -4294,7 +4302,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. try { final var methodMap = userData.mRawInputMethodMap.get().toInputMethodMap( AdditionalSubtypeMapRepository.get(userId), DirectBootAwareness.AUTO, - mUserManagerInternal.isUserUnlockingOrUnlocked(userId)); + userData.mIsUnlockingOrUnlocked.get()); final var newSettings = InputMethodSettings.create(methodMap, userId); InputMethodSettingsRepository.put(userId, newSettings); postInputMethodSettingUpdatedLocked(false /* resetDefaultEnabledIme */, userId); @@ -4445,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(); @@ -4634,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; @@ -4661,7 +4669,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. proto.write(IS_INTERACTIVE, mIsInteractive); proto.write(BACK_DISPOSITION, bindingController.getBackDisposition()); proto.write(IME_WINDOW_VISIBILITY, bindingController.getImeWindowVis()); - if (!mNewInputMethodSwitcherMenuEnabled) { + if (!Flags.imeSwitcherRevamp()) { proto.write(SHOW_IME_WITH_HARD_KEYBOARD, mMenuController.getShowImeWithHardKeyboard()); } @@ -4715,7 +4723,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } @GuardedBy("ImfLock.class") - private void setInputMethodWithSubtypeIdLocked(String id, int subtypeId, + private void setInputMethodWithSubtypeIndexLocked(String id, int subtypeIndex, @UserIdInt int userId) { final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); if (settings.getMethodMap().get(id) != null @@ -4725,7 +4733,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } final long ident = Binder.clearCallingIdentity(); try { - setInputMethodLocked(id, subtypeId, userId); + setInputMethodLocked(id, subtypeIndex, userId); } finally { Binder.restoreCallingIdentity(ident); } @@ -4886,7 +4894,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. final boolean isScreenLocked = mWindowManagerInternal.isKeyguardLocked() && mWindowManagerInternal.isKeyguardSecure(userId); final String lastInputMethodId = settings.getSelectedInputMethod(); - int lastInputMethodSubtypeId = settings.getSelectedInputMethodSubtypeId(lastInputMethodId); + final int lastInputMethodSubtypeIndex = + settings.getSelectedInputMethodSubtypeIndex(lastInputMethodId); final List<ImeSubtypeListItem> imList = InputMethodSubtypeSwitchingController .getSortedInputMethodAndSubtypeList( @@ -4900,30 +4909,30 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. return; } - if (mNewInputMethodSwitcherMenuEnabled) { + if (Flags.imeSwitcherRevamp()) { if (DEBUG) { Slog.v(TAG, "Show IME switcher menu," + " showAuxSubtypes=" + showAuxSubtypes + " displayId=" + displayId + " preferredInputMethodId=" + lastInputMethodId - + " preferredInputMethodSubtypeId=" + lastInputMethodSubtypeId); + + " preferredInputMethodSubtypeIndex=" + lastInputMethodSubtypeIndex); } final var itemsAndIndex = getInputMethodPickerItems(imList, - lastInputMethodId, lastInputMethodSubtypeId, userId); + lastInputMethodId, lastInputMethodSubtypeIndex, userId); final var menuItems = itemsAndIndex.first; final int selectedIndex = itemsAndIndex.second; if (selectedIndex == -1) { Slog.w(TAG, "Switching menu shown with no item selected" + ", IME id: " + lastInputMethodId - + ", subtype index: " + lastInputMethodSubtypeId); + + ", subtype index: " + lastInputMethodSubtypeIndex); } mMenuControllerNew.show(menuItems, selectedIndex, displayId, userId); } else { mMenuController.showInputMethodMenuLocked(showAuxSubtypes, displayId, - lastInputMethodId, lastInputMethodSubtypeId, imList, userId); + lastInputMethodId, lastInputMethodSubtypeIndex, imList, userId); } } @@ -4952,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 @@ -4990,7 +4999,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. // -------------------------------------------------------------- case MSG_HARD_KEYBOARD_SWITCH_CHANGED: - if (!mNewInputMethodSwitcherMenuEnabled) { + if (!Flags.imeSwitcherRevamp()) { mMenuController.handleHardKeyboardStatusChange(msg.arg1 == 1); } synchronized (ImfLock.class) { @@ -5018,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()) { @@ -5102,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; @@ -5442,7 +5452,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } @GuardedBy("ImfLock.class") - private void setSelectedInputMethodAndSubtypeLocked(InputMethodInfo imi, int subtypeId, + private void setSelectedInputMethodAndSubtypeLocked(InputMethodInfo imi, int subtypeIndex, boolean setSubtypeOnly, @UserIdInt int userId) { final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); final var bindingController = getInputMethodBindingController(userId); @@ -5452,12 +5462,12 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. // Set Subtype here final int newSubtypeHashcode; final InputMethodSubtype newSubtype; - if (imi == null || subtypeId < 0) { + if (imi == null || subtypeIndex < 0) { newSubtypeHashcode = INVALID_SUBTYPE_HASHCODE; newSubtype = null; } else { - if (subtypeId < imi.getSubtypeCount()) { - InputMethodSubtype subtype = imi.getSubtypeAt(subtypeId); + if (subtypeIndex < imi.getSubtypeCount()) { + InputMethodSubtype subtype = imi.getSubtypeAt(subtypeIndex); newSubtypeHashcode = subtype.hashCode(); newSubtype = subtype; } else { @@ -5493,20 +5503,20 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. settings.putSelectedDefaultDeviceInputMethod(null); InputMethodInfo imi = settings.getMethodMap().get(newDefaultIme); - int lastSubtypeId = NOT_A_SUBTYPE_ID; + int lastSubtypeIndex = NOT_A_SUBTYPE_INDEX; // newDefaultIme is empty when there is no candidate for the selected IME. if (imi != null && !TextUtils.isEmpty(newDefaultIme)) { String subtypeHashCode = settings.getLastSubtypeForInputMethod(newDefaultIme); if (subtypeHashCode != null) { try { - lastSubtypeId = SubtypeUtils.getSubtypeIdFromHashCode(imi, + lastSubtypeIndex = SubtypeUtils.getSubtypeIndexFromHashCode(imi, Integer.parseInt(subtypeHashCode)); } catch (NumberFormatException e) { Slog.w(TAG, "HashCode for subtype looks broken: " + subtypeHashCode, e); } } } - setSelectedInputMethodAndSubtypeLocked(imi, lastSubtypeId, false, userId); + setSelectedInputMethodAndSubtypeLocked(imi, lastSubtypeIndex, false, userId); } /** @@ -5530,14 +5540,14 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } @GuardedBy("ImfLock.class") - private boolean switchToInputMethodLocked(@NonNull String imeId, int subtypeId, + private boolean switchToInputMethodLocked(@NonNull String imeId, int subtypeIndex, @UserIdInt int userId) { final var settings = InputMethodSettingsRepository.get(userId); final var enabledImes = settings.getEnabledInputMethodList(); if (!CollectionUtils.any(enabledImes, imi -> imi.getId().equals(imeId))) { return false; // IME is not found or not enabled. } - setInputMethodLocked(imeId, subtypeId, userId); + setInputMethodLocked(imeId, subtypeIndex, userId); return true; } @@ -5593,8 +5603,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. return; } - final var nextSubtype = nextItem.mSubtypeId > NOT_A_SUBTYPE_ID - ? nextItem.mImi.getSubtypeAt(nextItem.mSubtypeId) : null; + final var nextSubtype = nextItem.mSubtypeIndex > NOT_A_SUBTYPE_INDEX + ? nextItem.mImi.getSubtypeAt(nextItem.mSubtypeIndex) : null; nextSubtypeHandle = InputMethodSubtypeHandle.of(nextItem.mImi, nextSubtype); } else { final InputMethodSubtypeHandle currentSubtypeHandle = @@ -5613,7 +5623,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. final int subtypeCount = nextImi.getSubtypeCount(); if (subtypeCount == 0) { if (nextSubtypeHandle.equals(InputMethodSubtypeHandle.of(nextImi, null))) { - setInputMethodLocked(nextImi.getId(), NOT_A_SUBTYPE_ID, userId); + setInputMethodLocked(nextImi.getId(), NOT_A_SUBTYPE_INDEX, userId); } return; } @@ -5691,10 +5701,10 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } @Override - public boolean switchToInputMethod(@NonNull String imeId, int subtypeId, + public boolean switchToInputMethod(@NonNull String imeId, int subtypeIndex, @UserIdInt int userId) { synchronized (ImfLock.class) { - return switchToInputMethodLocked(imeId, subtypeId, userId); + return switchToInputMethodLocked(imeId, subtypeIndex, userId); } } @@ -5776,7 +5786,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. final var visibilityStateComputer = userData.mVisibilityStateComputer; if (visibilityStateComputer.getLastImeTargetWindow() != userData.mImeBindingState.mFocusedWindow) { - if (mNewInputMethodSwitcherMenuEnabled) { + if (Flags.imeSwitcherRevamp()) { final var bindingController = getInputMethodBindingController(userId); mMenuControllerNew.hide(bindingController.getCurTokenDisplayId(), userId); } else { @@ -6061,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:"); @@ -6093,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=" @@ -6111,6 +6121,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @SuppressWarnings("GuardedBy") Consumer<UserData> userDataDump = u -> { p.println(" mUserId=" + u.mUserId); + p.println(" unlocked=" + u.mIsUnlockingOrUnlocked.get()); p.println(" hasMainConnection=" + u.mBindingController.hasMainConnection()); p.println(" isVisibleBound=" + u.mBindingController.isVisibleBound()); @@ -6134,7 +6145,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. }; mUserDataRepository.forAllUserData(userDataDump); - if (mNewInputMethodSwitcherMenuEnabled) { + if (Flags.imeSwitcherRevamp()) { p.println(" menuControllerNew:"); mMenuControllerNew.dump(p, " "); } else { @@ -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,13 +6568,13 @@ 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; } boolean failedToSelectUnknownIme = !switchToInputMethodLocked(imeId, - NOT_A_SUBTYPE_ID, userId); + NOT_A_SUBTYPE_INDEX, userId); if (failedToSelectUnknownIme) { error.print("Unknown input method "); error.print(imeId); @@ -6579,14 +6590,25 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. out.println(userId); // Workaround for b/354782333. - final var settingsValue = SecureSettingsWrapper.getString( - Settings.Secure.DEFAULT_INPUT_METHOD, "", userId); + 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."); - SecureSettingsWrapper.putString( - Settings.Secure.DEFAULT_INPUT_METHOD, imeId, userId); + if (deviceId == DEVICE_ID_DEFAULT) { + settings.putSelectedInputMethod(imeId); + } else { + settings.putSelectedDefaultDeviceInputMethod(imeId); + } } } hasFailed |= failedToSelectUnknownIme; @@ -6609,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/inputmethod/InputMethodMenuController.java b/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java index f16a5a077d8b..b5ee06863f2b 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java @@ -17,7 +17,7 @@ package com.android.server.inputmethod; import static com.android.server.inputmethod.InputMethodManagerService.DEBUG; -import static com.android.server.inputmethod.InputMethodUtils.NOT_A_SUBTYPE_ID; +import static com.android.server.inputmethod.InputMethodUtils.NOT_A_SUBTYPE_INDEX; import android.annotation.NonNull; import android.annotation.Nullable; @@ -62,7 +62,7 @@ final class InputMethodMenuController { private View mSwitchingDialogTitleView; private List<ImeSubtypeListItem> mImList; private InputMethodInfo[] mIms; - private int[] mSubtypeIds; + private int[] mSubtypeIndices; private boolean mShowImeWithHardKeyboard; @@ -77,7 +77,7 @@ final class InputMethodMenuController { @GuardedBy("ImfLock.class") void showInputMethodMenuLocked(boolean showAuxSubtypes, int displayId, - String preferredInputMethodId, int preferredInputMethodSubtypeId, + String preferredInputMethodId, int preferredInputMethodSubtypeIndex, @NonNull List<ImeSubtypeListItem> imList, @UserIdInt int userId) { if (DEBUG) Slog.v(TAG, "Show switching menu. showAuxSubtypes=" + showAuxSubtypes); @@ -85,14 +85,14 @@ final class InputMethodMenuController { hideInputMethodMenuLocked(userId); - if (preferredInputMethodSubtypeId == NOT_A_SUBTYPE_ID) { + if (preferredInputMethodSubtypeIndex == NOT_A_SUBTYPE_INDEX) { final InputMethodSubtype currentSubtype = bindingController.getCurrentInputMethodSubtype(); if (currentSubtype != null) { final String curMethodId = bindingController.getSelectedMethodId(); final InputMethodInfo currentImi = InputMethodSettingsRepository.get(userId).getMethodMap().get(curMethodId); - preferredInputMethodSubtypeId = SubtypeUtils.getSubtypeIdFromHashCode( + preferredInputMethodSubtypeIndex = SubtypeUtils.getSubtypeIndexFromHashCode( currentImi, currentSubtype.hashCode()); } } @@ -101,7 +101,7 @@ final class InputMethodMenuController { final int size = imList.size(); mImList = imList; mIms = new InputMethodInfo[size]; - mSubtypeIds = new int[size]; + mSubtypeIndices = new int[size]; // No items are checked by default. When we have a list of explicitly enabled subtypes, // the implicit subtype is no longer listed, but if it is still the selected one, // no items will be shown as checked. @@ -109,12 +109,13 @@ final class InputMethodMenuController { for (int i = 0; i < size; ++i) { final ImeSubtypeListItem item = imList.get(i); mIms[i] = item.mImi; - mSubtypeIds[i] = item.mSubtypeId; + mSubtypeIndices[i] = item.mSubtypeIndex; if (mIms[i].getId().equals(preferredInputMethodId)) { - int subtypeId = mSubtypeIds[i]; - if ((subtypeId == NOT_A_SUBTYPE_ID) - || (preferredInputMethodSubtypeId == NOT_A_SUBTYPE_ID && subtypeId == 0) - || (subtypeId == preferredInputMethodSubtypeId)) { + int subtypeIndex = mSubtypeIndices[i]; + if ((subtypeIndex == NOT_A_SUBTYPE_INDEX) + || (preferredInputMethodSubtypeIndex == NOT_A_SUBTYPE_INDEX + && subtypeIndex == 0) + || (subtypeIndex == preferredInputMethodSubtypeIndex)) { checkedItem = i; } } @@ -123,7 +124,7 @@ final class InputMethodMenuController { if (checkedItem == -1) { Slog.w(TAG, "Switching menu shown with no item selected" + ", IME id: " + preferredInputMethodId - + ", subtype index: " + preferredInputMethodSubtypeId); + + ", subtype index: " + preferredInputMethodSubtypeIndex); } if (mDialogWindowContext == null) { @@ -171,19 +172,19 @@ final class InputMethodMenuController { com.android.internal.R.layout.input_method_switch_item, imList, checkedItem); final DialogInterface.OnClickListener choiceListener = (dialog, which) -> { synchronized (ImfLock.class) { - if (mIms == null || mIms.length <= which || mSubtypeIds == null - || mSubtypeIds.length <= which) { + if (mIms == null || mIms.length <= which || mSubtypeIndices == null + || mSubtypeIndices.length <= which) { return; } final InputMethodInfo im = mIms[which]; - int subtypeId = mSubtypeIds[which]; + int subtypeIndex = mSubtypeIndices[which]; adapter.mCheckedItem = which; adapter.notifyDataSetChanged(); if (im != null) { - if (subtypeId < 0 || subtypeId >= im.getSubtypeCount()) { - subtypeId = NOT_A_SUBTYPE_ID; + if (subtypeIndex < 0 || subtypeIndex >= im.getSubtypeCount()) { + subtypeIndex = NOT_A_SUBTYPE_INDEX; } - mService.setInputMethodLocked(im.getId(), subtypeId, userId); + mService.setInputMethodLocked(im.getId(), subtypeIndex, userId); } hideInputMethodMenuLocked(userId); } @@ -251,7 +252,7 @@ final class InputMethodMenuController { mDialogBuilder = null; mImList = null; mIms = null; - mSubtypeIds = null; + mSubtypeIndices = null; } } diff --git a/services/core/java/com/android/server/inputmethod/InputMethodMenuControllerNew.java b/services/core/java/com/android/server/inputmethod/InputMethodMenuControllerNew.java index b72a34ddae5b..d9e9e0021028 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodMenuControllerNew.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodMenuControllerNew.java @@ -21,7 +21,7 @@ import static android.Manifest.permission.HIDE_OVERLAY_WINDOWS; import static android.Manifest.permission.INTERACT_ACROSS_USERS; import static com.android.server.inputmethod.InputMethodManagerService.DEBUG; -import static com.android.server.inputmethod.InputMethodUtils.NOT_A_SUBTYPE_ID; +import static com.android.server.inputmethod.InputMethodUtils.NOT_A_SUBTYPE_INDEX; import android.annotation.IntRange; import android.annotation.NonNull; @@ -107,7 +107,7 @@ final class InputMethodMenuControllerNew { if (which != selectedIndex) { final var item = items.get(which); InputMethodManagerInternal.get() - .switchToInputMethod(item.mImi.getId(), item.mSubtypeId, userId); + .switchToInputMethod(item.mImi.getId(), item.mSubtypeIndex, userId); } hide(displayId, userId); }; @@ -225,10 +225,10 @@ final class InputMethodMenuControllerNew { /** * The index of the subtype in the input method's array of subtypes, - * or {@link InputMethodUtils#NOT_A_SUBTYPE_ID} if this item doesn't have a subtype. + * or {@link InputMethodUtils#NOT_A_SUBTYPE_INDEX} if this item doesn't have a subtype. */ - @IntRange(from = NOT_A_SUBTYPE_ID) - private final int mSubtypeId; + @IntRange(from = NOT_A_SUBTYPE_INDEX) + private final int mSubtypeIndex; /** Whether this item has a group header (only the first item of each input method). */ private final boolean mHasHeader; @@ -240,12 +240,13 @@ final class InputMethodMenuControllerNew { private final boolean mHasDivider; MenuItem(@NonNull CharSequence imeName, @Nullable CharSequence subtypeName, - @NonNull InputMethodInfo imi, @IntRange(from = NOT_A_SUBTYPE_ID) int subtypeId, - boolean hasHeader, boolean hasDivider) { + @NonNull InputMethodInfo imi, + @IntRange(from = NOT_A_SUBTYPE_INDEX) int subtypeIndex, boolean hasHeader, + boolean hasDivider) { mImeName = imeName; mSubtypeName = subtypeName; mImi = imi; - mSubtypeId = subtypeId; + mSubtypeIndex = subtypeIndex; mHasHeader = hasHeader; mHasDivider = hasDivider; } @@ -255,7 +256,7 @@ final class InputMethodMenuControllerNew { return "MenuItem{" + "mImeName=" + mImeName + " mSubtypeName=" + mSubtypeName - + " mSubtypeId=" + mSubtypeId + + " mSubtypeIndex=" + mSubtypeIndex + " mHasHeader=" + mHasHeader + " mHasDivider=" + mHasDivider + "}"; diff --git a/services/core/java/com/android/server/inputmethod/InputMethodSettings.java b/services/core/java/com/android/server/inputmethod/InputMethodSettings.java index 0152158cbb7a..030a5fbc13c2 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodSettings.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodSettings.java @@ -54,7 +54,7 @@ final class InputMethodSettings { /** * An integer code that represents "no subtype" when a subtype hashcode is used. * - * <p>Due to historical confusions with {@link InputMethodUtils#NOT_A_SUBTYPE_ID}, we have + * <p>Due to historical confusions with {@link InputMethodUtils#NOT_A_SUBTYPE_INDEX}, we have * used {@code -1} here. We cannot change this value as it's already saved into secure settings. * </p> */ @@ -62,7 +62,7 @@ final class InputMethodSettings { /** * A string code that represents "no subtype" when a subtype hashcode is used. * - * <p>Due to historical confusions with {@link InputMethodUtils#NOT_A_SUBTYPE_ID}, we have + * <p>Due to historical confusions with {@link InputMethodUtils#NOT_A_SUBTYPE_INDEX}, we have * used {@code "-1"} here. We cannot change this value as it's already saved into secure * settings.</p> */ @@ -84,8 +84,8 @@ final class InputMethodSettings { // Inputmethod and subtypes are saved in the settings as follows: // ime0;subtype0;subtype1:ime1;subtype0:ime2:ime3;subtype0;subtype1 for (int i = 0; i < ime.second.size(); ++i) { - final String subtypeId = ime.second.get(i); - builder.append(INPUT_METHOD_SUBTYPE_SEPARATOR).append(subtypeId); + final String subtypeHashCode = ime.second.get(i); + builder.append(INPUT_METHOD_SUBTYPE_SEPARATOR).append(subtypeHashCode); } } @@ -350,12 +350,12 @@ final class InputMethodSettings { if (lastImi == null) return null; try { final int lastSubtypeHash = Integer.parseInt(lastIme.second); - final int lastSubtypeId = SubtypeUtils.getSubtypeIdFromHashCode(lastImi, + final int lastSubtypeIndex = SubtypeUtils.getSubtypeIndexFromHashCode(lastImi, lastSubtypeHash); - if (lastSubtypeId < 0 || lastSubtypeId >= lastImi.getSubtypeCount()) { + if (lastSubtypeIndex < 0 || lastSubtypeIndex >= lastImi.getSubtypeCount()) { return null; } - return lastImi.getSubtypeAt(lastSubtypeId); + return lastImi.getSubtypeAt(lastSubtypeIndex); } catch (NumberFormatException e) { return null; } @@ -427,7 +427,7 @@ final class InputMethodSettings { for (int j = 0; j < explicitlyEnabledSubtypes.size(); ++j) { final String s = explicitlyEnabledSubtypes.get(j); if (s.equals(subtypeHashCode)) { - // If both imeId and subtype are enabled, return subtypeId. + // If both imeId and subtype are enabled, return subtypeHashCode. try { final int hashCode = Integer.parseInt(subtypeHashCode); // Check whether the subtype is valid or not @@ -494,11 +494,11 @@ final class InputMethodSettings { putString(Settings.Secure.DEFAULT_INPUT_METHOD, imeId); } - void putSelectedSubtype(int subtypeId) { + void putSelectedSubtype(int subtypeHashCode) { if (DEBUG) { - Slog.d(TAG, "putSelectedInputMethodSubtypeStr: " + subtypeId + ", " + mUserId); + Slog.d(TAG, "putSelectedInputMethodSubtypeStr: " + subtypeHashCode + ", " + mUserId); } - putInt(Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, subtypeId); + putInt(Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, subtypeHashCode); } @Nullable @@ -551,13 +551,13 @@ final class InputMethodSettings { return mUserId; } - int getSelectedInputMethodSubtypeId(String selectedImiId) { + int getSelectedInputMethodSubtypeIndex(String selectedImiId) { final InputMethodInfo imi = mMethodMap.get(selectedImiId); if (imi == null) { - return InputMethodUtils.NOT_A_SUBTYPE_ID; + return InputMethodUtils.NOT_A_SUBTYPE_INDEX; } final int subtypeHashCode = getSelectedInputMethodSubtypeHashCode(); - return SubtypeUtils.getSubtypeIdFromHashCode(imi, subtypeHashCode); + return SubtypeUtils.getSubtypeIndexFromHashCode(imi, subtypeHashCode); } void saveCurrentInputMethodAndSubtypeToHistory(String curMethodId, diff --git a/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java b/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java index 05cc5985a8cc..c77b76864176 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java @@ -16,6 +16,8 @@ package com.android.server.inputmethod; +import static com.android.server.inputmethod.InputMethodUtils.NOT_A_SUBTYPE_INDEX; + import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; @@ -48,7 +50,6 @@ import java.util.Objects; final class InputMethodSubtypeSwitchingController { private static final String TAG = InputMethodSubtypeSwitchingController.class.getSimpleName(); private static final boolean DEBUG = false; - private static final int NOT_A_SUBTYPE_ID = InputMethodUtils.NOT_A_SUBTYPE_ID; @IntDef(prefix = {"MODE_"}, value = { MODE_STATIC, @@ -86,17 +87,21 @@ final class InputMethodSubtypeSwitchingController { public final CharSequence mSubtypeName; @NonNull public final InputMethodInfo mImi; - public final int mSubtypeId; + /** + * The index of the subtype in the input method's array of subtypes, + * or {@link InputMethodUtils#NOT_A_SUBTYPE_INDEX} if this item doesn't have a subtype. + */ + public final int mSubtypeIndex; public final boolean mIsSystemLocale; public final boolean mIsSystemLanguage; ImeSubtypeListItem(@NonNull CharSequence imeName, @Nullable CharSequence subtypeName, - @NonNull InputMethodInfo imi, int subtypeId, @Nullable String subtypeLocale, + @NonNull InputMethodInfo imi, int subtypeIndex, @Nullable String subtypeLocale, @NonNull String systemLocale) { mImeName = imeName; mSubtypeName = subtypeName; mImi = imi; - mSubtypeId = subtypeId; + mSubtypeIndex = subtypeIndex; if (TextUtils.isEmpty(subtypeLocale)) { mIsSystemLocale = false; mIsSystemLanguage = false; @@ -137,7 +142,7 @@ final class InputMethodSubtypeSwitchingController { * <li>{@link #mImi} with {@link InputMethodInfo#getId()}</li> * </ol> * Note: this class has a natural ordering that is inconsistent with - * {@link #equals(Object)}. This method doesn't compare {@link #mSubtypeId} but + * {@link #equals(Object)}. This method doesn't compare {@link #mSubtypeIndex} but * {@link #equals(Object)} does. * * @param other the object to be compared. @@ -177,7 +182,7 @@ final class InputMethodSubtypeSwitchingController { return "ImeSubtypeListItem{" + "mImeName=" + mImeName + " mSubtypeName=" + mSubtypeName - + " mSubtypeId=" + mSubtypeId + + " mSubtypeIndex=" + mSubtypeIndex + " mIsSystemLocale=" + mIsSystemLocale + " mIsSystemLanguage=" + mIsSystemLanguage + "}"; @@ -190,7 +195,8 @@ final class InputMethodSubtypeSwitchingController { } if (o instanceof ImeSubtypeListItem) { final ImeSubtypeListItem that = (ImeSubtypeListItem) o; - return Objects.equals(this.mImi, that.mImi) && this.mSubtypeId == that.mSubtypeId; + return Objects.equals(this.mImi, that.mImi) + && this.mSubtypeIndex == that.mSubtypeIndex; } return false; } @@ -256,7 +262,7 @@ final class InputMethodSubtypeSwitchingController { } } } else { - imList.add(new ImeSubtypeListItem(imeLabel, null, imi, NOT_A_SUBTYPE_ID, null, + imList.add(new ImeSubtypeListItem(imeLabel, null, imi, NOT_A_SUBTYPE_INDEX, null, mSystemLocaleStr)); } } @@ -310,17 +316,17 @@ final class InputMethodSubtypeSwitchingController { } } } else { - imList.add(new ImeSubtypeListItem(imeLabel, null, imi, NOT_A_SUBTYPE_ID, null, + imList.add(new ImeSubtypeListItem(imeLabel, null, imi, NOT_A_SUBTYPE_INDEX, null, mSystemLocaleStr)); } } return imList; } - private static int calculateSubtypeId(@NonNull InputMethodInfo imi, + private static int calculateSubtypeIndex(@NonNull InputMethodInfo imi, @Nullable InputMethodSubtype subtype) { - return subtype != null ? SubtypeUtils.getSubtypeIdFromHashCode(imi, subtype.hashCode()) - : NOT_A_SUBTYPE_ID; + return subtype != null ? SubtypeUtils.getSubtypeIndexFromHashCode(imi, subtype.hashCode()) + : NOT_A_SUBTYPE_INDEX; } private static class StaticRotationList { @@ -341,12 +347,12 @@ final class InputMethodSubtypeSwitchingController { * @return The index in the given list. -1 if not found. */ private int getIndex(@NonNull InputMethodInfo imi, @Nullable InputMethodSubtype subtype) { - final int currentSubtypeId = calculateSubtypeId(imi, subtype); + final int currentSubtypeIndex = calculateSubtypeIndex(imi, subtype); final int numSubtypes = mImeSubtypeList.size(); for (int i = 0; i < numSubtypes; ++i) { final ImeSubtypeListItem item = mImeSubtypeList.get(i); // Skip until the current IME/subtype is found. - if (imi.equals(item.mImi) && item.mSubtypeId == currentSubtypeId) { + if (imi.equals(item.mImi) && item.mSubtypeIndex == currentSubtypeIndex) { return i; } } @@ -414,14 +420,14 @@ final class InputMethodSubtypeSwitchingController { */ private int getUsageRank(@NonNull InputMethodInfo imi, @Nullable InputMethodSubtype subtype) { - final int currentSubtypeId = calculateSubtypeId(imi, subtype); + final int currentSubtypeIndex = calculateSubtypeIndex(imi, subtype); final int numItems = mUsageHistoryOfSubtypeListItemIndex.length; for (int usageRank = 0; usageRank < numItems; usageRank++) { final int subtypeListItemIndex = mUsageHistoryOfSubtypeListItemIndex[usageRank]; final ImeSubtypeListItem subtypeListItem = mImeSubtypeList.get(subtypeListItemIndex); if (subtypeListItem.mImi.equals(imi) - && subtypeListItem.mSubtypeId == currentSubtypeId) { + && subtypeListItem.mSubtypeIndex == currentSubtypeIndex) { return usageRank; } } @@ -506,6 +512,9 @@ final class InputMethodSubtypeSwitchingController { /** * Gets the next input method and subtype from the given ones. * + * <p>If the given input method and subtype are not found, this returns the most recent + * input method and subtype.</p> + * * @param imi the input method to find the next value from. * @param subtype the input method subtype to find the next value from, if any. * @param onlyCurrentIme whether to consider only subtypes of the current input method. @@ -517,17 +526,20 @@ final class InputMethodSubtypeSwitchingController { public ImeSubtypeListItem next(@NonNull InputMethodInfo imi, @Nullable InputMethodSubtype subtype, boolean onlyCurrentIme, boolean useRecency, boolean forward) { - final int size = mItems.size(); - if (size <= 1) { + if (mItems.isEmpty()) { return null; } final int index = getIndex(imi, subtype, useRecency); if (index < 0) { - return null; + Slog.w(TAG, "Trying to switch away from input method: " + imi + + " and subtype " + subtype + " which are not in the list," + + " falling back to most recent item in list."); + return mItems.get(mRecencyMap[0]); } final int incrementSign = (forward ? 1 : -1); + final int size = mItems.size(); for (int i = 1; i < size; i++) { final int nextIndex = (index + i * incrementSign + size) % size; final int mappedIndex = useRecency ? mRecencyMap[nextIndex] : nextIndex; @@ -548,7 +560,7 @@ final class InputMethodSubtypeSwitchingController { */ public boolean setMostRecent(@NonNull InputMethodInfo imi, @Nullable InputMethodSubtype subtype) { - if (mItems.size() <= 1) { + if (mItems.isEmpty()) { return false; } @@ -575,11 +587,11 @@ final class InputMethodSubtypeSwitchingController { @IntRange(from = -1) private int getIndex(@NonNull InputMethodInfo imi, @Nullable InputMethodSubtype subtype, boolean useRecency) { - final int subtypeIndex = calculateSubtypeId(imi, subtype); + final int subtypeIndex = calculateSubtypeIndex(imi, subtype); for (int i = 0; i < mItems.size(); i++) { final int mappedIndex = useRecency ? mRecencyMap[i] : i; final var item = mItems.get(mappedIndex); - if (item.mImi.equals(imi) && item.mSubtypeId == subtypeIndex) { + if (item.mImi.equals(imi) && item.mSubtypeIndex == subtypeIndex) { return i; } } @@ -591,13 +603,13 @@ final class InputMethodSubtypeSwitchingController { pw.println(prefix + "Static order:"); for (int i = 0; i < mItems.size(); ++i) { final var item = mItems.get(i); - pw.println(prefix + "i=" + i + " item=" + item); + pw.println(prefix + " i=" + i + " item=" + item); } pw.println(prefix + "Recency order:"); for (int i = 0; i < mRecencyMap.length; ++i) { final int index = mRecencyMap[i]; final var item = mItems.get(index); - pw.println(prefix + "i=" + i + " item=" + item); + pw.println(prefix + " i=" + i + " item=" + item); } } } @@ -800,7 +812,7 @@ final class InputMethodSubtypeSwitchingController { pw.println(prefix + "mHardwareRotationList:"); mHardwareRotationList.dump(pw, prefix + " "); } - pw.println("User action since last switch: " + mUserActionSinceSwitch); + pw.println(prefix + "User action since last switch: " + mUserActionSinceSwitch); } } } @@ -843,6 +855,9 @@ final class InputMethodSubtypeSwitchingController { /** * Gets the next input method and subtype, starting from the given ones, in the given direction. * + * <p>If the given input method and subtype are not found, this returns the most recent + * input method and subtype.</p> + * * @param onlyCurrentIme whether to consider only subtypes of the current input method. * @param imi the input method to find the next value from. * @param subtype the input method subtype to find the next value from, if any. @@ -861,6 +876,9 @@ final class InputMethodSubtypeSwitchingController { * Gets the next input method and subtype suitable for hardware keyboards, starting from the * given ones, in the given direction. * + * <p>If the given input method and subtype are not found, this returns the most recent + * input method and subtype.</p> + * * @param onlyCurrentIme whether to consider only subtypes of the current input method. * @param imi the input method to find the next value from. * @param subtype the input method subtype to find the next value from, if any. diff --git a/services/core/java/com/android/server/inputmethod/InputMethodUtils.java b/services/core/java/com/android/server/inputmethod/InputMethodUtils.java index 361cdbbc15bf..da35fe7c7e50 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodUtils.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodUtils.java @@ -59,7 +59,7 @@ import java.util.function.Consumer; */ final class InputMethodUtils { public static final boolean DEBUG = false; - static final int NOT_A_SUBTYPE_ID = -1; + static final int NOT_A_SUBTYPE_INDEX = -1; private static final String TAG = "InputMethodUtils"; // The string for enabled input method is saved as follows: diff --git a/services/core/java/com/android/server/inputmethod/SubtypeUtils.java b/services/core/java/com/android/server/inputmethod/SubtypeUtils.java index 1b4c0d6ef4d5..f615b52b9015 100644 --- a/services/core/java/com/android/server/inputmethod/SubtypeUtils.java +++ b/services/core/java/com/android/server/inputmethod/SubtypeUtils.java @@ -16,6 +16,9 @@ package com.android.server.inputmethod; +import static com.android.server.inputmethod.InputMethodSettings.INVALID_SUBTYPE_HASHCODE; +import static com.android.server.inputmethod.InputMethodUtils.NOT_A_SUBTYPE_INDEX; + import android.annotation.AnyThread; import android.annotation.NonNull; import android.annotation.Nullable; @@ -48,7 +51,6 @@ final class SubtypeUtils { static final String SUBTYPE_MODE_ANY = null; static final String SUBTYPE_MODE_KEYBOARD = "keyboard"; - static final int NOT_A_SUBTYPE_ID = -1; private static final String TAG_ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE = "EnabledWhenDefaultIsNotAsciiCapable"; @@ -103,10 +105,19 @@ final class SubtypeUtils { } static boolean isValidSubtypeHashCode(InputMethodInfo imi, int subtypeHashCode) { - return getSubtypeIdFromHashCode(imi, subtypeHashCode) != NOT_A_SUBTYPE_ID; + return getSubtypeIndexFromHashCode(imi, subtypeHashCode) != NOT_A_SUBTYPE_INDEX; } - static int getSubtypeIdFromHashCode(InputMethodInfo imi, int subtypeHashCode) { + /** + * Returns the index to be specified to {@link InputMethodInfo#getSubtypeAt(int)}. + * + * @param imi {@link InputMethodInfo} to be queried about + * @param subtypeHashCode {@link InputMethodSubtype#hashCode()} to be queried about + * + * @return The index to be specified to {@link InputMethodInfo#getSubtypeAt(int)}. + * {@link InputMethodUtils#NOT_A_SUBTYPE_INDEX} if not found + */ + static int getSubtypeIndexFromHashCode(InputMethodInfo imi, int subtypeHashCode) { if (imi != null) { final int subtypeCount = imi.getSubtypeCount(); for (int i = 0; i < subtypeCount; ++i) { @@ -116,7 +127,7 @@ final class SubtypeUtils { } } } - return NOT_A_SUBTYPE_ID; + return NOT_A_SUBTYPE_INDEX; } private static final LocaleUtils.LocaleExtractor<InputMethodSubtype> sSubtypeToLocale = @@ -242,7 +253,7 @@ final class SubtypeUtils { * most applicable subtype, it will return the first subtype * matched with mode * - * @return the most applicable subtypeId + * @return the most applicable {@link InputMethodSubtype} */ static InputMethodSubtype findLastResortApplicableSubtype( List<InputMethodSubtype> subtypes, String mode, @NonNull String locale, @@ -310,15 +321,15 @@ final class SubtypeUtils { @Nullable InputMethodSubtype currentSubtype) { final int userId = settings.getUserId(); final int selectedSubtypeHashCode = SecureSettingsWrapper.getInt( - Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, NOT_A_SUBTYPE_ID, userId); - if (selectedSubtypeHashCode != NOT_A_SUBTYPE_ID && currentSubtype != null + Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, INVALID_SUBTYPE_HASHCODE, userId); + if (selectedSubtypeHashCode != INVALID_SUBTYPE_HASHCODE && currentSubtype != null && isValidSubtypeHashCode(imi, currentSubtype.hashCode())) { return currentSubtype; } - final int subtypeId = settings.getSelectedInputMethodSubtypeId(imi.getId()); - if (subtypeId != NOT_A_SUBTYPE_ID) { - return imi.getSubtypeAt(subtypeId); + final int subtypeIndex = settings.getSelectedInputMethodSubtypeIndex(imi.getId()); + if (subtypeIndex != NOT_A_SUBTYPE_INDEX) { + return imi.getSubtypeAt(subtypeIndex); } // If there are no selected subtypes, the framework will try to find the most applicable diff --git a/services/core/java/com/android/server/inputmethod/UserData.java b/services/core/java/com/android/server/inputmethod/UserData.java index 28394c6a6272..96da17e434e1 100644 --- a/services/core/java/com/android/server/inputmethod/UserData.java +++ b/services/core/java/com/android/server/inputmethod/UserData.java @@ -19,14 +19,17 @@ package com.android.server.inputmethod; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; +import android.util.Pair; import android.util.SparseArray; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.ImeTracker; +import android.view.inputmethod.InputMethodSubtype; import android.window.ImeOnBackInvokedDispatcher; import com.android.internal.annotations.GuardedBy; import com.android.internal.inputmethod.IRemoteAccessibilityInputConnection; import com.android.internal.inputmethod.IRemoteInputConnection; +import com.android.internal.inputmethod.InputMethodSubtypeHandle; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicBoolean; @@ -149,11 +152,30 @@ final class UserData { String mLastEnabledInputMethodsStr = ""; /** + * A temporary solution to Bug 356879517, where we need to emulate the previous single-user mode + * behavior for KeyboardLayoutManager. + * + * <p>TODO(b/357663774): Remove this workaround</p> + */ + @GuardedBy("ImfLock.class") + @Nullable + Pair<InputMethodSubtypeHandle, InputMethodSubtype> mSubtypeForKeyboardLayoutMapping; + + /** * {@code true} when the IME is responsible for drawing the navigation bar and its buttons. */ @NonNull final AtomicBoolean mImeDrawsNavBar = new AtomicBoolean(); + + /** + * {@code true} if the user storage is considered to be unlocked. + * + * @see com.android.server.pm.UserManagerInternal#isUserUnlockingOrUnlocked(int) + */ + @NonNull + final AtomicBoolean mIsUnlockingOrUnlocked = new AtomicBoolean(false); + /** * Intended to be instantiated only from this file. */ diff --git a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java index 53b67969e91a..a6f4c0e597d1 100644 --- a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java +++ b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java @@ -530,6 +530,12 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { */ private boolean mUseDifferentDelaysForBackgroundChain; + /** + * Core uids and apps without the internet permission will not have any firewall rules applied + * to them. + */ + private boolean mNeverApplyRulesToCoreUids; + // See main javadoc for instructions on how to use these locks. final Object mUidRulesFirstLock = new Object(); final Object mNetworkPoliciesSecondLock = new Object(); @@ -622,16 +628,6 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { @GuardedBy("mUidRulesFirstLock") final SparseIntArray mUidFirewallStandbyRules = new SparseIntArray(); - @GuardedBy("mUidRulesFirstLock") - final SparseIntArray mUidFirewallDozableRules = new SparseIntArray(); - @GuardedBy("mUidRulesFirstLock") - final SparseIntArray mUidFirewallPowerSaveRules = new SparseIntArray(); - @GuardedBy("mUidRulesFirstLock") - final SparseIntArray mUidFirewallBackgroundRules = new SparseIntArray(); - @GuardedBy("mUidRulesFirstLock") - final SparseIntArray mUidFirewallRestrictedModeRules = new SparseIntArray(); - @GuardedBy("mUidRulesFirstLock") - final SparseIntArray mUidFirewallLowPowerStandbyModeRules = new SparseIntArray(); /** Set of states for the child firewall chains. True if the chain is active. */ @GuardedBy("mUidRulesFirstLock") @@ -770,7 +766,8 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { /** List of apps indexed by uid and whether they have the internet permission */ @GuardedBy("mUidRulesFirstLock") - private final SparseBooleanArray mInternetPermissionMap = new SparseBooleanArray(); + @VisibleForTesting + final SparseBooleanArray mInternetPermissionMap = new SparseBooleanArray(); /** * Map of uid -> UidStateCallbackInfo objects holding the data received from @@ -1048,6 +1045,7 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { mUseMeteredFirewallChains = Flags.useMeteredFirewallChains(); mUseDifferentDelaysForBackgroundChain = Flags.useDifferentDelaysForBackgroundChain(); + mNeverApplyRulesToCoreUids = Flags.neverApplyRulesToCoreUids(); synchronized (mUidRulesFirstLock) { synchronized (mNetworkPoliciesSecondLock) { @@ -4098,6 +4096,8 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { + mUseMeteredFirewallChains); fout.println(Flags.FLAG_USE_DIFFERENT_DELAYS_FOR_BACKGROUND_CHAIN + ": " + mUseDifferentDelaysForBackgroundChain); + fout.println(Flags.FLAG_NEVER_APPLY_RULES_TO_CORE_UIDS + ": " + + mNeverApplyRulesToCoreUids); fout.println(); fout.println("mRestrictBackgroundLowPowerMode: " + mRestrictBackgroundLowPowerMode); @@ -4589,7 +4589,7 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { @VisibleForTesting @GuardedBy("mUidRulesFirstLock") void updateRestrictedModeAllowlistUL() { - mUidFirewallRestrictedModeRules.clear(); + final SparseIntArray uidRules = new SparseIntArray(); forEachUid("updateRestrictedModeAllowlist", uid -> { synchronized (mUidRulesFirstLock) { final int effectiveBlockedReasons = updateBlockedReasonsForRestrictedModeUL( @@ -4599,13 +4599,13 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { // setUidFirewallRulesUL will allowlist all uids that are passed to it, so only add // non-default rules. if (newFirewallRule != FIREWALL_RULE_DEFAULT) { - mUidFirewallRestrictedModeRules.append(uid, newFirewallRule); + uidRules.append(uid, newFirewallRule); } } }); if (mRestrictedNetworkingMode) { // firewall rules only need to be set when this mode is being enabled. - setUidFirewallRulesUL(FIREWALL_CHAIN_RESTRICTED, mUidFirewallRestrictedModeRules); + setUidFirewallRulesUL(FIREWALL_CHAIN_RESTRICTED, uidRules); } enableFirewallChainUL(FIREWALL_CHAIN_RESTRICTED, mRestrictedNetworkingMode); } @@ -4689,8 +4689,7 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { void updateRulesForPowerSaveUL() { Trace.traceBegin(Trace.TRACE_TAG_NETWORK, "updateRulesForPowerSaveUL"); try { - updateRulesForAllowlistedPowerSaveUL(mRestrictPower, FIREWALL_CHAIN_POWERSAVE, - mUidFirewallPowerSaveRules); + updateRulesForAllowlistedPowerSaveUL(mRestrictPower, FIREWALL_CHAIN_POWERSAVE); } finally { Trace.traceEnd(Trace.TRACE_TAG_NETWORK); } @@ -4705,8 +4704,7 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { void updateRulesForDeviceIdleUL() { Trace.traceBegin(Trace.TRACE_TAG_NETWORK, "updateRulesForDeviceIdleUL"); try { - updateRulesForAllowlistedPowerSaveUL(mDeviceIdleMode, FIREWALL_CHAIN_DOZABLE, - mUidFirewallDozableRules); + updateRulesForAllowlistedPowerSaveUL(mDeviceIdleMode, FIREWALL_CHAIN_DOZABLE); } finally { Trace.traceEnd(Trace.TRACE_TAG_NETWORK); } @@ -4720,13 +4718,11 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { // NOTE: since both fw_dozable and fw_powersave uses the same map // (mPowerSaveTempWhitelistAppIds) for allowlisting, we can reuse their logic in this method. @GuardedBy("mUidRulesFirstLock") - private void updateRulesForAllowlistedPowerSaveUL(boolean enabled, int chain, - SparseIntArray rules) { + private void updateRulesForAllowlistedPowerSaveUL(boolean enabled, int chain) { if (enabled) { // Sync the allowlists before enabling the chain. We don't care about the rules if // we are disabling the chain. - final SparseIntArray uidRules = rules; - uidRules.clear(); + final SparseIntArray uidRules = new SparseIntArray(); final List<UserInfo> users = mUserManager.getUsers(); for (int ui = users.size() - 1; ui >= 0; ui--) { UserInfo user = users.get(ui); @@ -4755,9 +4751,7 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { private void updateRulesForBackgroundChainUL() { Trace.traceBegin(Trace.TRACE_TAG_NETWORK, "updateRulesForBackgroundChainUL"); try { - final SparseIntArray uidRules = mUidFirewallBackgroundRules; - uidRules.clear(); - + final SparseIntArray uidRules = new SparseIntArray(); final List<UserInfo> users = mUserManager.getUsers(); for (int ui = users.size() - 1; ui >= 0; ui--) { final UserInfo user = users.get(ui); @@ -4794,17 +4788,17 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { Trace.traceBegin(Trace.TRACE_TAG_NETWORK, "updateRulesForLowPowerStandbyUL"); try { if (mLowPowerStandbyActive) { - mUidFirewallLowPowerStandbyModeRules.clear(); + final SparseIntArray uidRules = new SparseIntArray(); for (int i = mUidState.size() - 1; i >= 0; i--) { final int uid = mUidState.keyAt(i); final int effectiveBlockedReasons = getEffectiveBlockedReasons(uid); if (hasInternetPermissionUL(uid) && (effectiveBlockedReasons & BLOCKED_REASON_LOW_POWER_STANDBY) == 0) { - mUidFirewallLowPowerStandbyModeRules.put(uid, FIREWALL_RULE_ALLOW); + uidRules.put(uid, FIREWALL_RULE_ALLOW); } } setUidFirewallRulesUL(FIREWALL_CHAIN_LOW_POWER_STANDBY, - mUidFirewallLowPowerStandbyModeRules, CHAIN_TOGGLE_ENABLE); + uidRules, CHAIN_TOGGLE_ENABLE); } else { setUidFirewallRulesUL(FIREWALL_CHAIN_LOW_POWER_STANDBY, null, CHAIN_TOGGLE_DISABLE); } @@ -4822,10 +4816,8 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { final int effectiveBlockedReasons = getEffectiveBlockedReasons(uid); if (mUidState.contains(uid) && (effectiveBlockedReasons & BLOCKED_REASON_LOW_POWER_STANDBY) == 0) { - mUidFirewallLowPowerStandbyModeRules.put(uid, FIREWALL_RULE_ALLOW); setUidFirewallRuleUL(FIREWALL_CHAIN_LOW_POWER_STANDBY, uid, FIREWALL_RULE_ALLOW); } else { - mUidFirewallLowPowerStandbyModeRules.delete(uid); setUidFirewallRuleUL(FIREWALL_CHAIN_LOW_POWER_STANDBY, uid, FIREWALL_RULE_DEFAULT); } } @@ -4896,6 +4888,12 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { int[] idleUids = mUsageStats.getIdleUidsForUser(user.id); for (int uid : idleUids) { if (!mPowerSaveTempWhitelistAppIds.get(UserHandle.getAppId(uid), false)) { + if (mNeverApplyRulesToCoreUids && !isUidValidForRulesUL(uid)) { + // This check is needed to keep mUidFirewallStandbyRules free of any + // such uids. Doing this keeps it in sync with the actual rules applied + // in the underlying connectivity stack. + continue; + } // quick check: if this uid doesn't have INTERNET permission, it // doesn't have network access anyway, so it is a waste to mess // with it here. @@ -5198,6 +5196,11 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { @GuardedBy("mUidRulesFirstLock") private boolean isUidValidForAllowlistRulesUL(int uid) { + return isUidValidForRulesUL(uid); + } + + @GuardedBy("mUidRulesFirstLock") + private boolean isUidValidForRulesUL(int uid) { return UserHandle.isApp(uid) && hasInternetPermissionUL(uid); } @@ -5313,16 +5316,11 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { mActivityManagerInternal.onUidBlockedReasonsChanged(uid, BLOCKED_REASON_NONE); mUidPolicy.delete(uid); mUidFirewallStandbyRules.delete(uid); - mUidFirewallDozableRules.delete(uid); - mUidFirewallPowerSaveRules.delete(uid); - mUidFirewallBackgroundRules.delete(uid); mBackgroundTransitioningUids.delete(uid); mPowerSaveWhitelistExceptIdleAppIds.delete(uid); mPowerSaveWhitelistAppIds.delete(uid); mPowerSaveTempWhitelistAppIds.delete(uid); mAppIdleTempWhitelistAppIds.delete(uid); - mUidFirewallRestrictedModeRules.delete(uid); - mUidFirewallLowPowerStandbyModeRules.delete(uid); synchronized (mUidStateCallbackInfos) { mUidStateCallbackInfos.remove(uid); } @@ -6217,41 +6215,33 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { } } - private void addSdkSandboxUidsIfNeeded(SparseIntArray uidRules) { - final int size = uidRules.size(); - final SparseIntArray sdkSandboxUids = new SparseIntArray(); - for (int index = 0; index < size; index++) { - final int uid = uidRules.keyAt(index); - final int rule = uidRules.valueAt(index); - if (Process.isApplicationUid(uid)) { - sdkSandboxUids.put(Process.toSdkSandboxUid(uid), rule); - } - } - - for (int index = 0; index < sdkSandboxUids.size(); index++) { - final int uid = sdkSandboxUids.keyAt(index); - final int rule = sdkSandboxUids.valueAt(index); - uidRules.put(uid, rule); - } - } - /** * Set uid rules on a particular firewall chain. This is going to synchronize the rules given * here to netd. It will clean up dead rules and make sure the target chain only contains rules * specified here. */ + @GuardedBy("mUidRulesFirstLock") private void setUidFirewallRulesUL(int chain, SparseIntArray uidRules) { - addSdkSandboxUidsIfNeeded(uidRules); try { int size = uidRules.size(); - int[] uids = new int[size]; - int[] rules = new int[size]; + final IntArray uids = new IntArray(size); + final IntArray rules = new IntArray(size); for(int index = size - 1; index >= 0; --index) { - uids[index] = uidRules.keyAt(index); - rules[index] = uidRules.valueAt(index); + final int uid = uidRules.keyAt(index); + if (mNeverApplyRulesToCoreUids && !isUidValidForRulesUL(uid)) { + continue; + } + uids.add(uid); + rules.add(uidRules.valueAt(index)); + if (Process.isApplicationUid(uid)) { + uids.add(Process.toSdkSandboxUid(uid)); + rules.add(uidRules.valueAt(index)); + } } - mNetworkManager.setFirewallUidRules(chain, uids, rules); - mLogger.firewallRulesChanged(chain, uids, rules); + final int[] uidArray = uids.toArray(); + final int[] ruleArray = rules.toArray(); + mNetworkManager.setFirewallUidRules(chain, uidArray, ruleArray); + mLogger.firewallRulesChanged(chain, uidArray, ruleArray); } catch (IllegalStateException e) { Log.wtf(TAG, "problem setting firewall uid rules", e); } catch (RemoteException e) { @@ -6264,26 +6254,17 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { */ @GuardedBy("mUidRulesFirstLock") private void setUidFirewallRuleUL(int chain, int uid, int rule) { + if (mNeverApplyRulesToCoreUids && !isUidValidForRulesUL(uid)) { + return; + } if (Trace.isTagEnabled(Trace.TRACE_TAG_NETWORK)) { Trace.traceBegin(Trace.TRACE_TAG_NETWORK, "setUidFirewallRuleUL: " + chain + "/" + uid + "/" + rule); } try { - if (chain == FIREWALL_CHAIN_DOZABLE) { - mUidFirewallDozableRules.put(uid, rule); - } else if (chain == FIREWALL_CHAIN_STANDBY) { + if (chain == FIREWALL_CHAIN_STANDBY) { mUidFirewallStandbyRules.put(uid, rule); - } else if (chain == FIREWALL_CHAIN_POWERSAVE) { - mUidFirewallPowerSaveRules.put(uid, rule); - } else if (chain == FIREWALL_CHAIN_RESTRICTED) { - mUidFirewallRestrictedModeRules.put(uid, rule); - } else if (chain == FIREWALL_CHAIN_LOW_POWER_STANDBY) { - mUidFirewallLowPowerStandbyModeRules.put(uid, rule); - } else if (chain == FIREWALL_CHAIN_BACKGROUND) { - mUidFirewallBackgroundRules.put(uid, rule); - } - // Note that we do not need keep a separate cache of uid rules for chains that we do - // not call #setUidFirewallRulesUL for. + } try { mNetworkManager.setFirewallUidRule(chain, uid, rule); @@ -6328,6 +6309,8 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { * Resets all firewall rules associated with an UID. */ private void resetUidFirewallRules(int uid) { + // Resetting rules for uids with isUidValidForRulesUL = false should be OK as no rules + // should be previously set and the downstream code will skip no-op changes. try { mNetworkManager.setFirewallUidRule(FIREWALL_CHAIN_DOZABLE, uid, FIREWALL_RULE_DEFAULT); diff --git a/services/core/java/com/android/server/net/flags.aconfig b/services/core/java/com/android/server/net/flags.aconfig index 586baf022897..7f04e665567e 100644 --- a/services/core/java/com/android/server/net/flags.aconfig +++ b/services/core/java/com/android/server/net/flags.aconfig @@ -27,3 +27,13 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "never_apply_rules_to_core_uids" + namespace: "backstage_power" + description: "Removes all rule bookkeeping and evaluation logic for core uids and uids without the internet permission" + bug: "356956588" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/services/core/java/com/android/server/notification/ConditionProviders.java b/services/core/java/com/android/server/notification/ConditionProviders.java index a44e55344fe3..66e61c076030 100644 --- a/services/core/java/com/android/server/notification/ConditionProviders.java +++ b/services/core/java/com/android/server/notification/ConditionProviders.java @@ -16,9 +16,6 @@ package com.android.server.notification; -import static android.service.notification.Condition.STATE_TRUE; -import static android.service.notification.ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI; - import android.app.INotificationManager; import android.app.NotificationManager; import android.content.ComponentName; @@ -322,20 +319,7 @@ public class ConditionProviders extends ManagedServices { final Condition c = conditions[i]; final ConditionRecord r = getRecordLocked(c.id, info.component, true /*create*/); r.info = info; - if (android.app.Flags.modesUi()) { - // if user turned on the mode, ignore the update unless the app also wants the - // mode on. this will update the origin of the mode and let the owner turn it - // off when the context ends - if (r.condition != null && r.condition.source == ORIGIN_USER_IN_SYSTEMUI) { - if (r.condition.state == STATE_TRUE && c.state == STATE_TRUE) { - r.condition = c; - } - } else { - r.condition = c; - } - } else { - r.condition = c; - } + r.condition = c; } } final int N = conditions.length; diff --git a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java index bd551fb2ab1b..981891669e7c 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)); } /** @@ -1519,7 +1519,14 @@ public final class NotificationAttentionHelper { @Override public void setLastNotificationUpdateTimeMs(NotificationRecord record, long timestampMillis) { - super.setLastNotificationUpdateTimeMs(record, timestampMillis); + if (Flags.politeNotificationsAttnUpdate()) { + // Set last update per package/channel only for exempt notifications + if (isAvalancheExempted(record)) { + super.setLastNotificationUpdateTimeMs(record, timestampMillis); + } + } else { + super.setLastNotificationUpdateTimeMs(record, timestampMillis); + } mLastNotificationTimestamp = timestampMillis; mAppStrategy.setLastNotificationUpdateTimeMs(record, timestampMillis); } 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/notification/ZenModeConditions.java b/services/core/java/com/android/server/notification/ZenModeConditions.java index 268d835e704c..d495ef5ce108 100644 --- a/services/core/java/com/android/server/notification/ZenModeConditions.java +++ b/services/core/java/com/android/server/notification/ZenModeConditions.java @@ -82,7 +82,7 @@ public class ZenModeConditions implements ConditionProviders.Callback { for (ZenRule automaticRule : config.automaticRules.values()) { if (automaticRule.component != null) { evaluateRule(automaticRule, current, trigger, processSubscriptions, false); - updateSnoozing(automaticRule); + automaticRule.reconsiderConditionOverride(); } } @@ -187,13 +187,4 @@ public class ZenModeConditions implements ConditionProviders.Callback { + rule.conditionId); } } - - private boolean updateSnoozing(ZenRule rule) { - if (rule != null && rule.snoozing && !rule.isTrueOrUnknown()) { - rule.snoozing = false; - if (DEBUG) Log.d(TAG, "Snoozing reset for " + rule.conditionId); - return true; - } - return false; - } } diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java index b164a52ff5d7..db48835a9b82 100644 --- a/services/core/java/com/android/server/notification/ZenModeHelper.java +++ b/services/core/java/com/android/server/notification/ZenModeHelper.java @@ -37,6 +37,8 @@ import static android.service.notification.ZenModeConfig.ORIGIN_SYSTEM; import static android.service.notification.ZenModeConfig.ORIGIN_UNKNOWN; import static android.service.notification.ZenModeConfig.ORIGIN_USER_IN_APP; import static android.service.notification.ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI; +import static android.service.notification.ZenModeConfig.ZenRule.OVERRIDE_ACTIVATE; +import static android.service.notification.ZenModeConfig.ZenRule.OVERRIDE_DEACTIVATE; import static com.android.internal.util.FrameworkStatsLog.DND_MODE_RULE; import static com.android.internal.util.Preconditions.checkArgument; @@ -643,10 +645,10 @@ public class ZenModeHelper { if ((rule.userModifiedFields & AutomaticZenRule.FIELD_INTERRUPTION_FILTER) == 0) { rule.zenMode = zenMode; } - rule.snoozing = false; rule.condition = new Condition(rule.conditionId, mContext.getString(R.string.zen_mode_implicit_activated), STATE_TRUE); + rule.resetConditionOverride(); setConfigLocked(newConfig, /* triggeringComponent= */ null, ORIGIN_APP, "applyGlobalZenModeAsImplicitZenRule", callingUid); @@ -867,8 +869,8 @@ public class ZenModeHelper { ZenRule deletedRule = ruleToRemove.copy(); deletedRule.deletionInstant = Instant.now(mClock); // If the rule is restored it shouldn't be active (or snoozed). - deletedRule.snoozing = false; deletedRule.condition = null; + deletedRule.resetConditionOverride(); // Overwrites a previously-deleted rule with the same conditionId, but that's okay. config.deletedRules.put(deletedKey, deletedRule); } @@ -885,7 +887,12 @@ public class ZenModeHelper { if (rule == null || !canManageAutomaticZenRule(rule)) { return Condition.STATE_UNKNOWN; } - return rule.condition != null ? rule.condition.state : STATE_FALSE; + if (Flags.modesApi() && Flags.modesUi()) { + return rule.isAutomaticActive() ? STATE_TRUE : STATE_FALSE; + } else { + // Buggy, does not consider snoozing! + return rule.condition != null ? rule.condition.state : STATE_FALSE; + } } } @@ -943,12 +950,40 @@ public class ZenModeHelper { } for (ZenRule rule : rules) { - rule.condition = condition; - updateSnoozing(rule); + applyConditionAndReconsiderOverride(rule, condition, origin); setConfigLocked(config, rule.component, origin, "conditionChanged", callingUid); } } + private static void applyConditionAndReconsiderOverride(ZenRule rule, Condition condition, + int origin) { + if (Flags.modesApi() && Flags.modesUi()) { + if (origin == ORIGIN_USER_IN_SYSTEMUI && condition != null + && condition.source == SOURCE_USER_ACTION) { + // Apply as override, instead of actual condition. + if (condition.state == STATE_TRUE) { + // Manually turn on a rule -> Apply override. + rule.setConditionOverride(OVERRIDE_ACTIVATE); + } else if (condition.state == STATE_FALSE) { + // Manually turn off a rule. If the rule was manually activated before, reset + // override -- but only if this will not result in the rule turning on + // immediately because of a previously snoozed condition! In that case, apply + // deactivate-override. + rule.resetConditionOverride(); + if (rule.isAutomaticActive()) { + rule.setConditionOverride(OVERRIDE_DEACTIVATE); + } + } + } else { + rule.condition = condition; + rule.reconsiderConditionOverride(); + } + } else { + rule.condition = condition; + rule.reconsiderConditionOverride(); + } + } + private static List<ZenRule> findMatchingRules(ZenModeConfig config, Uri id, Condition condition) { List<ZenRule> matchingRules = new ArrayList<>(); @@ -971,15 +1006,6 @@ public class ZenModeHelper { return true; } - private boolean updateSnoozing(ZenRule rule) { - if (rule != null && rule.snoozing && !rule.isTrueOrUnknown()) { - rule.snoozing = false; - if (DEBUG) Log.d(TAG, "Snoozing reset for " + rule.conditionId); - return true; - } - return false; - } - public int getCurrentInstanceCount(ComponentName cn) { if (cn == null) { return 0; @@ -1181,7 +1207,7 @@ public class ZenModeHelper { if (rule.enabled != azr.isEnabled()) { rule.enabled = azr.isEnabled(); - rule.snoozing = false; + rule.resetConditionOverride(); modified = true; } if (!Objects.equals(rule.configurationActivity, azr.getConfigurationActivity())) { @@ -1271,7 +1297,7 @@ public class ZenModeHelper { return modified; } else { if (rule.enabled != azr.isEnabled()) { - rule.snoozing = false; + rule.resetConditionOverride(); } rule.name = azr.getName(); rule.condition = null; @@ -1573,18 +1599,16 @@ public class ZenModeHelper { // For API calls (different origin) keep old behavior of snoozing all rules. for (ZenRule automaticRule : newConfig.automaticRules.values()) { if (automaticRule.isAutomaticActive()) { - automaticRule.snoozing = true; + automaticRule.setConditionOverride(OVERRIDE_DEACTIVATE); } } } } else { if (zenMode == Global.ZEN_MODE_OFF) { newConfig.manualRule = null; - // User deactivation of DND means just turning off the manual DND rule. - // For API calls (different origin) keep old behavior of snoozing all rules. for (ZenRule automaticRule : newConfig.automaticRules.values()) { if (automaticRule.isAutomaticActive()) { - automaticRule.snoozing = true; + automaticRule.setConditionOverride(OVERRIDE_DEACTIVATE); } } @@ -1626,13 +1650,11 @@ public class ZenModeHelper { void dump(ProtoOutputStream proto) { proto.write(ZenModeProto.ZEN_MODE, mZenMode); synchronized (mConfigLock) { - if (mConfig.manualRule != null) { + if (mConfig.isManualActive()) { mConfig.manualRule.dumpDebug(proto, ZenModeProto.ENABLED_ACTIVE_CONDITIONS); } for (ZenRule rule : mConfig.automaticRules.values()) { - if (rule.enabled && rule.condition != null - && rule.condition.state == STATE_TRUE - && !rule.snoozing) { + if (rule.isAutomaticActive()) { rule.dumpDebug(proto, ZenModeProto.ENABLED_ACTIVE_CONDITIONS); } } @@ -1676,7 +1698,11 @@ public class ZenModeHelper { if (config != null) { if (forRestore) { config.user = userId; - if (!Flags.modesUi()) { + if (Flags.modesUi()) { + if (config.manualRule != null) { + config.manualRule.condition = null; // don't restore transient state + } + } else { config.manualRule = null; // don't restore the manual rule } } @@ -1691,8 +1717,8 @@ public class ZenModeHelper { for (ZenRule automaticRule : config.automaticRules.values()) { if (forRestore) { // don't restore transient state from restored automatic rules - automaticRule.snoozing = false; automaticRule.condition = null; + automaticRule.resetConditionOverride(); automaticRule.creationTime = time; } diff --git a/services/core/java/com/android/server/om/OverlayManagerService.java b/services/core/java/com/android/server/om/OverlayManagerService.java index 46585a50ea36..6303ecd53dbb 100644 --- a/services/core/java/com/android/server/om/OverlayManagerService.java +++ b/services/core/java/com/android/server/om/OverlayManagerService.java @@ -80,6 +80,7 @@ import android.util.EventLog; import android.util.Slog; import android.util.SparseArray; +import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.KeepForWeakReference; import com.android.internal.content.PackageMonitor; import com.android.internal.content.om.OverlayConfig; @@ -1180,6 +1181,7 @@ public final class OverlayManagerService extends SystemService { // intent, querying the PackageManagerService for the actual current // state may lead to contradictions within OMS. Better then to lag // behind until all pending intents have been processed. + @GuardedBy("itself") private final ArrayMap<String, PackageStateUsers> mCache = new ArrayMap<>(); private final ArraySet<Integer> mInitializedUsers = new ArraySet<>(); @@ -1207,10 +1209,12 @@ public final class OverlayManagerService extends SystemService { } final ArrayMap<String, PackageState> userPackages = new ArrayMap<>(); - for (int i = 0, n = mCache.size(); i < n; i++) { - final PackageStateUsers pkg = mCache.valueAt(i); - if (pkg.mInstalledUsers.contains(userId)) { - userPackages.put(mCache.keyAt(i), pkg.mPackageState); + synchronized (mCache) { + for (int i = 0, n = mCache.size(); i < n; i++) { + final PackageStateUsers pkg = mCache.valueAt(i); + if (pkg.mInstalledUsers.contains(userId)) { + userPackages.put(mCache.keyAt(i), pkg.mPackageState); + } } } return userPackages; @@ -1220,7 +1224,11 @@ public final class OverlayManagerService extends SystemService { @Nullable public PackageState getPackageStateForUser(@NonNull final String packageName, final int userId) { - final PackageStateUsers pkg = mCache.get(packageName); + final PackageStateUsers pkg; + + synchronized (mCache) { + pkg = mCache.get(packageName); + } if (pkg != null && pkg.mInstalledUsers.contains(userId)) { return pkg.mPackageState; } @@ -1251,12 +1259,15 @@ public final class OverlayManagerService extends SystemService { @NonNull private PackageState addPackageUser(@NonNull final PackageState pkg, final int user) { - PackageStateUsers pkgUsers = mCache.get(pkg.getPackageName()); - if (pkgUsers == null) { - pkgUsers = new PackageStateUsers(pkg); - mCache.put(pkg.getPackageName(), pkgUsers); - } else { - pkgUsers.mPackageState = pkg; + PackageStateUsers pkgUsers; + synchronized (mCache) { + pkgUsers = mCache.get(pkg.getPackageName()); + if (pkgUsers == null) { + pkgUsers = new PackageStateUsers(pkg); + mCache.put(pkg.getPackageName(), pkgUsers); + } else { + pkgUsers.mPackageState = pkg; + } } pkgUsers.mInstalledUsers.add(user); return pkgUsers.mPackageState; @@ -1265,18 +1276,24 @@ public final class OverlayManagerService extends SystemService { @NonNull private void removePackageUser(@NonNull final String packageName, final int user) { - final PackageStateUsers pkgUsers = mCache.get(packageName); - if (pkgUsers == null) { - return; + // synchronize should include the call to the other removePackageUser() method so that + // the access and modification happen under the same lock. + synchronized (mCache) { + final PackageStateUsers pkgUsers = mCache.get(packageName); + if (pkgUsers == null) { + return; + } + removePackageUser(pkgUsers, user); } - removePackageUser(pkgUsers, user); } @NonNull private void removePackageUser(@NonNull final PackageStateUsers pkg, final int user) { pkg.mInstalledUsers.remove(user); if (pkg.mInstalledUsers.isEmpty()) { - mCache.remove(pkg.mPackageState.getPackageName()); + synchronized (mCache) { + mCache.remove(pkg.mPackageState.getPackageName()); + } } } @@ -1386,8 +1403,10 @@ public final class OverlayManagerService extends SystemService { public void forgetAllPackageInfos(final int userId) { // Iterate in reverse order since removing the package in all users will remove the // package from the cache. - for (int i = mCache.size() - 1; i >= 0; i--) { - removePackageUser(mCache.valueAt(i), userId); + synchronized (mCache) { + for (int i = mCache.size() - 1; i >= 0; i--) { + removePackageUser(mCache.valueAt(i), userId); + } } } @@ -1405,22 +1424,23 @@ public final class OverlayManagerService extends SystemService { public void dump(@NonNull final PrintWriter pw, @NonNull DumpState dumpState) { pw.println("AndroidPackage cache"); + synchronized (mCache) { + if (!dumpState.isVerbose()) { + pw.println(TAB1 + mCache.size() + " package(s)"); + return; + } - if (!dumpState.isVerbose()) { - pw.println(TAB1 + mCache.size() + " package(s)"); - return; - } - - if (mCache.size() == 0) { - pw.println(TAB1 + "<empty>"); - return; - } + if (mCache.size() == 0) { + pw.println(TAB1 + "<empty>"); + return; + } - for (int i = 0, n = mCache.size(); i < n; i++) { - final String packageName = mCache.keyAt(i); - final PackageStateUsers pkg = mCache.valueAt(i); - pw.print(TAB1 + packageName + ": " + pkg.mPackageState + " users="); - pw.println(TextUtils.join(", ", pkg.mInstalledUsers)); + for (int i = 0, n = mCache.size(); i < n; i++) { + final String packageName = mCache.keyAt(i); + final PackageStateUsers pkg = mCache.valueAt(i); + pw.print(TAB1 + packageName + ": " + pkg.mPackageState + " users="); + pw.println(TextUtils.join(", ", pkg.mInstalledUsers)); + } } } } diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index a0d5ea875abf..22b4d5def8f4 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -686,6 +686,9 @@ final class InstallPackageHelper { (installFlags & PackageManager.INSTALL_INSTANT_APP) != 0; final boolean fullApp = (installFlags & PackageManager.INSTALL_FULL_APP) != 0; + final boolean isPackageDeviceAdmin = mPm.isPackageDeviceAdmin(packageName, userId); + final boolean isProtectedPackage = mPm.mProtectedPackages != null + && mPm.mProtectedPackages.isPackageStateProtected(userId, packageName); // writer synchronized (mPm.mLock) { @@ -694,7 +697,8 @@ final class InstallPackageHelper { if (pkgSetting == null || pkgSetting.getPkg() == null) { return Pair.create(PackageManager.INSTALL_FAILED_INVALID_URI, intentSender); } - if (instantApp && (pkgSetting.isSystem() || pkgSetting.isUpdatedSystemApp())) { + if (instantApp && (pkgSetting.isSystem() || pkgSetting.isUpdatedSystemApp() + || isPackageDeviceAdmin || isProtectedPackage)) { return Pair.create(PackageManager.INSTALL_FAILED_INVALID_URI, intentSender); } if (!snapshot.canViewInstantApps(callingUid, UserHandle.getUserId(callingUid))) { @@ -1801,26 +1805,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/Settings.java b/services/core/java/com/android/server/pm/Settings.java index b7dfd8d0f8cd..55280b4cdc5b 100644 --- a/services/core/java/com/android/server/pm/Settings.java +++ b/services/core/java/com/android/server/pm/Settings.java @@ -2585,12 +2585,31 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile boolean optional = parser.getAttributeBoolean(null, ATTR_OPTIONAL, true); if (libName != null && libVersion >= 0) { + final int beforeUsesSdkLibrariesLength = outPs.getUsesSdkLibraries().length; + // If the lib already exists in the outPs#getUsesSdkLibraries, don't add it + // into the array and update its information below outPs.setUsesSdkLibraries(ArrayUtils.appendElement(String.class, outPs.getUsesSdkLibraries(), libName)); - outPs.setUsesSdkLibrariesVersionsMajor(ArrayUtils.appendLong( - outPs.getUsesSdkLibrariesVersionsMajor(), libVersion)); - outPs.setUsesSdkLibrariesOptional(ArrayUtils.appendBoolean( - outPs.getUsesSdkLibrariesOptional(), optional)); + + // If the lib has already been added before, update the other information + final int afterUsesSdkLibrariesLength = outPs.getUsesSdkLibraries().length; + if (beforeUsesSdkLibrariesLength == afterUsesSdkLibrariesLength) { + final int index = ArrayUtils.indexOf(outPs.getUsesSdkLibraries(), libName); + final long[] usesSdkLibrariesVersionsMajor = + outPs.getUsesSdkLibrariesVersionsMajor(); + usesSdkLibrariesVersionsMajor[index] = libVersion; + outPs.setUsesSdkLibrariesVersionsMajor(usesSdkLibrariesVersionsMajor); + + final boolean[] usesSdkLibrariesOptional = outPs.getUsesSdkLibrariesOptional(); + usesSdkLibrariesOptional[index] = optional; + outPs.setUsesSdkLibrariesOptional(usesSdkLibrariesOptional); + } else { + outPs.setUsesSdkLibrariesVersionsMajor(ArrayUtils.appendLong( + outPs.getUsesSdkLibrariesVersionsMajor(), libVersion, + /* allowDuplicates= */ true)); + outPs.setUsesSdkLibrariesOptional(ArrayUtils.appendBooleanDuplicatesAllowed( + outPs.getUsesSdkLibrariesOptional(), optional)); + } } XmlUtils.skipCurrentTag(parser); @@ -2602,10 +2621,24 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile long libVersion = parser.getAttributeLong(null, ATTR_VERSION, -1); if (libName != null && libVersion >= 0) { + final int beforeUsesStaticLibrariesLength = outPs.getUsesStaticLibraries().length; + // If the lib already exists in the outPs#getUsesStaticLibraries, don't add it + // into the array and update its information below outPs.setUsesStaticLibraries(ArrayUtils.appendElement(String.class, outPs.getUsesStaticLibraries(), libName)); - outPs.setUsesStaticLibrariesVersions(ArrayUtils.appendLong( - outPs.getUsesStaticLibrariesVersions(), libVersion)); + + // If the lib has already been added before, update the version + final int afterUsesStaticLibrariesLength = outPs.getUsesStaticLibraries().length; + if (beforeUsesStaticLibrariesLength == afterUsesStaticLibrariesLength) { + final int index = ArrayUtils.indexOf(outPs.getUsesStaticLibraries(), libName); + final long[] usesStaticLibrariesVersions = outPs.getUsesStaticLibrariesVersions(); + usesStaticLibrariesVersions[index] = libVersion; + outPs.setUsesStaticLibrariesVersions(usesStaticLibrariesVersions); + } else { + outPs.setUsesStaticLibrariesVersions(ArrayUtils.appendLong( + outPs.getUsesStaticLibrariesVersions(), libVersion, + /* allowDuplicates= */ true)); + } } XmlUtils.skipCurrentTag(parser); diff --git a/services/core/java/com/android/server/pm/TEST_MAPPING b/services/core/java/com/android/server/pm/TEST_MAPPING index c40608d12a0e..c95d88e8c697 100644 --- a/services/core/java/com/android/server/pm/TEST_MAPPING +++ b/services/core/java/com/android/server/pm/TEST_MAPPING @@ -163,6 +163,22 @@ }, { "name": "CtsUpdateOwnershipEnforcementTestCases" + }, + { + "name": "CtsPackageInstallerCUJTestCases", + "file_patterns": [ + "core/java/.*Install.*", + "services/core/.*Install.*", + "services/core/java/com/android/server/pm/.*" + ], + "options":[ + { + "exclude-annotation":"androidx.test.filters.FlakyTest" + }, + { + "exclude-annotation":"org.junit.Ignore" + } + ] } ], "imports": [ diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index 829ee27287c2..57d7d79b2392 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -2105,6 +2105,10 @@ public class UserManagerService extends IUserManager.Stub { @Override public void setUserAdmin(@UserIdInt int userId) { checkManageUserAndAcrossUsersFullPermission("set user admin"); + if (Flags.unicornModeRefactoringForHsumReadOnly()) { + checkAdminStatusChangeAllowed(userId); + } + mUserJourneyLogger.logUserJourneyBegin(userId, USER_JOURNEY_GRANT_ADMIN); UserData user; synchronized (mPackagesLock) { @@ -2133,6 +2137,10 @@ public class UserManagerService extends IUserManager.Stub { @Override public void revokeUserAdmin(@UserIdInt int userId) { checkManageUserAndAcrossUsersFullPermission("revoke admin privileges"); + if (Flags.unicornModeRefactoringForHsumReadOnly()) { + checkAdminStatusChangeAllowed(userId); + } + mUserJourneyLogger.logUserJourneyBegin(userId, USER_JOURNEY_REVOKE_ADMIN); UserData user; synchronized (mPackagesLock) { @@ -4065,6 +4073,26 @@ public class UserManagerService extends IUserManager.Stub { } } + /** + * Checks if changing the admin status of a target user is restricted + * due to the DISALLOW_GRANT_ADMIN restriction. If either the calling + * user or the target user has this restriction, a SecurityException + * is thrown. + * + * @param targetUser The user ID of the user whose admin status is being + * considered for change. + * @throws SecurityException if the admin status change is restricted due + * to the DISALLOW_GRANT_ADMIN restriction. + */ + private void checkAdminStatusChangeAllowed(int targetUser) { + if (hasUserRestriction(UserManager.DISALLOW_GRANT_ADMIN, UserHandle.getCallingUserId()) + || hasUserRestriction(UserManager.DISALLOW_GRANT_ADMIN, targetUser)) { + throw new SecurityException( + "Admin status change is restricted. The DISALLOW_GRANT_ADMIN " + + "restriction is applied either on the current or the target user."); + } + } + @GuardedBy({"mPackagesLock"}) private void writeBitmapLP(UserInfo info, Bitmap bitmap) { try { @@ -5443,6 +5471,13 @@ public class UserManagerService extends IUserManager.Stub { enforceUserRestriction(restriction, UserHandle.getCallingUserId(), "Cannot add user"); + if (Flags.unicornModeRefactoringForHsumReadOnly()) { + if ((flags & UserInfo.FLAG_ADMIN) != 0) { + enforceUserRestriction(UserManager.DISALLOW_GRANT_ADMIN, + UserHandle.getCallingUserId(), "Cannot create ADMIN user"); + } + } + return createUserInternalUnchecked(name, userType, flags, parentId, /* preCreate= */ false, disallowedPackages, /* token= */ null); } 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 21d6c6457e75..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; @@ -164,8 +165,6 @@ import android.os.SystemProperties; import android.os.Trace; import android.os.UEventObserver; import android.os.UserHandle; -import android.os.VibrationAttributes; -import android.os.VibrationEffect; import android.os.Vibrator; import android.provider.DeviceConfig; import android.provider.MediaStore; @@ -229,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; @@ -238,8 +235,6 @@ import com.android.server.policy.keyguard.KeyguardServiceDelegate; import com.android.server.policy.keyguard.KeyguardServiceDelegate.DrawnListener; import com.android.server.policy.keyguard.KeyguardStateMonitor.StateCallback; import com.android.server.statusbar.StatusBarManagerInternal; -import com.android.server.vibrator.HapticFeedbackVibrationProvider; -import com.android.server.vibrator.VibratorFrameworkStatsLogger; import com.android.server.vr.VrManagerInternal; import com.android.server.wallpaper.WallpaperManagerInternal; import com.android.server.wm.ActivityTaskManagerInternal; @@ -462,7 +457,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { PackageManager mPackageManager; SideFpsEventHandler mSideFpsEventHandler; LockPatternUtils mLockPatternUtils; - private HapticFeedbackVibrationProvider mHapticFeedbackVibrationProvider; private boolean mHasFeatureAuto; private boolean mHasFeatureWatch; private boolean mHasFeatureLeanback; @@ -737,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 { @@ -825,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; @@ -1829,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(); @@ -2063,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; @@ -2091,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: @@ -2388,8 +2383,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { mContext.registerReceiver(mMultiuserReceiver, filter); mVibrator = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE); - mHapticFeedbackVibrationProvider = - new HapticFeedbackVibrationProvider(mContext.getResources(), mVibrator); mGlobalKeyManager = new GlobalKeyManager(mContext); @@ -3292,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 @@ -3434,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: @@ -3443,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; @@ -3452,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; @@ -3465,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; @@ -3480,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; } @@ -3491,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; @@ -3504,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; } @@ -3520,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; } } @@ -3530,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; } } @@ -3540,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; @@ -3556,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; } } @@ -3568,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; @@ -3620,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: @@ -3665,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 = @@ -3674,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; } } @@ -3687,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: @@ -3705,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: @@ -3718,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; @@ -3737,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; } @@ -3769,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: @@ -3790,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; @@ -4739,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. @@ -4760,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); @@ -4862,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) { @@ -4872,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, @@ -4895,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()) { @@ -4918,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) { @@ -4929,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; @@ -4938,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; @@ -4953,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. @@ -4998,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; @@ -5009,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; @@ -5975,10 +6011,10 @@ public class PhoneWindowManager implements WindowManagerPolicy { public void setSafeMode(boolean safeMode) { mSafeMode = safeMode; if (safeMode) { - performHapticFeedback(Process.myUid(), mContext.getOpPackageName(), + performHapticFeedback( HapticFeedbackConstants.SAFE_MODE_ENABLED, - "Safe Mode Enabled", HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING, - 0 /* privFlags */); + "Safe Mode Enabled" /* reason */, + HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING); } } @@ -6447,33 +6483,18 @@ public class PhoneWindowManager implements WindowManagerPolicy { Settings.Global.THEATER_MODE_ON, 0) == 1; } - private boolean performHapticFeedback(int effectId, String reason) { - return performHapticFeedback(Process.myUid(), mContext.getOpPackageName(), - effectId, reason, 0 /* flags */, 0 /* privFlags */); + private void performHapticFeedback(int effectId, String reason) { + performHapticFeedback(effectId, reason, 0 /* flags */); } - @Override - public boolean isGlobalKey(int keyCode) { - return mGlobalKeyManager.shouldHandleGlobalKey(keyCode); + private void performHapticFeedback( + int effectId, String reason, @HapticFeedbackConstants.Flags int flags) { + mVibrator.performHapticFeedback(effectId, reason, flags, 0 /* privFlags */); } @Override - public boolean performHapticFeedback(int uid, String packageName, int effectId, String reason, - int flags, int privFlags) { - if (!mVibrator.hasVibrator()) { - return false; - } - VibrationEffect effect = - mHapticFeedbackVibrationProvider.getVibrationForHapticFeedback(effectId); - if (effect == null) { - return false; - } - VibrationAttributes attrs = - mHapticFeedbackVibrationProvider.getVibrationAttributesForHapticFeedback( - effectId, flags, privFlags); - VibratorFrameworkStatsLogger.logPerformHapticsFeedbackIfKeyboard(uid, effectId); - mVibrator.vibrate(uid, packageName, effect, reason, attrs); - return true; + public boolean isGlobalKey(int keyCode) { + return mGlobalKeyManager.shouldHandleGlobalKey(keyCode); } @@ -6651,7 +6672,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { pw.print(" mLockScreenTimerActive="); pw.println(mLockScreenTimerActive); pw.print(prefix); pw.print("mKidsModeEnabled="); pw.println(mKidsModeEnabled); - mHapticFeedbackVibrationProvider.dump(prefix, pw); mGlobalKeyManager.dump(prefix, pw); mKeyCombinationManager.dump(prefix, pw); mSingleKeyGestureDetector.dump(prefix, pw); diff --git a/services/core/java/com/android/server/policy/WindowManagerPolicy.java b/services/core/java/com/android/server/policy/WindowManagerPolicy.java index 1b394f65c5eb..67f5f27b42eb 100644 --- a/services/core/java/com/android/server/policy/WindowManagerPolicy.java +++ b/services/core/java/com/android/server/policy/WindowManagerPolicy.java @@ -80,7 +80,6 @@ import android.os.RemoteException; import android.util.Slog; import android.util.proto.ProtoOutputStream; import android.view.Display; -import android.view.HapticFeedbackConstants; import android.view.IDisplayFoldListener; import android.view.KeyEvent; import android.view.KeyboardShortcutGroup; @@ -1077,13 +1076,6 @@ public interface WindowManagerPolicy extends WindowManagerPolicyConstants { public void enableScreenAfterBoot(); /** - * Call from application to perform haptic feedback on its window. - */ - public boolean performHapticFeedback(int uid, String packageName, int effectId, - String reason, @HapticFeedbackConstants.Flags int flags, - @HapticFeedbackConstants.PrivateFlags int privFlags); - - /** * Called when we have started keeping the screen on because a window * requesting this has become visible. */ diff --git a/services/core/java/com/android/server/vibrator/HalVibration.java b/services/core/java/com/android/server/vibrator/HalVibration.java index 46bd7af159da..fe0cf5909970 100644 --- a/services/core/java/com/android/server/vibrator/HalVibration.java +++ b/services/core/java/com/android/server/vibrator/HalVibration.java @@ -170,9 +170,11 @@ final class HalVibration extends Vibration { /** Return {@link VibrationStats.StatsInfo} with read-only metrics about this vibration. */ public VibrationStats.StatsInfo getStatsInfo(long completionUptimeMillis) { - int vibrationType = isRepeating() - ? FrameworkStatsLog.VIBRATION_REPORTED__VIBRATION_TYPE__REPEATED - : FrameworkStatsLog.VIBRATION_REPORTED__VIBRATION_TYPE__SINGLE; + int vibrationType = mEffectToPlay.hasVendorEffects() + ? FrameworkStatsLog.VIBRATION_REPORTED__VIBRATION_TYPE__VENDOR + : isRepeating() + ? FrameworkStatsLog.VIBRATION_REPORTED__VIBRATION_TYPE__REPEATED + : FrameworkStatsLog.VIBRATION_REPORTED__VIBRATION_TYPE__SINGLE; return new VibrationStats.StatsInfo( callerInfo.uid, vibrationType, callerInfo.attrs.getUsage(), mStatus, stats, completionUptimeMillis); diff --git a/services/core/java/com/android/server/vibrator/HapticFeedbackCustomization.java b/services/core/java/com/android/server/vibrator/HapticFeedbackCustomization.java index 503a7268d5d3..65fc7b2c5c39 100644 --- a/services/core/java/com/android/server/vibrator/HapticFeedbackCustomization.java +++ b/services/core/java/com/android/server/vibrator/HapticFeedbackCustomization.java @@ -18,6 +18,7 @@ package com.android.server.vibrator; import android.annotation.Nullable; import android.content.res.Resources; +import android.content.res.XmlResourceParser; import android.os.VibrationEffect; import android.os.VibratorInfo; import android.os.vibrator.Flags; @@ -28,6 +29,7 @@ import android.util.Slog; import android.util.SparseArray; import android.util.Xml; +import com.android.internal.util.XmlUtils; import com.android.internal.vibrator.persistence.XmlParserException; import com.android.internal.vibrator.persistence.XmlReader; import com.android.internal.vibrator.persistence.XmlValidator; @@ -39,6 +41,7 @@ import org.xmlpull.v1.XmlPullParserException; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; +import java.io.Reader; /** * Class that loads custom {@link VibrationEffect} to be performed for each @@ -127,27 +130,19 @@ final class HapticFeedbackCustomization { Slog.d(TAG, "Haptic feedback customization feature is not enabled."); return null; } - String customizationFile = - res.getString( - com.android.internal.R.string.config_hapticFeedbackCustomizationFile); - if (TextUtils.isEmpty(customizationFile)) { - Slog.d(TAG, "Customization file not configured."); - return null; - } - FileReader fileReader; - try { - fileReader = new FileReader(customizationFile); - } catch (FileNotFoundException e) { - Slog.d(TAG, "Specified customization file not found."); - return null; + // Old loading path that reads customization from file at dir defined by config. + TypedXmlPullParser parser = readCustomizationFile(res); + if (parser == null) { + // When old loading path doesn't succeed, try loading customization from resources. + parser = readCustomizationResources(res); + } + if (parser == null) { + Slog.d(TAG, "No loadable haptic feedback customization."); + return null; } - TypedXmlPullParser parser = Xml.newFastPullParser(); - parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); - parser.setInput(fileReader); - - XmlReader.readDocumentStartTag(parser, TAG_CONSTANTS); + XmlUtils.beginDocument(parser, TAG_CONSTANTS); XmlValidator.checkTagHasNoUnexpectedAttributes(parser); int rootDepth = parser.getDepth(); @@ -191,6 +186,46 @@ final class HapticFeedbackCustomization { return mapping; } + // TODO(b/356412421): deprecate old path related files. + private static TypedXmlPullParser readCustomizationFile(Resources res) + throws XmlPullParserException { + String customizationFile = res.getString( + com.android.internal.R.string.config_hapticFeedbackCustomizationFile); + if (TextUtils.isEmpty(customizationFile)) { + return null; + } + + final Reader customizationReader; + try { + customizationReader = new FileReader(customizationFile); + } catch (FileNotFoundException e) { + Slog.e(TAG, "Specified customization file not found.", e); + return null; + } + + final TypedXmlPullParser parser; + parser = Xml.newFastPullParser(); + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); + parser.setInput(customizationReader); + Slog.d(TAG, "Successfully opened customization file."); + return parser; + } + + private static TypedXmlPullParser readCustomizationResources(Resources res) { + if (!Flags.loadHapticFeedbackVibrationCustomizationFromResources()) { + return null; + } + final XmlResourceParser resParser; + try { + resParser = res.getXml(com.android.internal.R.xml.haptic_feedback_customization); + } catch (Resources.NotFoundException e) { + Slog.e(TAG, "Haptic customization resource not found.", e); + return null; + } + Slog.d(TAG, "Successfully opened customization resource."); + return XmlUtils.makeTyped(resParser); + } + /** * Represents an error while parsing a haptic feedback customization XML. */ diff --git a/services/core/java/com/android/server/vibrator/PerformVendorEffectVibratorStep.java b/services/core/java/com/android/server/vibrator/PerformVendorEffectVibratorStep.java index 8f36118543ed..407f3d996798 100644 --- a/services/core/java/com/android/server/vibrator/PerformVendorEffectVibratorStep.java +++ b/services/core/java/com/android/server/vibrator/PerformVendorEffectVibratorStep.java @@ -52,6 +52,7 @@ final class PerformVendorEffectVibratorStep extends AbstractVibratorStep { long vibratorOnResult = controller.on(effect, getVibration().id); vibratorOnResult = Math.min(vibratorOnResult, VENDOR_EFFECT_MAX_DURATION_MS); handleVibratorOnResult(vibratorOnResult); + getVibration().stats.reportPerformVendorEffect(vibratorOnResult); return List.of(new CompleteEffectVibratorStep(conductor, startTime, /* cancelled= */ false, controller, mPendingVibratorOffDeadline)); } finally { diff --git a/services/core/java/com/android/server/vibrator/VibrationScaler.java b/services/core/java/com/android/server/vibrator/VibrationScaler.java index 39337594ff64..a74c4e07c9ed 100644 --- a/services/core/java/com/android/server/vibrator/VibrationScaler.java +++ b/services/core/java/com/android/server/vibrator/VibrationScaler.java @@ -17,7 +17,6 @@ package com.android.server.vibrator; import android.annotation.NonNull; -import android.content.Context; import android.hardware.vibrator.V1_0.EffectStrength; import android.os.ExternalVibrationScale; import android.os.VibrationAttributes; @@ -25,6 +24,7 @@ import android.os.VibrationEffect; import android.os.Vibrator; import android.os.vibrator.Flags; import android.os.vibrator.PrebakedSegment; +import android.os.vibrator.VibrationConfig; import android.util.IndentingPrintWriter; import android.util.Slog; import android.util.SparseArray; @@ -37,8 +37,11 @@ import java.util.Locale; final class VibrationScaler { private static final String TAG = "VibrationScaler"; + // TODO(b/345186129): remove this once we finish migrating to scale factor and clean up flags. // Scale levels. Each level, except MUTE, is defined as the delta between the current setting // and the default intensity for that type of vibration (i.e. current - default). + // It's important that we apply the scaling on the delta between the two so + // that the default intensity level applies no scaling to application provided effects. static final int SCALE_VERY_LOW = ExternalVibrationScale.ScaleLevel.SCALE_VERY_LOW; // -2 static final int SCALE_LOW = ExternalVibrationScale.ScaleLevel.SCALE_LOW; // -1 static final int SCALE_NONE = ExternalVibrationScale.ScaleLevel.SCALE_NONE; // 0 @@ -53,35 +56,15 @@ final class VibrationScaler { private static final float SCALE_FACTOR_HIGH = 1.2f; private static final float SCALE_FACTOR_VERY_HIGH = 1.4f; - private static final ScaleLevel SCALE_LEVEL_NONE = new ScaleLevel(SCALE_FACTOR_NONE); - - // A mapping from the intensity adjustment to the scaling to apply, where the intensity - // adjustment is defined as the delta between the default intensity level and the user selected - // intensity level. It's important that we apply the scaling on the delta between the two so - // that the default intensity level applies no scaling to application provided effects. - private final SparseArray<ScaleLevel> mScaleLevels; private final VibrationSettings mSettingsController; private final int mDefaultVibrationAmplitude; + private final float mDefaultVibrationScaleLevelGain; private final SparseArray<Float> mAdaptiveHapticsScales = new SparseArray<>(); - VibrationScaler(Context context, VibrationSettings settingsController) { + VibrationScaler(VibrationConfig config, VibrationSettings settingsController) { mSettingsController = settingsController; - mDefaultVibrationAmplitude = context.getResources().getInteger( - com.android.internal.R.integer.config_defaultVibrationAmplitude); - - mScaleLevels = new SparseArray<>(); - mScaleLevels.put(SCALE_VERY_LOW, new ScaleLevel(SCALE_FACTOR_VERY_LOW)); - mScaleLevels.put(SCALE_LOW, new ScaleLevel(SCALE_FACTOR_LOW)); - mScaleLevels.put(SCALE_NONE, SCALE_LEVEL_NONE); - mScaleLevels.put(SCALE_HIGH, new ScaleLevel(SCALE_FACTOR_HIGH)); - mScaleLevels.put(SCALE_VERY_HIGH, new ScaleLevel(SCALE_FACTOR_VERY_HIGH)); - } - - /** - * Returns the default vibration amplitude configured for this device, value in [1,255]. - */ - public int getDefaultVibrationAmplitude() { - return mDefaultVibrationAmplitude; + mDefaultVibrationAmplitude = config.getDefaultVibrationAmplitude(); + mDefaultVibrationScaleLevelGain = config.getDefaultVibrationScaleLevelGain(); } /** @@ -111,6 +94,16 @@ final class VibrationScaler { } /** + * Calculates the scale factor to be applied to a vibration with given usage. + * + * @param usageHint one of VibrationAttributes.USAGE_* + * @return The scale factor. + */ + public float getScaleFactor(int usageHint) { + return scaleLevelToScaleFactor(getScaleLevel(usageHint)); + } + + /** * Returns the adaptive haptics scale that should be applied to the vibrations with * the given usage. When no adaptive scales are available for the usages, then returns 1 * indicating no scaling will be applied @@ -135,20 +128,12 @@ final class VibrationScaler { @NonNull public VibrationEffect scale(@NonNull VibrationEffect effect, int usageHint) { int newEffectStrength = getEffectStrength(usageHint); - ScaleLevel scaleLevel = mScaleLevels.get(getScaleLevel(usageHint)); + float scaleFactor = getScaleFactor(usageHint); float adaptiveScale = getAdaptiveHapticsScale(usageHint); - if (scaleLevel == null) { - // Something about our scaling has gone wrong, so just play with no scaling. - Slog.e(TAG, "No configured scaling level found! (current=" - + mSettingsController.getCurrentIntensity(usageHint) + ", default= " - + mSettingsController.getDefaultIntensity(usageHint) + ")"); - scaleLevel = SCALE_LEVEL_NONE; - } - return effect.resolve(mDefaultVibrationAmplitude) .applyEffectStrength(newEffectStrength) - .scale(scaleLevel.factor) + .scale(scaleFactor) .scaleLinearly(adaptiveScale); } @@ -192,14 +177,11 @@ final class VibrationScaler { void dump(IndentingPrintWriter pw) { pw.println("VibrationScaler:"); pw.increaseIndent(); - pw.println("defaultVibrationAmplitude = " + mDefaultVibrationAmplitude); pw.println("ScaleLevels:"); pw.increaseIndent(); - for (int i = 0; i < mScaleLevels.size(); i++) { - int scaleLevelKey = mScaleLevels.keyAt(i); - ScaleLevel scaleLevel = mScaleLevels.valueAt(i); - pw.println(scaleLevelToString(scaleLevelKey) + " = " + scaleLevel); + for (int level = SCALE_VERY_LOW; level <= SCALE_VERY_HIGH; level++) { + pw.println(scaleLevelToString(level) + " = " + scaleLevelToScaleFactor(level)); } pw.decreaseIndent(); @@ -224,16 +206,24 @@ final class VibrationScaler { @Override public String toString() { + StringBuilder scaleLevelsStr = new StringBuilder("{"); + for (int level = SCALE_VERY_LOW; level <= SCALE_VERY_HIGH; level++) { + scaleLevelsStr.append(scaleLevelToString(level)) + .append("=").append(scaleLevelToScaleFactor(level)); + if (level < SCALE_FACTOR_VERY_HIGH) { + scaleLevelsStr.append(", "); + } + } + scaleLevelsStr.append("}"); + return "VibrationScaler{" - + "mScaleLevels=" + mScaleLevels - + ", mDefaultVibrationAmplitude=" + mDefaultVibrationAmplitude + + "mScaleLevels=" + scaleLevelsStr + ", mAdaptiveHapticsScales=" + mAdaptiveHapticsScales + '}'; } private int getEffectStrength(int usageHint) { int currentIntensity = mSettingsController.getCurrentIntensity(usageHint); - if (currentIntensity == Vibrator.VIBRATION_INTENSITY_OFF) { // Bypassing user settings, or it has changed between checking and scaling. Use default. currentIntensity = mSettingsController.getDefaultIntensity(usageHint); @@ -244,17 +234,44 @@ final class VibrationScaler { /** Mapping of Vibrator.VIBRATION_INTENSITY_* values to {@link EffectStrength}. */ private static int intensityToEffectStrength(int intensity) { - switch (intensity) { - case Vibrator.VIBRATION_INTENSITY_LOW: - return EffectStrength.LIGHT; - case Vibrator.VIBRATION_INTENSITY_MEDIUM: - return EffectStrength.MEDIUM; - case Vibrator.VIBRATION_INTENSITY_HIGH: - return EffectStrength.STRONG; - default: + return switch (intensity) { + case Vibrator.VIBRATION_INTENSITY_LOW -> EffectStrength.LIGHT; + case Vibrator.VIBRATION_INTENSITY_MEDIUM -> EffectStrength.MEDIUM; + case Vibrator.VIBRATION_INTENSITY_HIGH -> EffectStrength.STRONG; + default -> { Slog.w(TAG, "Got unexpected vibration intensity: " + intensity); - return EffectStrength.STRONG; + yield EffectStrength.STRONG; + } + }; + } + + /** Mapping of ExternalVibrationScale.ScaleLevel.SCALE_* values to scale factor. */ + private float scaleLevelToScaleFactor(int level) { + if (Flags.hapticsScaleV2Enabled()) { + if (level == SCALE_NONE || level < SCALE_VERY_LOW || level > SCALE_VERY_HIGH) { + // Scale set to none or to a bad value, use default factor for no scaling. + return SCALE_FACTOR_NONE; + } + float scaleFactor = (float) Math.pow(mDefaultVibrationScaleLevelGain, level); + if (scaleFactor <= 0) { + // Something about our scaling has gone wrong, so just play with no scaling. + Slog.wtf(TAG, String.format(Locale.ROOT, "Error in scaling calculations, ended up" + + " with invalid scale factor %.2f for scale level %s and default" + + " level gain of %.2f", scaleFactor, scaleLevelToString(level), + mDefaultVibrationScaleLevelGain)); + scaleFactor = SCALE_FACTOR_NONE; + } + return scaleFactor; } + + return switch (level) { + case SCALE_VERY_LOW -> SCALE_FACTOR_VERY_LOW; + case SCALE_LOW -> SCALE_FACTOR_LOW; + case SCALE_HIGH -> SCALE_FACTOR_HIGH; + case SCALE_VERY_HIGH -> SCALE_FACTOR_VERY_HIGH; + // Scale set to none or to a bad value, use default factor for no scaling. + default -> SCALE_FACTOR_NONE; + }; } static String scaleLevelToString(int scaleLevel) { @@ -267,18 +284,4 @@ final class VibrationScaler { default -> String.valueOf(scaleLevel); }; } - - /** Represents the scale that must be applied to a vibration effect intensity. */ - private static final class ScaleLevel { - public final float factor; - - ScaleLevel(float factor) { - this.factor = factor; - } - - @Override - public String toString() { - return "ScaleLevel{factor=" + factor + "}"; - } - } } diff --git a/services/core/java/com/android/server/vibrator/VibrationSettings.java b/services/core/java/com/android/server/vibrator/VibrationSettings.java index f2f5eda7c05a..0d6778c18759 100644 --- a/services/core/java/com/android/server/vibrator/VibrationSettings.java +++ b/services/core/java/com/android/server/vibrator/VibrationSettings.java @@ -16,7 +16,6 @@ package com.android.server.vibrator; -import static android.os.VibrationAttributes.CATEGORY_KEYBOARD; import static android.os.VibrationAttributes.USAGE_ACCESSIBILITY; import static android.os.VibrationAttributes.USAGE_ALARM; import static android.os.VibrationAttributes.USAGE_COMMUNICATION_REQUEST; @@ -191,8 +190,6 @@ final class VibrationSettings { @GuardedBy("mLock") private boolean mVibrateOn; @GuardedBy("mLock") - private boolean mKeyboardVibrationOn; - @GuardedBy("mLock") private int mRingerMode; @GuardedBy("mLock") private boolean mOnWirelessCharger; @@ -532,14 +529,6 @@ final class VibrationSettings { return false; } - if (mVibrationConfig.isKeyboardVibrationSettingsSupported()) { - int category = callerInfo.attrs.getCategory(); - if (usage == USAGE_TOUCH && category == CATEGORY_KEYBOARD) { - // Keyboard touch has a different user setting. - return mKeyboardVibrationOn; - } - } - // Apply individual user setting based on usage. return getCurrentIntensity(usage) != Vibrator.VIBRATION_INTENSITY_OFF; } @@ -556,10 +545,11 @@ final class VibrationSettings { mVibrateInputDevices = loadSystemSetting(Settings.System.VIBRATE_INPUT_DEVICES, 0, userHandle) > 0; mVibrateOn = loadSystemSetting(Settings.System.VIBRATE_ON, 1, userHandle) > 0; - mKeyboardVibrationOn = loadSystemSetting( - Settings.System.KEYBOARD_VIBRATION_ENABLED, 1, userHandle) > 0; - int keyboardIntensity = getDefaultIntensity(USAGE_IME_FEEDBACK); + boolean isKeyboardVibrationOn = loadSystemSetting( + Settings.System.KEYBOARD_VIBRATION_ENABLED, 1, userHandle) > 0; + int keyboardIntensity = toIntensity(isKeyboardVibrationOn, + getDefaultIntensity(USAGE_IME_FEEDBACK)); int alarmIntensity = toIntensity( loadSystemSetting(Settings.System.ALARM_VIBRATION_INTENSITY, -1, userHandle), getDefaultIntensity(USAGE_ALARM)); @@ -654,7 +644,6 @@ final class VibrationSettings { return "VibrationSettings{" + "mVibratorConfig=" + mVibrationConfig + ", mVibrateOn=" + mVibrateOn - + ", mKeyboardVibrationOn=" + mKeyboardVibrationOn + ", mVibrateInputDevices=" + mVibrateInputDevices + ", mBatterySaverMode=" + mBatterySaverMode + ", mRingerMode=" + ringerModeToString(mRingerMode) @@ -671,7 +660,6 @@ final class VibrationSettings { pw.println("VibrationSettings:"); pw.increaseIndent(); pw.println("vibrateOn = " + mVibrateOn); - pw.println("keyboardVibrationOn = " + mKeyboardVibrationOn); pw.println("vibrateInputDevices = " + mVibrateInputDevices); pw.println("batterySaverMode = " + mBatterySaverMode); pw.println("ringerMode = " + ringerModeToString(mRingerMode)); @@ -698,8 +686,6 @@ final class VibrationSettings { void dump(ProtoOutputStream proto) { synchronized (mLock) { proto.write(VibratorManagerServiceDumpProto.VIBRATE_ON, mVibrateOn); - proto.write(VibratorManagerServiceDumpProto.KEYBOARD_VIBRATION_ON, - mKeyboardVibrationOn); proto.write(VibratorManagerServiceDumpProto.LOW_POWER_MODE, mBatterySaverMode); proto.write(VibratorManagerServiceDumpProto.ALARM_INTENSITY, getCurrentIntensity(USAGE_ALARM)); @@ -774,6 +760,11 @@ final class VibrationSettings { return value; } + @VibrationIntensity + private int toIntensity(boolean enabled, @VibrationIntensity int defaultValue) { + return enabled ? defaultValue : Vibrator.VIBRATION_INTENSITY_OFF; + } + private boolean loadBooleanSetting(String settingKey, int userHandle) { return loadSystemSetting(settingKey, 0, userHandle) != 0; } diff --git a/services/core/java/com/android/server/vibrator/VibrationStats.java b/services/core/java/com/android/server/vibrator/VibrationStats.java index dd66809e7ae6..8179d6aea9ca 100644 --- a/services/core/java/com/android/server/vibrator/VibrationStats.java +++ b/services/core/java/com/android/server/vibrator/VibrationStats.java @@ -79,6 +79,7 @@ final class VibrationStats { private int mVibratorSetAmplitudeCount; private int mVibratorSetExternalControlCount; private int mVibratorPerformCount; + private int mVibratorPerformVendorCount; private int mVibratorComposeCount; private int mVibratorComposePwleCount; @@ -239,6 +240,11 @@ final class VibrationStats { } } + /** Report a call to vibrator method to trigger a vendor vibration effect. */ + void reportPerformVendorEffect(long halResult) { + mVibratorPerformVendorCount++; + } + /** Report a call to vibrator method to trigger a vibration as a composition of primitives. */ void reportComposePrimitives(long halResult, PrimitiveSegment[] primitives) { mVibratorComposeCount++; @@ -313,6 +319,7 @@ final class VibrationStats { public final int halOnCount; public final int halOffCount; public final int halPerformCount; + public final int halPerformVendorCount; public final int halSetAmplitudeCount; public final int halSetExternalControlCount; public final int halCompositionSize; @@ -357,6 +364,7 @@ final class VibrationStats { halOnCount = stats.mVibratorOnCount; halOffCount = stats.mVibratorOffCount; halPerformCount = stats.mVibratorPerformCount; + halPerformVendorCount = stats.mVibratorPerformVendorCount; halSetAmplitudeCount = stats.mVibratorSetAmplitudeCount; halSetExternalControlCount = stats.mVibratorSetExternalControlCount; halCompositionSize = stats.mVibrationCompositionTotalSize; @@ -390,7 +398,8 @@ final class VibrationStats { halOnCount, halOffCount, halPerformCount, halSetAmplitudeCount, halSetExternalControlCount, halSupportedCompositionPrimitivesUsed, halSupportedEffectsUsed, halUnsupportedCompositionPrimitivesUsed, - halUnsupportedEffectsUsed, halCompositionSize, halPwleSize, adaptiveScale); + halUnsupportedEffectsUsed, halCompositionSize, halPwleSize, adaptiveScale, + halPerformVendorCount); } private static int[] filteredKeys(SparseBooleanArray supportArray, boolean supported) { diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java index 7610d7d6b659..f2ad5b95fe5e 100644 --- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java +++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java @@ -57,6 +57,7 @@ import android.os.VibrationEffect; import android.os.VibratorInfo; import android.os.vibrator.Flags; import android.os.vibrator.PrebakedSegment; +import android.os.vibrator.VibrationConfig; import android.os.vibrator.VibrationEffectSegment; import android.os.vibrator.VibratorInfoFactory; import android.os.vibrator.persistence.ParsedVibration; @@ -251,8 +252,9 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { mHandler = injector.createHandler(Looper.myLooper()); mFrameworkStatsLogger = injector.getFrameworkStatsLogger(mHandler); - mVibrationSettings = new VibrationSettings(mContext, mHandler); - mVibrationScaler = new VibrationScaler(mContext, mVibrationSettings); + VibrationConfig vibrationConfig = new VibrationConfig(context.getResources()); + mVibrationSettings = new VibrationSettings(mContext, mHandler, vibrationConfig); + mVibrationScaler = new VibrationScaler(vibrationConfig, mVibrationSettings); mVibratorControlService = new VibratorControlService(mContext, injector.createVibratorControllerHolder(), mVibrationScaler, mVibrationSettings, mFrameworkStatsLogger, mLock); @@ -467,6 +469,14 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { this, flags, privFlags); } + @Override // Binder call + public void performHapticFeedbackForInputDevice(int uid, int deviceId, String opPkg, + int constant, int inputDeviceId, int inputSource, String reason, int flags, + int privFlags) { + performHapticFeedbackForInputDeviceInternal(uid, deviceId, opPkg, constant, inputDeviceId, + inputSource, reason, /* token= */ this, flags, privFlags); + } + /** * An internal-only version of performHapticFeedback that allows the caller access to the * {@link HalVibration}. @@ -501,6 +511,24 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } /** + * An internal-only version of performHapticFeedback that allows the caller access to the + * {@link HalVibration}. + * The Vibration is only returned if it is ongoing after this method returns. + */ + @VisibleForTesting + @Nullable + HalVibration performHapticFeedbackForInputDeviceInternal( + int uid, int deviceId, String opPkg, int constant, int inputDeviceId, int inputSource, + String reason, IBinder token, int flags, int privFlags) { + // TODO(b/355543835): implement input device specific logic. + if (DEBUG) { + Slog.d(TAG, "performHapticFeedbackForInput: input device specific not implemented."); + } + return performHapticFeedbackInternal(uid, deviceId, opPkg, constant, reason, /* token= */ + this, flags, privFlags); + } + + /** * An internal-only version of vibrate that allows the caller access to the * {@link HalVibration}. * The Vibration is only returned if it is ongoing after this method returns. @@ -1672,7 +1700,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { IBinder.DeathRecipient { public final ExternalVibration externalVibration; - public ExternalVibrationScale scale = new ExternalVibrationScale(); + public final ExternalVibrationScale scale = new ExternalVibrationScale(); private Vibration.Status mStatus; @@ -1686,8 +1714,18 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { mStatus = Vibration.Status.RUNNING; } + public void muteScale() { + scale.scaleLevel = ExternalVibrationScale.ScaleLevel.SCALE_MUTE; + if (Flags.hapticsScaleV2Enabled()) { + scale.scaleFactor = 0; + } + } + public void scale(VibrationScaler scaler, int usage) { scale.scaleLevel = scaler.getScaleLevel(usage); + if (Flags.hapticsScaleV2Enabled()) { + scale.scaleFactor = scaler.getScaleFactor(usage); + } scale.adaptiveHapticsScale = scaler.getAdaptiveHapticsScale(usage); stats.reportAdaptiveScale(scale.adaptiveHapticsScale); } @@ -2021,7 +2059,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { // Create Vibration.Stats as close to the received request as possible, for tracking. ExternalVibrationHolder vibHolder = new ExternalVibrationHolder(vib); // Mute the request until we run all the checks and accept the vibration. - vibHolder.scale.scaleLevel = ExternalVibrationScale.ScaleLevel.SCALE_MUTE; + vibHolder.muteScale(); boolean alreadyUnderExternalControl = false; boolean waitForCompletion = false; @@ -2120,7 +2158,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { new Vibration.EndInfo(Vibration.Status.IGNORED_ERROR_CANCELLING), /* continueExternalControl= */ false); // Mute the request, vibration will be ignored. - vibHolder.scale.scaleLevel = ExternalVibrationScale.ScaleLevel.SCALE_MUTE; + vibHolder.muteScale(); } return vibHolder.scale; } diff --git a/services/core/java/com/android/server/wm/AbsAppSnapshotController.java b/services/core/java/com/android/server/wm/AbsAppSnapshotController.java index 68f37380659e..19eba5fe5755 100644 --- a/services/core/java/com/android/server/wm/AbsAppSnapshotController.java +++ b/services/core/java/com/android/server/wm/AbsAppSnapshotController.java @@ -330,6 +330,7 @@ abstract class AbsAppSnapshotController<TYPE extends WindowContainer, builder.setIsTranslucent(isTranslucent); builder.setWindowingMode(source.getWindowingMode()); builder.setAppearance(mainWindow.mAttrs.insetsFlags.appearance); + builder.setUiMode(activity.getConfiguration().uiMode); final Configuration taskConfig = activity.getTask().getConfiguration(); final int displayRotation = taskConfig.windowConfiguration.getDisplayRotation(); @@ -448,7 +449,8 @@ abstract class AbsAppSnapshotController<TYPE extends WindowContainer, mainWindow.getWindowConfiguration().getRotation(), new Point(taskWidth, taskHeight), contentInsets, letterboxInsets, false /* isLowResolution */, false /* isRealSnapshot */, source.getWindowingMode(), - attrs.insetsFlags.appearance, false /* isTranslucent */, false /* hasImeSurface */); + attrs.insetsFlags.appearance, false /* isTranslucent */, false /* hasImeSurface */, + topActivity.getConfiguration().uiMode /* uiMode */); return validateSnapshot(taskSnapshot); } 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/AppCompatController.java b/services/core/java/com/android/server/wm/AppCompatController.java index d38edfc39a8a..42900512de5d 100644 --- a/services/core/java/com/android/server/wm/AppCompatController.java +++ b/services/core/java/com/android/server/wm/AppCompatController.java @@ -40,6 +40,8 @@ class AppCompatController { private final AppCompatOverrides mAppCompatOverrides; @NonNull private final AppCompatDeviceStateQuery mAppCompatDeviceStateQuery; + @NonNull + private final AppCompatLetterboxPolicy mAppCompatLetterboxPolicy; AppCompatController(@NonNull WindowManagerService wmService, @NonNull ActivityRecord activityRecord) { @@ -57,6 +59,7 @@ class AppCompatController { mTransparentPolicy, mAppCompatOverrides); mAppCompatReachabilityPolicy = new AppCompatReachabilityPolicy(mActivityRecord, wmService.mAppCompatConfiguration); + mAppCompatLetterboxPolicy = new AppCompatLetterboxPolicy(mActivityRecord); } @NonNull @@ -113,6 +116,11 @@ class AppCompatController { } @NonNull + AppCompatLetterboxPolicy getAppCompatLetterboxPolicy() { + return mAppCompatLetterboxPolicy; + } + + @NonNull AppCompatFocusOverrides getAppCompatFocusOverrides() { return mAppCompatOverrides.getAppCompatFocusOverrides(); } @@ -127,4 +135,9 @@ class AppCompatController { return mAppCompatDeviceStateQuery; } + @NonNull + AppCompatLetterboxOverrides getAppCompatLetterboxOverrides() { + return mAppCompatOverrides.getAppCompatLetterboxOverrides(); + } + } diff --git a/services/core/java/com/android/server/wm/AppCompatLetterboxOverrides.java b/services/core/java/com/android/server/wm/AppCompatLetterboxOverrides.java new file mode 100644 index 000000000000..24ed14c5398f --- /dev/null +++ b/services/core/java/com/android/server/wm/AppCompatLetterboxOverrides.java @@ -0,0 +1,147 @@ +/* + * 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.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM; +import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME; +import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND; +import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING; +import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_SOLID_COLOR; +import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_WALLPAPER; + +import android.annotation.NonNull; +import android.app.ActivityManager; +import android.graphics.Color; +import android.util.Slog; +import android.view.WindowManager; + +import com.android.server.wm.AppCompatConfiguration.LetterboxBackgroundType; + +/** + * Encapsulates overrides and configuration related to the Letterboxing policy. + */ +class AppCompatLetterboxOverrides { + + private static final String TAG = TAG_WITH_CLASS_NAME ? "AppCompatLetterboxOverrides" : TAG_ATM; + + @NonNull + private final ActivityRecord mActivityRecord; + @NonNull + private final AppCompatConfiguration mAppCompatConfiguration; + + private boolean mShowWallpaperForLetterboxBackground; + + AppCompatLetterboxOverrides(@NonNull ActivityRecord activityRecord, + @NonNull AppCompatConfiguration appCompatConfiguration) { + mActivityRecord = activityRecord; + mAppCompatConfiguration = appCompatConfiguration; + } + + boolean shouldLetterboxHaveRoundedCorners() { + // TODO(b/214030873): remove once background is drawn for transparent activities + // Letterbox shouldn't have rounded corners if the activity is transparent + return mAppCompatConfiguration.isLetterboxActivityCornersRounded() + && mActivityRecord.fillsParent(); + } + + boolean isLetterboxEducationEnabled() { + return mAppCompatConfiguration.getIsEducationEnabled(); + } + + boolean hasWallpaperBackgroundForLetterbox() { + return mShowWallpaperForLetterboxBackground; + } + + boolean checkWallpaperBackgroundForLetterbox(boolean wallpaperShouldBeShown) { + if (mShowWallpaperForLetterboxBackground != wallpaperShouldBeShown) { + mShowWallpaperForLetterboxBackground = wallpaperShouldBeShown; + return true; + } + return false; + } + + @NonNull + Color getLetterboxBackgroundColor() { + final WindowState w = mActivityRecord.findMainWindow(); + if (w == null || w.isLetterboxedForDisplayCutout()) { + return Color.valueOf(Color.BLACK); + } + final @LetterboxBackgroundType int letterboxBackgroundType = + mAppCompatConfiguration.getLetterboxBackgroundType(); + final ActivityManager.TaskDescription taskDescription = mActivityRecord.taskDescription; + switch (letterboxBackgroundType) { + case LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING: + if (taskDescription != null && taskDescription.getBackgroundColorFloating() != 0) { + return Color.valueOf(taskDescription.getBackgroundColorFloating()); + } + break; + case LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND: + if (taskDescription != null && taskDescription.getBackgroundColor() != 0) { + return Color.valueOf(taskDescription.getBackgroundColor()); + } + break; + case LETTERBOX_BACKGROUND_WALLPAPER: + if (hasWallpaperBackgroundForLetterbox()) { + // Color is used for translucent scrim that dims wallpaper. + return mAppCompatConfiguration.getLetterboxBackgroundColor(); + } + Slog.w(TAG, "Wallpaper option is selected for letterbox background but " + + "blur is not supported by a device or not supported in the current " + + "window configuration or both alpha scrim and blur radius aren't " + + "provided so using solid color background"); + break; + case LETTERBOX_BACKGROUND_SOLID_COLOR: + return mAppCompatConfiguration.getLetterboxBackgroundColor(); + default: + throw new AssertionError( + "Unexpected letterbox background type: " + letterboxBackgroundType); + } + // If picked option configured incorrectly or not supported then default to a solid color + // background. + return mAppCompatConfiguration.getLetterboxBackgroundColor(); + } + + int getLetterboxActivityCornersRadius() { + return mAppCompatConfiguration.getLetterboxActivityCornersRadius(); + } + + boolean isLetterboxActivityCornersRounded() { + return mAppCompatConfiguration.isLetterboxActivityCornersRounded(); + } + + @LetterboxBackgroundType + int getLetterboxBackgroundType() { + return mAppCompatConfiguration.getLetterboxBackgroundType(); + } + + int getLetterboxWallpaperBlurRadiusPx() { + int blurRadius = mAppCompatConfiguration.getLetterboxBackgroundWallpaperBlurRadiusPx(); + return Math.max(blurRadius, 0); + } + + float getLetterboxWallpaperDarkScrimAlpha() { + float alpha = mAppCompatConfiguration.getLetterboxBackgroundWallpaperDarkScrimAlpha(); + // No scrim by default. + return (alpha < 0 || alpha >= 1) ? 0.0f : alpha; + } + + boolean isLetterboxWallpaperBlurSupported() { + return mAppCompatConfiguration.mContext.getSystemService(WindowManager.class) + .isCrossWindowBlurEnabled(); + } + +} diff --git a/services/core/java/com/android/server/wm/AppCompatLetterboxPolicy.java b/services/core/java/com/android/server/wm/AppCompatLetterboxPolicy.java new file mode 100644 index 000000000000..48a9311c0374 --- /dev/null +++ b/services/core/java/com/android/server/wm/AppCompatLetterboxPolicy.java @@ -0,0 +1,463 @@ +/* + * 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 android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; +import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING; +import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; + +import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_WALLPAPER; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.graphics.Point; +import android.graphics.Rect; +import android.view.InsetsSource; +import android.view.InsetsState; +import android.view.RoundedCorner; +import android.view.SurfaceControl; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.statusbar.LetterboxDetails; +import com.android.server.wm.AppCompatConfiguration.LetterboxBackgroundType; + +/** + * Encapsulates the logic for the Letterboxing policy. + */ +class AppCompatLetterboxPolicy { + + @NonNull + private final ActivityRecord mActivityRecord; + @NonNull + private final LetterboxPolicyState mLetterboxPolicyState; + + private boolean mLastShouldShowLetterboxUi; + + AppCompatLetterboxPolicy(@NonNull ActivityRecord activityRecord) { + mActivityRecord = activityRecord; + mLetterboxPolicyState = new LetterboxPolicyState(); + } + + /** Cleans up {@link Letterbox} if it exists.*/ + void destroy() { + mLetterboxPolicyState.destroy(); + } + + /** @return {@value true} if the letterbox policy is running and the activity letterboxed. */ + boolean isRunning() { + return mLetterboxPolicyState.isRunning(); + } + + void onMovedToDisplay(int displayId) { + mLetterboxPolicyState.onMovedToDisplay(displayId); + } + + /** Gets the letterbox insets. The insets will be empty if there is no letterbox. */ + @NonNull + Rect getLetterboxInsets() { + return mLetterboxPolicyState.getLetterboxInsets(); + } + + /** Gets the inner bounds of letterbox. The bounds will be empty if there is no letterbox. */ + void getLetterboxInnerBounds(@NonNull Rect outBounds) { + mLetterboxPolicyState.getLetterboxInnerBounds(outBounds); + } + + @Nullable + LetterboxDetails getLetterboxDetails() { + return mLetterboxPolicyState.getLetterboxDetails(); + } + + /** + * @return {@code true} if bar shown within a given rectangle is allowed to be fully transparent + * when the current activity is displayed. + */ + boolean isFullyTransparentBarAllowed(@NonNull Rect rect) { + return mLetterboxPolicyState.isFullyTransparentBarAllowed(rect); + } + + void updateLetterboxSurfaceIfNeeded(@NonNull WindowState winHint, + @NonNull SurfaceControl.Transaction t, + @NonNull SurfaceControl.Transaction inputT) { + mLetterboxPolicyState.updateLetterboxSurfaceIfNeeded(winHint, t, inputT); + } + + void updateLetterboxSurfaceIfNeeded(@NonNull WindowState winHint) { + mLetterboxPolicyState.updateLetterboxSurfaceIfNeeded(winHint, + mActivityRecord.getSyncTransaction(), mActivityRecord.getPendingTransaction()); + } + + void start(@NonNull WindowState w) { + if (shouldNotLayoutLetterbox(w)) { + return; + } + updateRoundedCornersIfNeeded(w); + updateWallpaperForLetterbox(w); + if (shouldShowLetterboxUi(w)) { + mLetterboxPolicyState.layoutLetterboxIfNeeded(w); + } else { + mLetterboxPolicyState.hide(); + } + } + + @VisibleForTesting + boolean shouldShowLetterboxUi(@NonNull WindowState mainWindow) { + if (mActivityRecord.mAppCompatController.getAppCompatOrientationOverrides() + .getIsRelaunchingAfterRequestedOrientationChanged()) { + return mLastShouldShowLetterboxUi; + } + + final boolean shouldShowLetterboxUi = + (mActivityRecord.isInLetterboxAnimation() || mActivityRecord.isVisible() + || mActivityRecord.isVisibleRequested()) + && mainWindow.areAppWindowBoundsLetterboxed() + // Check for FLAG_SHOW_WALLPAPER explicitly instead of using + // WindowContainer#showWallpaper because the later will return true when + // this activity is using blurred wallpaper for letterbox background. + && (mainWindow.getAttrs().flags & FLAG_SHOW_WALLPAPER) == 0; + + mLastShouldShowLetterboxUi = shouldShowLetterboxUi; + + return shouldShowLetterboxUi; + } + + @VisibleForTesting + @Nullable + Rect getCropBoundsIfNeeded(@NonNull final WindowState mainWindow) { + if (!requiresRoundedCorners(mainWindow) || mActivityRecord.isInLetterboxAnimation()) { + // We don't want corner radius on the window. + // In the case the ActivityRecord requires a letterboxed animation we never want + // rounded corners on the window because rounded corners are applied at the + // animation-bounds surface level and rounded corners on the window would interfere + // with that leading to unexpected rounded corner positioning during the animation. + return null; + } + + final Rect cropBounds = new Rect(mActivityRecord.getBounds()); + + // In case of translucent activities we check if the requested size is different from + // the size provided using inherited bounds. In that case we decide to not apply rounded + // corners because we assume the specific layout would. This is the case when the layout + // of the translucent activity uses only a part of all the bounds because of the use of + // LayoutParams.WRAP_CONTENT. + final TransparentPolicy transparentPolicy = mActivityRecord.mAppCompatController + .getTransparentPolicy(); + if (transparentPolicy.isRunning() && (cropBounds.width() != mainWindow.mRequestedWidth + || cropBounds.height() != mainWindow.mRequestedHeight)) { + return null; + } + + // It is important to call {@link #adjustBoundsIfNeeded} before {@link cropBounds.offsetTo} + // because taskbar bounds used in {@link #adjustBoundsIfNeeded} + // are in screen coordinates + adjustBoundsForTaskbar(mainWindow, cropBounds); + + final float scale = mainWindow.mInvGlobalScale; + if (scale != 1f && scale > 0f) { + cropBounds.scale(scale); + } + + // ActivityRecord bounds are in screen coordinates while (0,0) for activity's surface + // control is in the top left corner of an app window so offsetting bounds + // accordingly. + cropBounds.offsetTo(0, 0); + return cropBounds; + } + + + // Returns rounded corners radius the letterboxed activity should have based on override in + // R.integer.config_letterboxActivityCornersRadius or min device bottom corner radii. + // Device corners can be different on the right and left sides, but we use the same radius + // for all corners for consistency and pick a minimal bottom one for consistency with a + // taskbar rounded corners. + int getRoundedCornersRadius(@NonNull final WindowState mainWindow) { + if (!requiresRoundedCorners(mainWindow)) { + return 0; + } + final AppCompatLetterboxOverrides letterboxOverrides = mActivityRecord + .mAppCompatController.getAppCompatLetterboxOverrides(); + final int radius; + if (letterboxOverrides.getLetterboxActivityCornersRadius() >= 0) { + radius = letterboxOverrides.getLetterboxActivityCornersRadius(); + } else { + final InsetsState insetsState = mainWindow.getInsetsState(); + radius = Math.min( + getInsetsStateCornerRadius(insetsState, RoundedCorner.POSITION_BOTTOM_LEFT), + getInsetsStateCornerRadius(insetsState, RoundedCorner.POSITION_BOTTOM_RIGHT)); + } + + final float scale = mainWindow.mInvGlobalScale; + return (scale != 1f && scale > 0f) ? (int) (scale * radius) : radius; + } + + void adjustBoundsForTaskbar(@NonNull final WindowState mainWindow, + @NonNull final Rect bounds) { + // Rounded corners should be displayed above the taskbar. When taskbar is hidden, + // an insets frame is equal to a navigation bar which shouldn't affect position of + // rounded corners since apps are expected to handle navigation bar inset. + // This condition checks whether the taskbar is visible. + // Do not crop the taskbar inset if the window is in immersive mode - the user can + // swipe to show/hide the taskbar as an overlay. + // Adjust the bounds only in case there is an expanded taskbar, + // otherwise the rounded corners will be shown behind the navbar. + final InsetsSource expandedTaskbarOrNull = + AppCompatUtils.getExpandedTaskbarOrNull(mainWindow); + if (expandedTaskbarOrNull != null) { + // Rounded corners should be displayed above the expanded taskbar. + bounds.bottom = Math.min(bounds.bottom, expandedTaskbarOrNull.getFrame().top); + } + } + + private int getInsetsStateCornerRadius(@NonNull InsetsState insetsState, + @RoundedCorner.Position int position) { + final RoundedCorner corner = insetsState.getRoundedCorners().getRoundedCorner(position); + return corner == null ? 0 : corner.getRadius(); + } + + private void updateWallpaperForLetterbox(@NonNull WindowState mainWindow) { + final AppCompatLetterboxOverrides letterboxOverrides = mActivityRecord + .mAppCompatController.getAppCompatLetterboxOverrides(); + final @LetterboxBackgroundType int letterboxBackgroundType = + letterboxOverrides.getLetterboxBackgroundType(); + boolean wallpaperShouldBeShown = + letterboxBackgroundType == LETTERBOX_BACKGROUND_WALLPAPER + // Don't use wallpaper as a background if letterboxed for display cutout. + && isLetterboxedNotForDisplayCutout(mainWindow) + // Check that dark scrim alpha or blur radius are provided + && (letterboxOverrides.getLetterboxWallpaperBlurRadiusPx() > 0 + || letterboxOverrides.getLetterboxWallpaperDarkScrimAlpha() > 0) + // Check that blur is supported by a device if blur radius is provided. + && (letterboxOverrides.getLetterboxWallpaperBlurRadiusPx() <= 0 + || letterboxOverrides.isLetterboxWallpaperBlurSupported()); + if (letterboxOverrides.checkWallpaperBackgroundForLetterbox(wallpaperShouldBeShown)) { + mActivityRecord.requestUpdateWallpaperIfNeeded(); + } + } + + void updateRoundedCornersIfNeeded(@NonNull final WindowState mainWindow) { + final SurfaceControl windowSurface = mainWindow.getSurfaceControl(); + if (windowSurface == null || !windowSurface.isValid()) { + return; + } + + // cropBounds must be non-null for the cornerRadius to be ever applied. + mActivityRecord.getSyncTransaction() + .setCrop(windowSurface, getCropBoundsIfNeeded(mainWindow)) + .setCornerRadius(windowSurface, getRoundedCornersRadius(mainWindow)); + } + + private boolean requiresRoundedCorners(@NonNull final WindowState mainWindow) { + final AppCompatLetterboxOverrides letterboxOverrides = mActivityRecord + .mAppCompatController.getAppCompatLetterboxOverrides(); + return isLetterboxedNotForDisplayCutout(mainWindow) + && letterboxOverrides.isLetterboxActivityCornersRounded(); + } + + private boolean isLetterboxedNotForDisplayCutout(@NonNull WindowState mainWindow) { + return shouldShowLetterboxUi(mainWindow) + && !mainWindow.isLetterboxedForDisplayCutout(); + } + + private static boolean shouldNotLayoutLetterbox(@Nullable WindowState w) { + if (w == null) { + return true; + } + final int type = w.mAttrs.type; + // Allow letterbox to be displayed early for base application or application starting + // windows even if it is not on the top z order to prevent flickering when the + // letterboxed window is brought to the top + return (type != TYPE_BASE_APPLICATION && type != TYPE_APPLICATION_STARTING) + || w.mAnimatingExit; + } + + private class LetterboxPolicyState { + + @Nullable + private Letterbox mLetterbox; + + void layoutLetterboxIfNeeded(@NonNull WindowState w) { + if (!isRunning()) { + final AppCompatLetterboxOverrides letterboxOverrides = mActivityRecord + .mAppCompatController.getAppCompatLetterboxOverrides(); + final AppCompatReachabilityPolicy reachabilityPolicy = mActivityRecord + .mAppCompatController.getAppCompatReachabilityPolicy(); + mLetterbox = new Letterbox(() -> mActivityRecord.makeChildSurface(null), + mActivityRecord.mWmService.mTransactionFactory, + reachabilityPolicy, letterboxOverrides, + this::getLetterboxParentSurface); + mLetterbox.attachInput(w); + mActivityRecord.mAppCompatController.getAppCompatReachabilityPolicy() + .setLetterboxInnerBoundsSupplier(mLetterbox::getInnerFrame); + } + final Point letterboxPosition = new Point(); + if (mActivityRecord.isInLetterboxAnimation()) { + // In this case we attach the letterbox to the task instead of the activity. + mActivityRecord.getTask().getPosition(letterboxPosition); + } else { + mActivityRecord.getPosition(letterboxPosition); + } + + // Get the bounds of the "space-to-fill". The transformed bounds have the highest + // priority because the activity is launched in a rotated environment. In multi-window + // mode, the taskFragment-level represents this for both split-screen + // and activity-embedding. In fullscreen-mode, the task container does + // (since the orientation letterbox is also applied to the task). + final Rect transformedBounds = mActivityRecord.getFixedRotationTransformDisplayBounds(); + final Rect spaceToFill = transformedBounds != null + ? transformedBounds + : mActivityRecord.inMultiWindowMode() + ? mActivityRecord.getTaskFragment().getBounds() + : mActivityRecord.getRootTask().getParent().getBounds(); + // In case of translucent activities an option is to use the WindowState#getFrame() of + // the first opaque activity beneath. In some cases (e.g. an opaque activity is using + // non MATCH_PARENT layouts or a Dialog theme) this might not provide the correct + // information and in particular it might provide a value for a smaller area making + // the letterbox overlap with the translucent activity's frame. + // If we use WindowState#getFrame() for the translucent activity's letterbox inner + // frame, the letterbox will then be overlapped with the translucent activity's frame. + // Because the surface layer of letterbox is lower than an activity window, this + // won't crop the content, but it may affect other features that rely on values stored + // in mLetterbox, e.g. transitions, a status bar scrim and recents preview in Launcher + // For this reason we use ActivityRecord#getBounds() that the translucent activity + // inherits from the first opaque activity beneath and also takes care of the scaling + // in case of activities in size compat mode. + final TransparentPolicy transparentPolicy = + mActivityRecord.mAppCompatController.getTransparentPolicy(); + final Rect innerFrame = + transparentPolicy.isRunning() ? mActivityRecord.getBounds() : w.getFrame(); + mLetterbox.layout(spaceToFill, innerFrame, letterboxPosition); + if (mActivityRecord.mAppCompatController.getAppCompatReachabilityOverrides() + .isDoubleTapEvent()) { + // We need to notify Shell that letterbox position has changed. + mActivityRecord.getTask().dispatchTaskInfoChangedIfNeeded(true /* force */); + } + } + + /** + * @return {@code true} if the policy is running and so if the current activity is + * letterboxed. + */ + boolean isRunning() { + return mLetterbox != null; + } + + void onMovedToDisplay(int displayId) { + if (isRunning()) { + mLetterbox.onMovedToDisplay(displayId); + } + } + + /** Cleans up {@link Letterbox} if it exists.*/ + void destroy() { + if (isRunning()) { + mLetterbox.destroy(); + mLetterbox = null; + } + mActivityRecord.mAppCompatController.getAppCompatReachabilityPolicy() + .setLetterboxInnerBoundsSupplier(null); + } + + void updateLetterboxSurfaceIfNeeded(@NonNull WindowState winHint, + @NonNull SurfaceControl.Transaction t, + @NonNull SurfaceControl.Transaction inputT) { + if (shouldNotLayoutLetterbox(winHint)) { + return; + } + start(winHint); + if (isRunning() && mLetterbox.needsApplySurfaceChanges()) { + mLetterbox.applySurfaceChanges(t, inputT); + } + } + + void hide() { + if (isRunning()) { + mLetterbox.hide(); + } + } + + /** Gets the letterbox insets. The insets will be empty if there is no letterbox. */ + @NonNull + Rect getLetterboxInsets() { + if (isRunning()) { + return mLetterbox.getInsets(); + } else { + return new Rect(); + } + } + + /** Gets the inner bounds of letterbox. The bounds will be empty with no letterbox. */ + void getLetterboxInnerBounds(@NonNull Rect outBounds) { + if (isRunning()) { + outBounds.set(mLetterbox.getInnerFrame()); + final WindowState w = mActivityRecord.findMainWindow(); + if (w != null) { + adjustBoundsForTaskbar(w, outBounds); + } + } else { + outBounds.setEmpty(); + } + } + + /** Gets the outer bounds of letterbox. The bounds will be empty with no letterbox. */ + private void getLetterboxOuterBounds(@NonNull Rect outBounds) { + if (isRunning()) { + outBounds.set(mLetterbox.getOuterFrame()); + } else { + outBounds.setEmpty(); + } + } + + /** + * @return {@code true} if bar shown within a given rectangle is allowed to be fully + * transparent when the current activity is displayed. + */ + boolean isFullyTransparentBarAllowed(@NonNull Rect rect) { + return !isRunning() || mLetterbox.notIntersectsOrFullyContains(rect); + } + + @Nullable + LetterboxDetails getLetterboxDetails() { + final WindowState w = mActivityRecord.findMainWindow(); + if (!isRunning() || w == null || w.isLetterboxedForDisplayCutout()) { + return null; + } + final Rect letterboxInnerBounds = new Rect(); + final Rect letterboxOuterBounds = new Rect(); + getLetterboxInnerBounds(letterboxInnerBounds); + getLetterboxOuterBounds(letterboxOuterBounds); + + if (letterboxInnerBounds.isEmpty() || letterboxOuterBounds.isEmpty()) { + return null; + } + + return new LetterboxDetails( + letterboxInnerBounds, + letterboxOuterBounds, + w.mAttrs.insetsFlags.appearance + ); + } + + @Nullable + private SurfaceControl getLetterboxParentSurface() { + if (mActivityRecord.isInLetterboxAnimation()) { + return mActivityRecord.getTask().getSurfaceControl(); + } + return mActivityRecord.getSurfaceControl(); + } + + } +} diff --git a/services/core/java/com/android/server/wm/AppCompatOverrides.java b/services/core/java/com/android/server/wm/AppCompatOverrides.java index 80bbee3dd78d..2f03105846bd 100644 --- a/services/core/java/com/android/server/wm/AppCompatOverrides.java +++ b/services/core/java/com/android/server/wm/AppCompatOverrides.java @@ -37,6 +37,8 @@ public class AppCompatOverrides { private final AppCompatResizeOverrides mAppCompatResizeOverrides; @NonNull private final AppCompatReachabilityOverrides mAppCompatReachabilityOverrides; + @NonNull + private final AppCompatLetterboxOverrides mAppCompatLetterboxOverrides; AppCompatOverrides(@NonNull ActivityRecord activityRecord, @NonNull AppCompatConfiguration appCompatConfiguration, @@ -54,6 +56,8 @@ public class AppCompatOverrides { mAppCompatFocusOverrides = new AppCompatFocusOverrides(activityRecord, appCompatConfiguration, optPropBuilder); mAppCompatResizeOverrides = new AppCompatResizeOverrides(activityRecord, optPropBuilder); + mAppCompatLetterboxOverrides = new AppCompatLetterboxOverrides(activityRecord, + appCompatConfiguration); } @NonNull @@ -85,4 +89,9 @@ public class AppCompatOverrides { AppCompatReachabilityOverrides getAppCompatReachabilityOverrides() { return mAppCompatReachabilityOverrides; } + + @NonNull + AppCompatLetterboxOverrides getAppCompatLetterboxOverrides() { + return mAppCompatLetterboxOverrides; + } } 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/AppCompatUtils.java b/services/core/java/com/android/server/wm/AppCompatUtils.java index 0244d27f4363..91205fc757ad 100644 --- a/services/core/java/com/android/server/wm/AppCompatUtils.java +++ b/services/core/java/com/android/server/wm/AppCompatUtils.java @@ -28,6 +28,9 @@ import android.app.CameraCompatTaskInfo; import android.app.TaskInfo; import android.content.res.Configuration; import android.graphics.Rect; +import android.view.InsetsSource; +import android.view.InsetsState; +import android.view.WindowInsets; import java.util.function.BooleanSupplier; @@ -212,6 +215,23 @@ class AppCompatUtils { return "UNKNOWN_REASON"; } + /** + * Returns the taskbar in case it is visible and expanded in height, otherwise returns null. + */ + @Nullable + static InsetsSource getExpandedTaskbarOrNull(@NonNull final WindowState mainWindow) { + final InsetsState state = mainWindow.getInsetsState(); + for (int i = state.sourceSize() - 1; i >= 0; i--) { + final InsetsSource source = state.sourceAt(i); + if (source.getType() == WindowInsets.Type.navigationBars() + && source.hasFlags(InsetsSource.FLAG_INSETS_ROUNDED_CORNER) + && source.isVisible()) { + return source; + } + } + return null; + } + private static void clearAppCompatTaskInfo(@NonNull AppCompatTaskInfo info) { info.topActivityLetterboxVerticalPosition = TaskInfo.PROPERTY_VALUE_UNSET; info.topActivityLetterboxHorizontalPosition = TaskInfo.PROPERTY_VALUE_UNSET; diff --git a/services/core/java/com/android/server/wm/AppSnapshotLoader.java b/services/core/java/com/android/server/wm/AppSnapshotLoader.java index ed65a2b2f8e6..5b697e518d86 100644 --- a/services/core/java/com/android/server/wm/AppSnapshotLoader.java +++ b/services/core/java/com/android/server/wm/AppSnapshotLoader.java @@ -203,7 +203,7 @@ class AppSnapshotLoader { new Rect(proto.letterboxInsetLeft, proto.letterboxInsetTop, proto.letterboxInsetRight, proto.letterboxInsetBottom), loadLowResolutionBitmap, proto.isRealSnapshot, proto.windowingMode, - proto.appearance, proto.isTranslucent, false /* hasImeSurface */); + proto.appearance, proto.isTranslucent, false /* hasImeSurface */, proto.uiMode); } catch (IOException e) { Slog.w(TAG, "Unable to load task snapshot data for Id=" + id); return null; diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java index 48e107931913..fe5b142289fc 100644 --- a/services/core/java/com/android/server/wm/BackNavigationController.java +++ b/services/core/java/com/android/server/wm/BackNavigationController.java @@ -761,13 +761,55 @@ class BackNavigationController { if (isMonitorForRemote()) { mObserver.sendResult(null /* result */); } - if (isMonitorAnimationOrTransition()) { + if (isMonitorAnimationOrTransition() && canCancelAnimations()) { clearBackAnimations(true /* cancel */); } cancelPendingAnimation(); } } + 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,42 @@ 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)) { + transition.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<>(); @@ -1887,18 +1990,12 @@ class BackNavigationController { activity.makeVisibleIfNeeded(null /* starting */, true /* notifyToClient */); } } - boolean needTransition = false; - final DisplayContent dc = affects.get(0).getDisplayContent(); - for (int i = affects.size() - 1; i >= 0; --i) { - final ActivityRecord activity = affects.get(i); - needTransition |= tc.isCollecting(activity); - } if (prepareOpen != null) { - if (needTransition) { + if (prepareOpen.hasChanges()) { tc.requestStartTransition(prepareOpen, null /*startTask */, null /* remoteTransition */, null /* displayChange */); - tc.setReady(dc); + prepareOpen.setReady(affects.get(0), true); return prepareOpen; } else { prepareOpen.abort(); @@ -1919,9 +2016,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. @@ -1946,11 +2046,22 @@ class BackNavigationController { } } + /** If the open transition is playing, wait for transition to clear the animation */ + private boolean canCancelAnimations() { + if (!Flags.migratePredictiveBackTransition()) { + return true; + } + return mAnimationHandler.mOpenAnimAdaptor == null + || mAnimationHandler.mOpenAnimAdaptor.mPreparedOpenTransition == null; + } + void startAnimation() { if (!mBackAnimationInProgress) { // gesture is already finished, do not start animation if (mPendingAnimation != null) { - clearBackAnimations(true /* cancel */); + if (canCancelAnimations()) { + clearBackAnimations(true /* cancel */); + } mPendingAnimation = null; } return; @@ -2015,7 +2126,7 @@ class BackNavigationController { return isSnapshotCompatible(snapshot, visibleOpenActivities) ? snapshot : null; } - static boolean isSnapshotCompatible(@NonNull TaskSnapshot snapshot, + static boolean isSnapshotCompatible(@Nullable TaskSnapshot snapshot, @NonNull ActivityRecord[] visibleOpenActivities) { if (snapshot == null) { return false; @@ -2026,6 +2137,12 @@ class BackNavigationController { if (!ar.isSnapshotOrientationCompatible(snapshot)) { return false; } + final int appNightMode = ar.getConfiguration().uiMode + & Configuration.UI_MODE_NIGHT_MASK; + final int snapshotNightMode = snapshot.getUiMode() & Configuration.UI_MODE_NIGHT_MASK; + if (appNightMode != snapshotNightMode) { + return false; + } oneComponentMatch |= ar.isSnapshotComponentCompatible(snapshot); } return oneComponentMatch; 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/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 9c8c759765bc..fcc6b11d46c5 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -1804,9 +1804,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp return; } final int displayRotation = getRotation(); - final int rotation = ar.isVisible() - ? ar.getWindowConfiguration().getDisplayRotation() - : mDisplayRotation.rotationForOrientation(orientation, displayRotation); + final int rotation = mDisplayRotation.rotationForOrientation(orientation, displayRotation); if (rotation == displayRotation) { return; } @@ -6710,6 +6708,11 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp final boolean rotationChanged = super.setIgnoreOrientationRequest(ignoreOrientationRequest); mWmService.mDisplayWindowSettings.setIgnoreOrientationRequest( this, mSetIgnoreOrientationRequest); + if (ignoreOrientationRequest && mWmService.mFlags.mRespectNonTopVisibleFixedOrientation) { + forAllActivities(r -> { + r.finishFixedRotationTransform(); + }); + } return rotationChanged; } diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java index 8272e1609e0d..a5da5e7cc0de 100644 --- a/services/core/java/com/android/server/wm/DisplayRotation.java +++ b/services/core/java/com/android/server/wm/DisplayRotation.java @@ -1239,7 +1239,6 @@ public class DisplayRotation { * @param lastRotation The most recently used rotation. * @return The surface rotation to use. */ - @VisibleForTesting @Surface.Rotation int rotationForOrientation(@ScreenOrientation int orientation, @Surface.Rotation int lastRotation) { diff --git a/services/core/java/com/android/server/wm/DisplayWindowSettings.java b/services/core/java/com/android/server/wm/DisplayWindowSettings.java index 7a95c2d6d934..2f0ee171b5ba 100644 --- a/services/core/java/com/android/server/wm/DisplayWindowSettings.java +++ b/services/core/java/com/android/server/wm/DisplayWindowSettings.java @@ -129,21 +129,33 @@ class DisplayWindowSettings { @WindowConfiguration.WindowingMode private int getWindowingModeLocked(@NonNull SettingsProvider.SettingsEntry settings, @NonNull DisplayContent dc) { - int windowingMode = settings.mWindowingMode; + final int windowingModeFromDisplaySettings = settings.mWindowingMode; // This display used to be in freeform, but we don't support freeform anymore, so fall // back to fullscreen. - if (windowingMode == WindowConfiguration.WINDOWING_MODE_FREEFORM + if (windowingModeFromDisplaySettings == WindowConfiguration.WINDOWING_MODE_FREEFORM && !mService.mAtmService.mSupportsFreeformWindowManagement) { return WindowConfiguration.WINDOWING_MODE_FULLSCREEN; } + if (windowingModeFromDisplaySettings != WindowConfiguration.WINDOWING_MODE_UNDEFINED) { + return windowingModeFromDisplaySettings; + } // No record is present so use default windowing mode policy. - if (windowingMode == WindowConfiguration.WINDOWING_MODE_UNDEFINED) { - windowingMode = mService.mAtmService.mSupportsFreeformWindowManagement - && (mService.mIsPc || dc.forceDesktopMode()) - ? WindowConfiguration.WINDOWING_MODE_FREEFORM - : WindowConfiguration.WINDOWING_MODE_FULLSCREEN; + final boolean forceFreeForm = mService.mAtmService.mSupportsFreeformWindowManagement + && (mService.mIsPc || dc.forceDesktopMode()); + if (forceFreeForm) { + return WindowConfiguration.WINDOWING_MODE_FREEFORM; + } + final int currentWindowingMode = dc.getDefaultTaskDisplayArea().getWindowingMode(); + if (currentWindowingMode == WindowConfiguration.WINDOWING_MODE_UNDEFINED) { + // No record preset in settings + no mode set via the display area policy. + // Move to fullscreen as a fallback. + return WindowConfiguration.WINDOWING_MODE_FULLSCREEN; + } + if (currentWindowingMode == WindowConfiguration.WINDOWING_MODE_FREEFORM) { + // Freeform was enabled before but disabled now, the TDA should now move to fullscreen. + return WindowConfiguration.WINDOWING_MODE_FULLSCREEN; } - return windowingMode; + return currentWindowingMode; } @WindowConfiguration.WindowingMode diff --git a/services/core/java/com/android/server/wm/KeyguardController.java b/services/core/java/com/android/server/wm/KeyguardController.java index 7a0fd3e34fdd..5d8a96c530ef 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; @@ -50,6 +51,7 @@ import static com.android.server.wm.KeyguardControllerProto.KEYGUARD_SHOWING; import android.annotation.Nullable; import android.os.IBinder; import android.os.RemoteException; +import android.os.SystemClock; import android.os.Trace; import android.util.Slog; import android.util.SparseArray; @@ -77,6 +79,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 +236,7 @@ class KeyguardController { dc.mWallpaperController.adjustWallpaperWindows(); dc.executeAppTransition(); } + scheduleGoingAwayTimeout(displayId); } // Update the sleep token first such that ensureActivitiesVisible has correct sleep token @@ -286,6 +291,8 @@ class KeyguardController { mRootWindowContainer.ensureActivitiesVisible(); mRootWindowContainer.addStartingWindowsForVisibleActivities(); mWindowManager.executeAppTransition(); + + scheduleGoingAwayTimeout(displayId); } finally { mService.continueWindowLayout(); Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); @@ -417,31 +424,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 +469,8 @@ class KeyguardController { } } updateKeyguardSleepToken(displayId); - if (performTransition && executeTransition) { - mWindowManager.executeAppTransition(); + if (executeTransition) { + dc.executeAppTransition(); } } finally { mService.continueWindowLayout(); @@ -590,6 +608,35 @@ 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( + SystemClock.uptimeMillis() - GOING_AWAY_TIMEOUT_MS); + } + }; + + 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 +756,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/Letterbox.java b/services/core/java/com/android/server/wm/Letterbox.java index 3fc5eafc8737..252590e0b696 100644 --- a/services/core/java/com/android/server/wm/Letterbox.java +++ b/services/core/java/com/android/server/wm/Letterbox.java @@ -38,9 +38,6 @@ import android.view.WindowManager; import com.android.server.UiThread; -import java.util.function.BooleanSupplier; -import java.util.function.DoubleSupplier; -import java.util.function.IntSupplier; import java.util.function.Supplier; /** @@ -54,12 +51,6 @@ public class Letterbox { private final Supplier<SurfaceControl.Builder> mSurfaceControlFactory; private final Supplier<SurfaceControl.Transaction> mTransactionFactory; - private final BooleanSupplier mAreCornersRounded; - private final Supplier<Color> mColorSupplier; - // Parameters for "blurred wallpaper" letterbox background. - private final BooleanSupplier mHasWallpaperBackgroundSupplier; - private final IntSupplier mBlurRadiusSupplier; - private final DoubleSupplier mDarkScrimAlphaSupplier; private final Supplier<SurfaceControl> mParentSurfaceSupplier; private final Rect mOuter = new Rect(); @@ -77,6 +68,8 @@ public class Letterbox { private final LetterboxSurface[] mSurfaces = { mLeft, mTop, mRight, mBottom }; @NonNull private final AppCompatReachabilityPolicy mAppCompatReachabilityPolicy; + @NonNull + private final AppCompatLetterboxOverrides mAppCompatLetterboxOverrides; /** * Constructs a Letterbox. @@ -85,24 +78,14 @@ public class Letterbox { */ public Letterbox(Supplier<SurfaceControl.Builder> surfaceControlFactory, Supplier<SurfaceControl.Transaction> transactionFactory, - BooleanSupplier areCornersRounded, - Supplier<Color> colorSupplier, - BooleanSupplier hasWallpaperBackgroundSupplier, - IntSupplier blurRadiusSupplier, - DoubleSupplier darkScrimAlphaSupplier, @NonNull AppCompatReachabilityPolicy appCompatReachabilityPolicy, + @NonNull AppCompatLetterboxOverrides appCompatLetterboxOverrides, Supplier<SurfaceControl> parentSurface) { mSurfaceControlFactory = surfaceControlFactory; mTransactionFactory = transactionFactory; - mAreCornersRounded = areCornersRounded; - mColorSupplier = colorSupplier; - mHasWallpaperBackgroundSupplier = hasWallpaperBackgroundSupplier; - mBlurRadiusSupplier = blurRadiusSupplier; - mDarkScrimAlphaSupplier = darkScrimAlphaSupplier; mAppCompatReachabilityPolicy = appCompatReachabilityPolicy; + mAppCompatLetterboxOverrides = appCompatLetterboxOverrides; mParentSurfaceSupplier = parentSurface; - // TODO Remove after Letterbox refactoring. - mAppCompatReachabilityPolicy.setLetterboxInnerBoundsSupplier(this::getInnerFrame); } /** @@ -252,7 +235,8 @@ public class Letterbox { * Returns {@code true} when using {@link #mFullWindowSurface} instead of {@link mSurfaces}. */ private boolean useFullWindowSurface() { - return mAreCornersRounded.getAsBoolean() || mHasWallpaperBackgroundSupplier.getAsBoolean(); + return mAppCompatLetterboxOverrides.shouldLetterboxHaveRoundedCorners() + || mAppCompatLetterboxOverrides.hasWallpaperBackgroundForLetterbox(); } private final class TapEventReceiver extends InputEventReceiver { @@ -431,7 +415,7 @@ public class Letterbox { createSurface(t); } - mColor = mColorSupplier.get(); + mColor = mAppCompatLetterboxOverrides.getLetterboxBackgroundColor(); mParentSurface = mParentSurfaceSupplier.get(); t.setColor(mSurface, getRgbColorArray()); t.setPosition(mSurface, mSurfaceFrameRelative.left, mSurfaceFrameRelative.top); @@ -439,7 +423,8 @@ public class Letterbox { mSurfaceFrameRelative.height()); t.reparent(mSurface, mParentSurface); - mHasWallpaperBackground = mHasWallpaperBackgroundSupplier.getAsBoolean(); + mHasWallpaperBackground = mAppCompatLetterboxOverrides + .hasWallpaperBackgroundForLetterbox(); updateAlphaAndBlur(t); t.show(mSurface); @@ -460,17 +445,19 @@ public class Letterbox { t.setBackgroundBlurRadius(mSurface, 0); return; } - final float alpha = (float) mDarkScrimAlphaSupplier.getAsDouble(); + final float alpha = mAppCompatLetterboxOverrides.getLetterboxWallpaperDarkScrimAlpha(); t.setAlpha(mSurface, alpha); // Translucent dark scrim can be shown without blur. - if (mBlurRadiusSupplier.getAsInt() <= 0) { + final int blurRadiusPx = mAppCompatLetterboxOverrides + .getLetterboxWallpaperBlurRadiusPx(); + if (blurRadiusPx <= 0) { // Removing pre-exesting blur t.setBackgroundBlurRadius(mSurface, 0); return; } - t.setBackgroundBlurRadius(mSurface, mBlurRadiusSupplier.getAsInt()); + t.setBackgroundBlurRadius(mSurface, blurRadiusPx); } private float[] getRgbColorArray() { @@ -487,8 +474,9 @@ public class Letterbox { // and mParentSurface may never be updated in applySurfaceChanges but this // doesn't mean that update is needed. || !mSurfaceFrameRelative.isEmpty() - && (mHasWallpaperBackgroundSupplier.getAsBoolean() != mHasWallpaperBackground - || !mColorSupplier.get().equals(mColor) + && (mAppCompatLetterboxOverrides.hasWallpaperBackgroundForLetterbox() + != mHasWallpaperBackground + || !mAppCompatLetterboxOverrides.getLetterboxBackgroundColor().equals(mColor) || mParentSurfaceSupplier.get() != mParentSurface); } } diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java index 38df1b0e0511..0e33734fc7a5 100644 --- a/services/core/java/com/android/server/wm/LetterboxUiController.java +++ b/services/core/java/com/android/server/wm/LetterboxUiController.java @@ -16,36 +16,19 @@ package com.android.server.wm; -import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; -import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING; -import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; - import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM; import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME; -import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND; -import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING; -import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_SOLID_COLOR; import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_WALLPAPER; import static com.android.server.wm.AppCompatConfiguration.letterboxBackgroundTypeToString; import android.annotation.NonNull; import android.annotation.Nullable; -import android.app.ActivityManager.TaskDescription; import android.graphics.Color; -import android.graphics.Point; import android.graphics.Rect; -import android.util.Slog; -import android.view.InsetsSource; -import android.view.InsetsState; -import android.view.RoundedCorner; -import android.view.SurfaceControl; import android.view.SurfaceControl.Transaction; -import android.view.WindowInsets; -import android.view.WindowManager; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.statusbar.LetterboxDetails; -import com.android.server.wm.AppCompatConfiguration.LetterboxBackgroundType; import java.io.PrintWriter; @@ -56,8 +39,6 @@ final class LetterboxUiController { private static final String TAG = TAG_WITH_CLASS_NAME ? "LetterboxUiController" : TAG_ATM; - private final Point mTmpPoint = new Point(); - private final AppCompatConfiguration mAppCompatConfiguration; private final ActivityRecord mActivityRecord; @@ -66,18 +47,9 @@ final class LetterboxUiController { @NonNull private final AppCompatReachabilityOverrides mAppCompatReachabilityOverrides; @NonNull - private final AppCompatReachabilityPolicy mAppCompatReachabilityPolicy; - @NonNull - private final TransparentPolicy mTransparentPolicy; + private final AppCompatLetterboxPolicy mAppCompatLetterboxPolicy; @NonNull - private final AppCompatOrientationOverrides mAppCompatOrientationOverrides; - - private boolean mShowWallpaperForLetterboxBackground; - - @Nullable - private Letterbox mLetterbox; - - private boolean mLastShouldShowLetterboxUi; + private final AppCompatLetterboxOverrides mAppCompatLetterboxOverrides; LetterboxUiController(WindowManagerService wmService, ActivityRecord activityRecord) { mAppCompatConfiguration = wmService.mAppCompatConfiguration; @@ -88,63 +60,34 @@ final class LetterboxUiController { // TODO(b/356385137): Remove these we added to make dependencies temporarily explicit. mAppCompatReachabilityOverrides = mActivityRecord.mAppCompatController .getAppCompatReachabilityOverrides(); - mAppCompatReachabilityPolicy = mActivityRecord.mAppCompatController - .getAppCompatReachabilityPolicy(); - mTransparentPolicy = mActivityRecord.mAppCompatController.getTransparentPolicy(); - mAppCompatOrientationOverrides = mActivityRecord.mAppCompatController - .getAppCompatOrientationOverrides(); + mAppCompatLetterboxPolicy = mActivityRecord.mAppCompatController + .getAppCompatLetterboxPolicy(); + mAppCompatLetterboxOverrides = mActivityRecord.mAppCompatController + .getAppCompatLetterboxOverrides(); } /** Cleans up {@link Letterbox} if it exists.*/ void destroy() { - if (mLetterbox != null) { - mLetterbox.destroy(); - mLetterbox = null; - // TODO Remove after Letterbox refactoring. - mAppCompatReachabilityPolicy.setLetterboxInnerBoundsSupplier(null); - } + mAppCompatLetterboxPolicy.destroy(); } void onMovedToDisplay(int displayId) { - if (mLetterbox != null) { - mLetterbox.onMovedToDisplay(displayId); - } + mAppCompatLetterboxPolicy.onMovedToDisplay(displayId); } boolean hasWallpaperBackgroundForLetterbox() { - return mShowWallpaperForLetterboxBackground; + return mAppCompatLetterboxOverrides.hasWallpaperBackgroundForLetterbox(); } /** Gets the letterbox insets. The insets will be empty if there is no letterbox. */ Rect getLetterboxInsets() { - if (mLetterbox != null) { - return mLetterbox.getInsets(); - } else { - return new Rect(); - } + return mAppCompatLetterboxPolicy.getLetterboxInsets(); } /** Gets the inner bounds of letterbox. The bounds will be empty if there is no letterbox. */ void getLetterboxInnerBounds(Rect outBounds) { - if (mLetterbox != null) { - outBounds.set(mLetterbox.getInnerFrame()); - final WindowState w = mActivityRecord.findMainWindow(); - if (w != null) { - adjustBoundsForTaskbar(w, outBounds); - } - } else { - outBounds.setEmpty(); - } - } - - /** Gets the outer bounds of letterbox. The bounds will be empty if there is no letterbox. */ - private void getLetterboxOuterBounds(Rect outBounds) { - if (mLetterbox != null) { - outBounds.set(mLetterbox.getOuterFrame()); - } else { - outBounds.setEmpty(); - } + mAppCompatLetterboxPolicy.getLetterboxInnerBounds(outBounds); } /** @@ -152,234 +95,39 @@ final class LetterboxUiController { * when the current activity is displayed. */ boolean isFullyTransparentBarAllowed(Rect rect) { - return mLetterbox == null || mLetterbox.notIntersectsOrFullyContains(rect); + return mAppCompatLetterboxPolicy.isFullyTransparentBarAllowed(rect); } void updateLetterboxSurfaceIfNeeded(WindowState winHint) { - updateLetterboxSurfaceIfNeeded(winHint, mActivityRecord.getSyncTransaction(), - mActivityRecord.getPendingTransaction()); + mAppCompatLetterboxPolicy.updateLetterboxSurfaceIfNeeded(winHint); } void updateLetterboxSurfaceIfNeeded(WindowState winHint, @NonNull Transaction t, @NonNull Transaction inputT) { - if (shouldNotLayoutLetterbox(winHint)) { - return; - } - layoutLetterboxIfNeeded(winHint); - if (mLetterbox != null && mLetterbox.needsApplySurfaceChanges()) { - mLetterbox.applySurfaceChanges(t, inputT); - } + mAppCompatLetterboxPolicy.updateLetterboxSurfaceIfNeeded(winHint, t, inputT); } void layoutLetterboxIfNeeded(WindowState w) { - if (shouldNotLayoutLetterbox(w)) { - return; - } - updateRoundedCornersIfNeeded(w); - updateWallpaperForLetterbox(w); - if (shouldShowLetterboxUi(w)) { - if (mLetterbox == null) { - mLetterbox = new Letterbox(() -> mActivityRecord.makeChildSurface(null), - mActivityRecord.mWmService.mTransactionFactory, - this::shouldLetterboxHaveRoundedCorners, - this::getLetterboxBackgroundColor, - this::hasWallpaperBackgroundForLetterbox, - this::getLetterboxWallpaperBlurRadiusPx, - this::getLetterboxWallpaperDarkScrimAlpha, - mAppCompatReachabilityPolicy, - this::getLetterboxParentSurface); - mLetterbox.attachInput(w); - } - - if (mActivityRecord.isInLetterboxAnimation()) { - // In this case we attach the letterbox to the task instead of the activity. - mActivityRecord.getTask().getPosition(mTmpPoint); - } else { - mActivityRecord.getPosition(mTmpPoint); - } - - // Get the bounds of the "space-to-fill". The transformed bounds have the highest - // priority because the activity is launched in a rotated environment. In multi-window - // mode, the taskFragment-level represents this for both split-screen - // and activity-embedding. In fullscreen-mode, the task container does - // (since the orientation letterbox is also applied to the task). - final Rect transformedBounds = mActivityRecord.getFixedRotationTransformDisplayBounds(); - final Rect spaceToFill = transformedBounds != null - ? transformedBounds - : mActivityRecord.inMultiWindowMode() - ? mActivityRecord.getTaskFragment().getBounds() - : mActivityRecord.getRootTask().getParent().getBounds(); - // In case of translucent activities an option is to use the WindowState#getFrame() of - // the first opaque activity beneath. In some cases (e.g. an opaque activity is using - // non MATCH_PARENT layouts or a Dialog theme) this might not provide the correct - // information and in particular it might provide a value for a smaller area making - // the letterbox overlap with the translucent activity's frame. - // If we use WindowState#getFrame() for the translucent activity's letterbox inner - // frame, the letterbox will then be overlapped with the translucent activity's frame. - // Because the surface layer of letterbox is lower than an activity window, this - // won't crop the content, but it may affect other features that rely on values stored - // in mLetterbox, e.g. transitions, a status bar scrim and recents preview in Launcher - // For this reason we use ActivityRecord#getBounds() that the translucent activity - // inherits from the first opaque activity beneath and also takes care of the scaling - // in case of activities in size compat mode. - final Rect innerFrame = - mTransparentPolicy.isRunning() ? mActivityRecord.getBounds() : w.getFrame(); - mLetterbox.layout(spaceToFill, innerFrame, mTmpPoint); - if (mAppCompatReachabilityOverrides.isDoubleTapEvent()) { - // We need to notify Shell that letterbox position has changed. - mActivityRecord.getTask().dispatchTaskInfoChangedIfNeeded(true /* force */); - } - } else if (mLetterbox != null) { - mLetterbox.hide(); - } - } - - SurfaceControl getLetterboxParentSurface() { - if (mActivityRecord.isInLetterboxAnimation()) { - return mActivityRecord.getTask().getSurfaceControl(); - } - return mActivityRecord.getSurfaceControl(); - } - - private static boolean shouldNotLayoutLetterbox(WindowState w) { - if (w == null) { - return true; - } - final int type = w.mAttrs.type; - // Allow letterbox to be displayed early for base application or application starting - // windows even if it is not on the top z order to prevent flickering when the - // letterboxed window is brought to the top - return (type != TYPE_BASE_APPLICATION && type != TYPE_APPLICATION_STARTING) - || w.mAnimatingExit; - } - - private boolean shouldLetterboxHaveRoundedCorners() { - // TODO(b/214030873): remove once background is drawn for transparent activities - // Letterbox shouldn't have rounded corners if the activity is transparent - return mAppCompatConfiguration.isLetterboxActivityCornersRounded() - && mActivityRecord.fillsParent(); + mAppCompatLetterboxPolicy.start(w); } boolean isLetterboxEducationEnabled() { - return mAppCompatConfiguration.getIsEducationEnabled(); + return mAppCompatLetterboxOverrides.isLetterboxEducationEnabled(); } @VisibleForTesting boolean shouldShowLetterboxUi(WindowState mainWindow) { - if (mAppCompatOrientationOverrides.getIsRelaunchingAfterRequestedOrientationChanged()) { - return mLastShouldShowLetterboxUi; - } - - final boolean shouldShowLetterboxUi = - (mActivityRecord.isInLetterboxAnimation() || mActivityRecord.isVisible() - || mActivityRecord.isVisibleRequested()) - && mainWindow.areAppWindowBoundsLetterboxed() - // Check for FLAG_SHOW_WALLPAPER explicitly instead of using - // WindowContainer#showWallpaper because the later will return true when this - // activity is using blurred wallpaper for letterbox background. - && (mainWindow.getAttrs().flags & FLAG_SHOW_WALLPAPER) == 0; - - mLastShouldShowLetterboxUi = shouldShowLetterboxUi; - - return shouldShowLetterboxUi; + return mAppCompatLetterboxPolicy.shouldShowLetterboxUi(mainWindow); } Color getLetterboxBackgroundColor() { - final WindowState w = mActivityRecord.findMainWindow(); - if (w == null || w.isLetterboxedForDisplayCutout()) { - return Color.valueOf(Color.BLACK); - } - @LetterboxBackgroundType int letterboxBackgroundType = - mAppCompatConfiguration.getLetterboxBackgroundType(); - TaskDescription taskDescription = mActivityRecord.taskDescription; - switch (letterboxBackgroundType) { - case LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING: - if (taskDescription != null && taskDescription.getBackgroundColorFloating() != 0) { - return Color.valueOf(taskDescription.getBackgroundColorFloating()); - } - break; - case LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND: - if (taskDescription != null && taskDescription.getBackgroundColor() != 0) { - return Color.valueOf(taskDescription.getBackgroundColor()); - } - break; - case LETTERBOX_BACKGROUND_WALLPAPER: - if (hasWallpaperBackgroundForLetterbox()) { - // Color is used for translucent scrim that dims wallpaper. - return mAppCompatConfiguration.getLetterboxBackgroundColor(); - } - Slog.w(TAG, "Wallpaper option is selected for letterbox background but " - + "blur is not supported by a device or not supported in the current " - + "window configuration or both alpha scrim and blur radius aren't " - + "provided so using solid color background"); - break; - case LETTERBOX_BACKGROUND_SOLID_COLOR: - return mAppCompatConfiguration.getLetterboxBackgroundColor(); - default: - throw new AssertionError( - "Unexpected letterbox background type: " + letterboxBackgroundType); - } - // If picked option configured incorrectly or not supported then default to a solid color - // background. - return mAppCompatConfiguration.getLetterboxBackgroundColor(); - } - - private void updateRoundedCornersIfNeeded(final WindowState mainWindow) { - final SurfaceControl windowSurface = mainWindow.getSurfaceControl(); - if (windowSurface == null || !windowSurface.isValid()) { - return; - } - - // cropBounds must be non-null for the cornerRadius to be ever applied. - mActivityRecord.getSyncTransaction() - .setCrop(windowSurface, getCropBoundsIfNeeded(mainWindow)) - .setCornerRadius(windowSurface, getRoundedCornersRadius(mainWindow)); + return mAppCompatLetterboxOverrides.getLetterboxBackgroundColor(); } @VisibleForTesting @Nullable Rect getCropBoundsIfNeeded(final WindowState mainWindow) { - if (!requiresRoundedCorners(mainWindow) || mActivityRecord.isInLetterboxAnimation()) { - // We don't want corner radius on the window. - // In the case the ActivityRecord requires a letterboxed animation we never want - // rounded corners on the window because rounded corners are applied at the - // animation-bounds surface level and rounded corners on the window would interfere - // with that leading to unexpected rounded corner positioning during the animation. - return null; - } - - final Rect cropBounds = new Rect(mActivityRecord.getBounds()); - - // In case of translucent activities we check if the requested size is different from - // the size provided using inherited bounds. In that case we decide to not apply rounded - // corners because we assume the specific layout would. This is the case when the layout - // of the translucent activity uses only a part of all the bounds because of the use of - // LayoutParams.WRAP_CONTENT. - if (mTransparentPolicy.isRunning() && (cropBounds.width() != mainWindow.mRequestedWidth - || cropBounds.height() != mainWindow.mRequestedHeight)) { - return null; - } - - // It is important to call {@link #adjustBoundsIfNeeded} before {@link cropBounds.offsetTo} - // because taskbar bounds used in {@link #adjustBoundsIfNeeded} - // are in screen coordinates - adjustBoundsForTaskbar(mainWindow, cropBounds); - - final float scale = mainWindow.mInvGlobalScale; - if (scale != 1f && scale > 0f) { - cropBounds.scale(scale); - } - - // ActivityRecord bounds are in screen coordinates while (0,0) for activity's surface - // control is in the top left corner of an app window so offsetting bounds - // accordingly. - cropBounds.offsetTo(0, 0); - return cropBounds; - } - - private boolean requiresRoundedCorners(final WindowState mainWindow) { - return isLetterboxedNotForDisplayCutout(mainWindow) - && mAppCompatConfiguration.isLetterboxActivityCornersRounded(); + return mAppCompatLetterboxPolicy.getCropBoundsIfNeeded(mainWindow); } // Returns rounded corners radius the letterboxed activity should have based on override in @@ -388,102 +136,12 @@ final class LetterboxUiController { // for all corners for consistency and pick a minimal bottom one for consistency with a // taskbar rounded corners. int getRoundedCornersRadius(final WindowState mainWindow) { - if (!requiresRoundedCorners(mainWindow)) { - return 0; - } - - final int radius; - if (mAppCompatConfiguration.getLetterboxActivityCornersRadius() >= 0) { - radius = mAppCompatConfiguration.getLetterboxActivityCornersRadius(); - } else { - final InsetsState insetsState = mainWindow.getInsetsState(); - radius = Math.min( - getInsetsStateCornerRadius(insetsState, RoundedCorner.POSITION_BOTTOM_LEFT), - getInsetsStateCornerRadius(insetsState, RoundedCorner.POSITION_BOTTOM_RIGHT)); - } - - final float scale = mainWindow.mInvGlobalScale; - return (scale != 1f && scale > 0f) ? (int) (scale * radius) : radius; + return mAppCompatLetterboxPolicy.getRoundedCornersRadius(mainWindow); } - /** - * Returns the taskbar in case it is visible and expanded in height, otherwise returns null. - */ - @VisibleForTesting @Nullable - InsetsSource getExpandedTaskbarOrNull(final WindowState mainWindow) { - final InsetsState state = mainWindow.getInsetsState(); - for (int i = state.sourceSize() - 1; i >= 0; i--) { - final InsetsSource source = state.sourceAt(i); - if (source.getType() == WindowInsets.Type.navigationBars() - && source.hasFlags(InsetsSource.FLAG_INSETS_ROUNDED_CORNER) - && source.isVisible()) { - return source; - } - } - return null; - } - - private void adjustBoundsForTaskbar(final WindowState mainWindow, final Rect bounds) { - // Rounded corners should be displayed above the taskbar. When taskbar is hidden, - // an insets frame is equal to a navigation bar which shouldn't affect position of - // rounded corners since apps are expected to handle navigation bar inset. - // This condition checks whether the taskbar is visible. - // Do not crop the taskbar inset if the window is in immersive mode - the user can - // swipe to show/hide the taskbar as an overlay. - // Adjust the bounds only in case there is an expanded taskbar, - // otherwise the rounded corners will be shown behind the navbar. - final InsetsSource expandedTaskbarOrNull = getExpandedTaskbarOrNull(mainWindow); - if (expandedTaskbarOrNull != null) { - // Rounded corners should be displayed above the expanded taskbar. - bounds.bottom = Math.min(bounds.bottom, expandedTaskbarOrNull.getFrame().top); - } - } - - private int getInsetsStateCornerRadius( - InsetsState insetsState, @RoundedCorner.Position int position) { - RoundedCorner corner = insetsState.getRoundedCorners().getRoundedCorner(position); - return corner == null ? 0 : corner.getRadius(); - } - - private boolean isLetterboxedNotForDisplayCutout(WindowState mainWindow) { - return shouldShowLetterboxUi(mainWindow) - && !mainWindow.isLetterboxedForDisplayCutout(); - } - - private void updateWallpaperForLetterbox(WindowState mainWindow) { - @LetterboxBackgroundType int letterboxBackgroundType = - mAppCompatConfiguration.getLetterboxBackgroundType(); - boolean wallpaperShouldBeShown = - letterboxBackgroundType == LETTERBOX_BACKGROUND_WALLPAPER - // Don't use wallpaper as a background if letterboxed for display cutout. - && isLetterboxedNotForDisplayCutout(mainWindow) - // Check that dark scrim alpha or blur radius are provided - && (getLetterboxWallpaperBlurRadiusPx() > 0 - || getLetterboxWallpaperDarkScrimAlpha() > 0) - // Check that blur is supported by a device if blur radius is provided. - && (getLetterboxWallpaperBlurRadiusPx() <= 0 - || isLetterboxWallpaperBlurSupported()); - if (mShowWallpaperForLetterboxBackground != wallpaperShouldBeShown) { - mShowWallpaperForLetterboxBackground = wallpaperShouldBeShown; - mActivityRecord.requestUpdateWallpaperIfNeeded(); - } - } - - private int getLetterboxWallpaperBlurRadiusPx() { - int blurRadius = mAppCompatConfiguration.getLetterboxBackgroundWallpaperBlurRadiusPx(); - return Math.max(blurRadius, 0); - } - - private float getLetterboxWallpaperDarkScrimAlpha() { - float alpha = mAppCompatConfiguration.getLetterboxBackgroundWallpaperDarkScrimAlpha(); - // No scrim by default. - return (alpha < 0 || alpha >= 1) ? 0.0f : alpha; - } - - private boolean isLetterboxWallpaperBlurSupported() { - return mAppCompatConfiguration.mContext.getSystemService(WindowManager.class) - .isCrossWindowBlurEnabled(); + LetterboxDetails getLetterboxDetails() { + return mAppCompatLetterboxPolicy.getLetterboxDetails(); } void dump(PrintWriter pw, String prefix) { @@ -492,6 +150,9 @@ final class LetterboxUiController { return; } + pw.println(prefix + "isTransparentPolicyRunning=" + + mActivityRecord.mAppCompatController.getTransparentPolicy().isRunning()); + boolean areBoundsLetterboxed = mainWin.areAppWindowBoundsLetterboxed(); pw.println(prefix + "areBoundsLetterboxed=" + areBoundsLetterboxed); if (!areBoundsLetterboxed) { @@ -523,11 +184,11 @@ final class LetterboxUiController { if (mAppCompatConfiguration.getLetterboxBackgroundType() == LETTERBOX_BACKGROUND_WALLPAPER) { pw.println(prefix + " isLetterboxWallpaperBlurSupported=" - + isLetterboxWallpaperBlurSupported()); + + mAppCompatLetterboxOverrides.isLetterboxWallpaperBlurSupported()); pw.println(prefix + " letterboxBackgroundWallpaperDarkScrimAlpha=" - + getLetterboxWallpaperDarkScrimAlpha()); + + mAppCompatLetterboxOverrides.getLetterboxWallpaperDarkScrimAlpha()); pw.println(prefix + " letterboxBackgroundWallpaperBlurRadius=" - + getLetterboxWallpaperBlurRadiusPx()); + + mAppCompatLetterboxOverrides.getLetterboxWallpaperBlurRadiusPx()); } final AppCompatReachabilityOverrides reachabilityOverrides = mActivityRecord .mAppCompatController.getAppCompatReachabilityOverrides(); @@ -557,26 +218,4 @@ final class LetterboxUiController { + mAppCompatConfiguration .getIsDisplayAspectRatioEnabledForFixedOrientationLetterbox()); } - - @Nullable - LetterboxDetails getLetterboxDetails() { - final WindowState w = mActivityRecord.findMainWindow(); - if (mLetterbox == null || w == null || w.isLetterboxedForDisplayCutout()) { - return null; - } - Rect letterboxInnerBounds = new Rect(); - Rect letterboxOuterBounds = new Rect(); - getLetterboxInnerBounds(letterboxInnerBounds); - getLetterboxOuterBounds(letterboxOuterBounds); - - if (letterboxInnerBounds.isEmpty() || letterboxOuterBounds.isEmpty()) { - return null; - } - - return new LetterboxDetails( - letterboxInnerBounds, - letterboxOuterBounds, - w.mAttrs.insetsFlags.appearance - ); - } } diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java index 32ec020580d9..7c875c1f3322 100644 --- a/services/core/java/com/android/server/wm/Session.java +++ b/services/core/java/com/android/server/wm/Session.java @@ -324,22 +324,6 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient { Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); } - @Override - public boolean performHapticFeedback(int effectId, int flags, int privFlags) { - final long ident = Binder.clearCallingIdentity(); - try { - return mService.mPolicy.performHapticFeedback(mUid, mPackageName, effectId, null, flags, - privFlags); - } finally { - Binder.restoreCallingIdentity(ident); - } - } - - @Override - public void performHapticFeedbackAsync(int effectId, int flags, int privFlags) { - performHapticFeedback(effectId, flags, privFlags); - } - /* Drag/drop */ @Override 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/SnapshotPersistQueue.java b/services/core/java/com/android/server/wm/SnapshotPersistQueue.java index 16fcb097ca5c..1c8c245f7640 100644 --- a/services/core/java/com/android/server/wm/SnapshotPersistQueue.java +++ b/services/core/java/com/android/server/wm/SnapshotPersistQueue.java @@ -313,6 +313,7 @@ class SnapshotPersistQueue { proto.appearance = mSnapshot.getAppearance(); proto.isTranslucent = mSnapshot.isTranslucent(); proto.topActivityComponent = mSnapshot.getTopActivityComponent().flattenToString(); + proto.uiMode = mSnapshot.getUiMode(); proto.id = mSnapshot.getId(); final byte[] bytes = TaskSnapshotProto.toByteArray(proto); final File file = mPersistInfoProvider.getProtoFile(mId, mUserId); diff --git a/services/core/java/com/android/server/wm/SystemGesturesPointerEventListener.java b/services/core/java/com/android/server/wm/SystemGesturesPointerEventListener.java index b7944d3b8234..a83e8c7a28bd 100644 --- a/services/core/java/com/android/server/wm/SystemGesturesPointerEventListener.java +++ b/services/core/java/com/android/server/wm/SystemGesturesPointerEventListener.java @@ -116,7 +116,7 @@ class SystemGesturesPointerEventListener implements PointerEventListener { final Display display = DisplayManagerGlobal.getInstance() .getRealDisplay(Display.DEFAULT_DISPLAY); - final DisplayCutout displayCutout = display.getCutout(); + final DisplayCutout displayCutout = display != null ? display.getCutout() : null; if (displayCutout != null) { // Expand swipe start threshold such that we can catch touches that just start beyond // the notch area diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index d3df5fdcc447..12e91adef9c5 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -3507,7 +3507,7 @@ class Task extends TaskFragment { | StartingWindowInfo.TYPE_PARAMETER_WINDOWLESS); if ((info.startingWindowTypeParameter & StartingWindowInfo.TYPE_PARAMETER_ACTIVITY_CREATED) != 0) { - final WindowState topMainWin = getWindow(w -> w.mAttrs.type == TYPE_BASE_APPLICATION); + final WindowState topMainWin = getTopFullscreenMainWindow(); if (topMainWin != null) { info.mainWindowLayoutParams = topMainWin.getAttrs(); info.requestedVisibleTypes = topMainWin.getRequestedVisibleTypes(); 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/TaskFragmentOrganizerController.java b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java index 561ff7db5b9d..7212d379162b 100644 --- a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java +++ b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java @@ -407,8 +407,7 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr change.setTaskFragmentToken(lastParentTfToken); } // Only pass the activity token to the client if it belongs to the same process. - if (Flags.fixPipRestoreToOverlay() && nextFillTaskActivity != null - && nextFillTaskActivity.getPid() == mOrganizerPid) { + if (nextFillTaskActivity != null && nextFillTaskActivity.getPid() == mOrganizerPid) { change.setOtherActivityToken(nextFillTaskActivity.token); } return change; diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index 5698750170f4..486a61b7aef6 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) { @@ -3350,6 +3354,15 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { return chg.hasChanged(); } + boolean hasChanges() { + for (int i = 0; i < mParticipants.size(); ++i) { + if (mChanges.get(mParticipants.valueAt(i)).hasChanged()) { + return true; + } + } + return false; + } + @VisibleForTesting static class ChangeInfo { private static final int FLAG_NONE = 0; diff --git a/services/core/java/com/android/server/wm/TransparentPolicy.java b/services/core/java/com/android/server/wm/TransparentPolicy.java index 2f46103fdf17..39b2635eb8ac 100644 --- a/services/core/java/com/android/server/wm/TransparentPolicy.java +++ b/services/core/java/com/android/server/wm/TransparentPolicy.java @@ -92,6 +92,7 @@ class TransparentPolicy { if (parent == null) { return; } + final boolean wasStarted = mTransparentPolicyState.isRunning(); mTransparentPolicyState.reset(); // In case mActivityRecord.hasCompatDisplayInsetsWithoutOverride() we don't apply the // opaque activity constraints because we're expecting the activity is already letterboxed. @@ -102,6 +103,9 @@ class TransparentPolicy { // We check if we need for some reason to skip the policy gievn the specific first // opaque activity if (shouldSkipTransparentPolicy(firstOpaqueActivity)) { + if (wasStarted) { + mActivityRecord.recomputeConfiguration(); + } return; } mTransparentPolicyState.start(firstOpaqueActivity); @@ -190,7 +194,6 @@ class TransparentPolicy { // We skip letterboxing if the translucent activity doesn't have any // opaque activities beneath or the activity below is embedded which // never has letterbox. - mActivityRecord.recomputeConfiguration(); return true; } if (mActivityRecord.getTask() == null || mActivityRecord.fillsParent() @@ -260,6 +263,10 @@ class TransparentPolicy { mLetterboxConfigListener = WindowContainer.overrideConfigurationPropagation( mActivityRecord, mFirstOpaqueActivity, (opaqueConfig, transparentOverrideConfig) -> { + if (!isPolicyEnabled()) { + transparentOverrideConfig.unset(); + return transparentOverrideConfig; + } resetTranslucentOverrideConfig(transparentOverrideConfig); final Rect parentBounds = parent.getWindowConfiguration().getBounds(); final Rect bounds = transparentOverrideConfig @@ -313,7 +320,17 @@ class TransparentPolicy { } private boolean isRunning() { - return mLetterboxConfigListener != null; + return mLetterboxConfigListener != null && isPolicyEnabled(); + } + + private boolean isPolicyEnabled() { + if (!mActivityRecord.mWmService.mFlags.mRespectNonTopVisibleFixedOrientation) { + return true; + } + // Do not enable the policy if the activity can affect display orientation. + final int orientation = mActivityRecord.getOverrideOrientation(); + return orientation == SCREEN_ORIENTATION_UNSPECIFIED + || !mActivityRecord.handlesOrientationChangeFromDescendant(orientation); } private void clearInheritedCompatDisplayInsets() { diff --git a/services/core/java/com/android/server/wm/WindowAnimationSpec.java b/services/core/java/com/android/server/wm/WindowAnimationSpec.java index 34b9913c9738..2c58c61701cc 100644 --- a/services/core/java/com/android/server/wm/WindowAnimationSpec.java +++ b/services/core/java/com/android/server/wm/WindowAnimationSpec.java @@ -97,10 +97,10 @@ public class WindowAnimationSpec implements AnimationSpec { /** * @return If a window animation has outsets applied to it. - * @see Animation#hasExtension() + * @see Animation#getExtensionEdges() */ public boolean hasExtension() { - return mAnimation.hasExtension(); + return mAnimation.getExtensionEdges() != 0; } @Override diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index 0093e9d0788b..c6d0b729cd5a 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(); } @@ -1187,7 +1194,13 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub } } } - effects |= sanitizeAndApplyHierarchyOp(wc, hop); + if (wc.asTask() != null) { + effects |= sanitizeAndApplyHierarchyOpForTask(wc.asTask(), hop); + } else if (wc.asDisplayArea() != null) { + effects |= sanitizeAndApplyHierarchyOpForDisplayArea(wc.asDisplayArea(), hop); + } else { + throw new IllegalArgumentException("Invalid container in hierarchy op"); + } break; } case HIERARCHY_OP_TYPE_ADD_TASK_FRAGMENT_OPERATION: { @@ -1386,6 +1399,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; } @@ -1837,12 +1856,22 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub return starterResult[0]; } - private int sanitizeAndApplyHierarchyOp(WindowContainer container, - WindowContainerTransaction.HierarchyOp hop) { - final Task task = container.asTask(); - if (task == null) { - throw new IllegalArgumentException("Invalid container in hierarchy op"); + private int sanitizeAndApplyHierarchyOpForDisplayArea(@NonNull DisplayArea displayArea, + @NonNull WindowContainerTransaction.HierarchyOp hop) { + if (hop.getType() != HIERARCHY_OP_TYPE_REORDER) { + throw new UnsupportedOperationException("DisplayArea only supports reordering"); + } + if (displayArea.getParent() == null) { + return TRANSACT_EFFECTS_NONE; } + displayArea.getParent().positionChildAt( + hop.getToTop() ? POSITION_TOP : POSITION_BOTTOM, + displayArea, hop.includingParents()); + return TRANSACT_EFFECTS_LIFECYCLE; + } + + private int sanitizeAndApplyHierarchyOpForTask(@NonNull Task task, + @NonNull WindowContainerTransaction.HierarchyOp hop) { final DisplayContent dc = task.getDisplayContent(); if (dc == null) { Slog.w(TAG, "Container is no longer attached: " + task); diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index e6467522a410..ec2fd3f17556 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -55,6 +55,7 @@ import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_M import static android.view.WindowManager.LayoutParams.MATCH_PARENT; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NOT_MAGNIFIABLE; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION; +import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_OPT_OUT_EDGE_TO_EDGE; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_SYSTEM_APPLICATION_OVERLAY; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY; import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; @@ -2989,6 +2990,25 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP return (mAttrs.flags & FLAG_SHOW_WHEN_LOCKED) != 0; } + @Override + void resolveOverrideConfiguration(Configuration newParentConfig) { + super.resolveOverrideConfiguration(newParentConfig); + if (mActivityRecord != null) { + // Let the activity decide whether to apply the size override. + return; + } + final Configuration resolvedConfig = getResolvedOverrideConfiguration(); + resolvedConfig.seq = newParentConfig.seq; + applySizeOverrideIfNeeded( + getDisplayContent(), + mSession.mProcess.mInfo, + newParentConfig, + resolvedConfig, + (mAttrs.privateFlags & PRIVATE_FLAG_OPT_OUT_EDGE_TO_EDGE) != 0, + false /* hasFixedRotationTransform */, + false /* hasCompatDisplayInsets */); + } + /** * @return {@code true} if this window can receive touches based on among other things, * windowing state and recents animation state. 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/DevicePolicyEngine.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java index 669a999c921e..a08af72586ee 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java @@ -2080,10 +2080,14 @@ final class DevicePolicyEngine { String tag = parser.getName(); switch (tag) { case TAG_LOCAL_POLICY_ENTRY: - readLocalPoliciesInner(parser); + int userId = parser.getAttributeInt(/* namespace= */ null, ATTR_USER_ID); + if (!mLocalPolicies.contains(userId)) { + mLocalPolicies.put(userId, new HashMap<>()); + } + readPoliciesInner(parser, mLocalPolicies.get(userId)); break; case TAG_GLOBAL_POLICY_ENTRY: - readGlobalPoliciesInner(parser); + readPoliciesInner(parser, mGlobalPolicies); break; case TAG_ENFORCING_ADMINS_ENTRY: readEnforcingAdminsInner(parser); @@ -2100,64 +2104,45 @@ final class DevicePolicyEngine { } } - private void readLocalPoliciesInner(TypedXmlPullParser parser) - throws XmlPullParserException, IOException { - int userId = parser.getAttributeInt(/* namespace= */ null, ATTR_USER_ID); - PolicyKey policyKey = null; - PolicyState<?> policyState = null; - int outerDepth = parser.getDepth(); - while (XmlUtils.nextElementWithin(parser, outerDepth)) { - String tag = parser.getName(); - switch (tag) { - case TAG_POLICY_KEY_ENTRY: - policyKey = PolicyDefinition.readPolicyKeyFromXml(parser); - break; - case TAG_POLICY_STATE_ENTRY: - policyState = PolicyState.readFromXml(parser); - break; - default: - Slogf.wtf(TAG, "Unknown tag for local policy entry" + tag); - } - } - - if (policyKey != null && policyState != null) { - if (!mLocalPolicies.contains(userId)) { - mLocalPolicies.put(userId, new HashMap<>()); - } - mLocalPolicies.get(userId).put(policyKey, policyState); - } else { - Slogf.wtf(TAG, "Error parsing local policy, policyKey is " - + (policyKey == null ? "null" : policyKey) + ", and policyState is " - + (policyState == null ? "null" : policyState) + "."); - } - } - - private void readGlobalPoliciesInner(TypedXmlPullParser parser) + private static void readPoliciesInner( + TypedXmlPullParser parser, Map<PolicyKey, PolicyState<?>> policyStateMap) throws IOException, XmlPullParserException { PolicyKey policyKey = null; + PolicyDefinition<?> policyDefinition = null; PolicyState<?> policyState = null; int outerDepth = parser.getDepth(); while (XmlUtils.nextElementWithin(parser, outerDepth)) { String tag = parser.getName(); switch (tag) { case TAG_POLICY_KEY_ENTRY: - policyKey = PolicyDefinition.readPolicyKeyFromXml(parser); + if (Flags.dontReadPolicyDefinition()) { + policyDefinition = PolicyDefinition.readFromXml(parser); + if (policyDefinition != null) { + policyKey = policyDefinition.getPolicyKey(); + } + } else { + policyKey = PolicyDefinition.readPolicyKeyFromXml(parser); + } break; case TAG_POLICY_STATE_ENTRY: - policyState = PolicyState.readFromXml(parser); + if (Flags.dontReadPolicyDefinition() && policyDefinition == null) { + Slogf.w(TAG, "Skipping policy state - unknown policy definition"); + } else { + policyState = PolicyState.readFromXml(policyDefinition, parser); + } break; default: - Slogf.wtf(TAG, "Unknown tag for local policy entry" + tag); + Slogf.wtf(TAG, "Unknown tag for policy entry" + tag); } } - if (policyKey != null && policyState != null) { - mGlobalPolicies.put(policyKey, policyState); - } else { - Slogf.wtf(TAG, "Error parsing global policy, policyKey is " - + (policyKey == null ? "null" : policyKey) + ", and policyState is " - + (policyState == null ? "null" : policyState) + "."); + if (policyKey == null || policyState == null) { + Slogf.wtf(TAG, "Error parsing policy, policyKey is %s, and policyState is %s.", + policyKey, policyState); + return; } + + policyStateMap.put(policyKey, policyState); } private void readEnforcingAdminsInner(TypedXmlPullParser parser) diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java index 8e3248eaa6bf..19a942cd2eed 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java @@ -129,9 +129,8 @@ final class PolicyDefinition<V> { */ static PolicyDefinition<Integer> PERMISSION_GRANT( @NonNull String packageName, @NonNull String permissionName) { - if (packageName == null || permissionName == null) { - return GENERIC_PERMISSION_GRANT; - } + Objects.requireNonNull(packageName, "packageName must not be null"); + Objects.requireNonNull(permissionName, "permissionName must not be null"); return GENERIC_PERMISSION_GRANT.createPolicyDefinition( new PackagePermissionPolicyKey( DevicePolicyIdentifiers.PERMISSION_GRANT_POLICY, @@ -190,10 +189,8 @@ final class PolicyDefinition<V> { * {@link #GENERIC_PERSISTENT_PREFERRED_ACTIVITY}. */ static PolicyDefinition<ComponentName> PERSISTENT_PREFERRED_ACTIVITY( - IntentFilter intentFilter) { - if (intentFilter == null) { - return GENERIC_PERSISTENT_PREFERRED_ACTIVITY; - } + @NonNull IntentFilter intentFilter) { + Objects.requireNonNull(intentFilter, "intentFilter must not be null"); return GENERIC_PERSISTENT_PREFERRED_ACTIVITY.createPolicyDefinition( new IntentFilterPolicyKey( DevicePolicyIdentifiers.PERSISTENT_PREFERRED_ACTIVITY_POLICY, @@ -216,11 +213,8 @@ final class PolicyDefinition<V> { * Passing in {@code null} for {@code packageName} will return * {@link #GENERIC_PACKAGE_UNINSTALL_BLOCKED}. */ - static PolicyDefinition<Boolean> PACKAGE_UNINSTALL_BLOCKED( - String packageName) { - if (packageName == null) { - return GENERIC_PACKAGE_UNINSTALL_BLOCKED; - } + static PolicyDefinition<Boolean> PACKAGE_UNINSTALL_BLOCKED(@NonNull String packageName) { + Objects.requireNonNull(packageName, "packageName must not be null"); return GENERIC_PACKAGE_UNINSTALL_BLOCKED.createPolicyDefinition( new PackagePolicyKey( DevicePolicyIdentifiers.PACKAGE_UNINSTALL_BLOCKED_POLICY, packageName)); @@ -247,10 +241,8 @@ final class PolicyDefinition<V> { * Passing in {@code null} for {@code packageName} will return * {@link #GENERIC_APPLICATION_RESTRICTIONS}. */ - static PolicyDefinition<Bundle> APPLICATION_RESTRICTIONS(String packageName) { - if (packageName == null) { - return GENERIC_APPLICATION_RESTRICTIONS; - } + static PolicyDefinition<Bundle> APPLICATION_RESTRICTIONS(@NonNull String packageName) { + Objects.requireNonNull(packageName, "packageName must not be null"); return GENERIC_APPLICATION_RESTRICTIONS.createPolicyDefinition( new PackagePolicyKey( DevicePolicyIdentifiers.APPLICATION_RESTRICTIONS_POLICY, packageName)); @@ -293,10 +285,8 @@ final class PolicyDefinition<V> { * Passing in {@code null} for {@code packageName} will return * {@link #GENERIC_APPLICATION_HIDDEN}. */ - static PolicyDefinition<Boolean> APPLICATION_HIDDEN(String packageName) { - if (packageName == null) { - return GENERIC_APPLICATION_HIDDEN; - } + static PolicyDefinition<Boolean> APPLICATION_HIDDEN(@NonNull String packageName) { + Objects.requireNonNull(packageName, "packageName must not be null"); return GENERIC_APPLICATION_HIDDEN.createPolicyDefinition( new PackagePolicyKey( DevicePolicyIdentifiers.APPLICATION_HIDDEN_POLICY, packageName)); @@ -319,10 +309,8 @@ final class PolicyDefinition<V> { * Passing in {@code null} for {@code accountType} will return * {@link #GENERIC_ACCOUNT_MANAGEMENT_DISABLED}. */ - static PolicyDefinition<Boolean> ACCOUNT_MANAGEMENT_DISABLED(String accountType) { - if (accountType == null) { - return GENERIC_ACCOUNT_MANAGEMENT_DISABLED; - } + static PolicyDefinition<Boolean> ACCOUNT_MANAGEMENT_DISABLED(@NonNull String accountType) { + Objects.requireNonNull(accountType, "accountType must not be null"); return GENERIC_ACCOUNT_MANAGEMENT_DISABLED.createPolicyDefinition( new AccountTypePolicyKey( DevicePolicyIdentifiers.ACCOUNT_MANAGEMENT_DISABLED_POLICY, accountType)); @@ -708,17 +696,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/devicepolicy/java/com/android/server/devicepolicy/PolicyState.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyState.java index 245c43884e02..b81348969f7d 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyState.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyState.java @@ -19,6 +19,7 @@ package com.android.server.devicepolicy; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.admin.PolicyValue; +import android.app.admin.flags.Flags; import android.util.IndentingPrintWriter; import com.android.internal.util.XmlUtils; @@ -254,11 +255,9 @@ final class PolicyState<V> { } @Nullable - static <V> PolicyState<V> readFromXml(TypedXmlPullParser parser) + static <V> PolicyState<V> readFromXml( + PolicyDefinition<V> policyDefinition, TypedXmlPullParser parser) throws IOException, XmlPullParserException { - - PolicyDefinition<V> policyDefinition = null; - PolicyValue<V> currentResolvedPolicy = null; LinkedHashMap<EnforcingAdmin, PolicyValue<V>> policiesSetByAdmins = new LinkedHashMap<>(); @@ -300,10 +299,15 @@ final class PolicyState<V> { } break; case TAG_POLICY_DEFINITION_ENTRY: - policyDefinition = PolicyDefinition.readFromXml(parser); - if (policyDefinition == null) { - Slogf.wtf(TAG, "Error Parsing TAG_POLICY_DEFINITION_ENTRY, " - + "PolicyDefinition is null"); + if (Flags.dontReadPolicyDefinition()) { + // Should be passed by the caller. + Objects.requireNonNull(policyDefinition); + } else { + policyDefinition = PolicyDefinition.readFromXml(parser); + if (policyDefinition == null) { + Slogf.wtf(TAG, "Error Parsing TAG_POLICY_DEFINITION_ENTRY, " + + "PolicyDefinition is null"); + } } break; diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 9e8811f419a2..09c54cb40373 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -16,6 +16,7 @@ package com.android.server; +import static android.app.appfunctions.flags.Flags.enableAppFunctionManager; import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK; import static android.os.IServiceManager.DUMP_FLAG_PRIORITY_CRITICAL; import static android.os.IServiceManager.DUMP_FLAG_PRIORITY_HIGH; @@ -105,6 +106,7 @@ import com.android.internal.notification.SystemNotificationChannels; import com.android.internal.os.BinderInternal; import com.android.internal.os.RuntimeInit; import com.android.internal.policy.AttributeCache; +import com.android.internal.protolog.ProtoLogService; import com.android.internal.util.ConcurrentUtils; import com.android.internal.util.EmergencyAffordanceManager; import com.android.internal.util.FrameworkStatsLog; @@ -119,6 +121,7 @@ import com.android.server.am.ActivityManagerService; import com.android.server.ambientcontext.AmbientContextManagerService; import com.android.server.app.GameManagerService; import com.android.server.appbinding.AppBindingService; +import com.android.server.appfunctions.AppFunctionManagerService; import com.android.server.apphibernation.AppHibernationService; import com.android.server.appop.AppOpMigrationHelper; import com.android.server.appop.AppOpMigrationHelperImpl; @@ -1087,6 +1090,13 @@ public final class SystemServer implements Dumpable { SystemServerInitThreadPool.submit(SystemConfig::getInstance, TAG_SYSTEM_CONFIG); t.traceEnd(); + // Orchestrates some ProtoLogging functionality. + if (android.tracing.Flags.clientSideProtoLogging()) { + t.traceBegin("StartProtoLogService"); + ServiceManager.addService(Context.PROTOLOG_SERVICE, new ProtoLogService()); + t.traceEnd(); + } + // Platform compat service is used by ActivityManagerService, PackageManagerService, and // possibly others in the future. b/135010838. t.traceBegin("PlatformCompat"); @@ -1719,6 +1729,12 @@ public final class SystemServer implements Dumpable { mSystemServiceManager.startService(LogcatManagerService.class); t.traceEnd(); + t.traceBegin("StartAppFunctionManager"); + if (enableAppFunctionManager()) { + mSystemServiceManager.startService(AppFunctionManagerService.class); + } + t.traceEnd(); + } catch (Throwable e) { Slog.e("System", "******************************************"); Slog.e("System", "************ Failure starting core service"); diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java index 8ae4f9a41efb..6afcae797277 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java @@ -45,7 +45,11 @@ import android.annotation.Nullable; import android.os.Binder; import android.os.IBinder; import android.os.RemoteException; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.view.Display; +import android.view.inputmethod.Flags; import android.view.inputmethod.ImeTracker; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -58,6 +62,7 @@ import com.android.internal.inputmethod.StartInputFlags; import com.android.internal.inputmethod.StartInputReason; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -70,6 +75,9 @@ import org.junit.runner.RunWith; */ @RunWith(AndroidJUnit4.class) public class DefaultImeVisibilityApplierTest extends InputMethodManagerServiceTestBase { + + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); private DefaultImeVisibilityApplier mVisibilityApplier; @Before @@ -112,6 +120,7 @@ public class DefaultImeVisibilityApplierTest extends InputMethodManagerServiceTe } @Test + @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) public void testApplyImeVisibility_showIme() { final var statsToken = ImeTracker.Token.empty(); synchronized (ImfLock.class) { @@ -122,6 +131,7 @@ public class DefaultImeVisibilityApplierTest extends InputMethodManagerServiceTe } @Test + @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) public void testApplyImeVisibility_hideIme() { final var statsToken = ImeTracker.Token.empty(); synchronized (ImfLock.class) { @@ -141,7 +151,12 @@ public class DefaultImeVisibilityApplierTest extends InputMethodManagerServiceTe mVisibilityApplier.applyImeVisibility(mWindowToken, ImeTracker.Token.empty(), STATE_HIDE_IME_EXPLICIT, eq(SoftInputShowHideReason.NOT_SET), mUserId); } - verifyHideSoftInput(true, true); + if (Flags.refactorInsetsController()) { + verifySetImeVisibility(true /* setVisible */, false /* invoked */); + verifySetImeVisibility(false /* setVisible */, true /* invoked */); + } else { + verifyHideSoftInput(true, true); + } } @Test @@ -153,7 +168,12 @@ public class DefaultImeVisibilityApplierTest extends InputMethodManagerServiceTe mVisibilityApplier.applyImeVisibility(mWindowToken, ImeTracker.Token.empty(), STATE_HIDE_IME_NOT_ALWAYS, eq(SoftInputShowHideReason.NOT_SET), mUserId); } - verifyHideSoftInput(true, true); + if (Flags.refactorInsetsController()) { + verifySetImeVisibility(true /* setVisible */, false /* invoked */); + verifySetImeVisibility(false /* setVisible */, true /* invoked */); + } else { + verifyHideSoftInput(true, true); + } } @Test @@ -162,10 +182,16 @@ public class DefaultImeVisibilityApplierTest extends InputMethodManagerServiceTe mVisibilityApplier.applyImeVisibility(mWindowToken, ImeTracker.Token.empty(), STATE_SHOW_IME_IMPLICIT, eq(SoftInputShowHideReason.NOT_SET), mUserId); } - verifyShowSoftInput(true, true, 0 /* showFlags */); + if (Flags.refactorInsetsController()) { + verifySetImeVisibility(true /* setVisible */, true /* invoked */); + verifySetImeVisibility(false /* setVisible */, false /* invoked */); + } else { + verifyShowSoftInput(true, true, 0 /* showFlags */); + } } @Test + @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) public void testApplyImeVisibility_hideImeFromTargetOnSecondaryDisplay() { // Init a IME target client on the secondary display to show IME. mInputMethodManagerService.addClient(mMockInputMethodClient, mMockRemoteInputConnection, @@ -234,8 +260,10 @@ public class DefaultImeVisibilityApplierTest extends InputMethodManagerServiceTe verify(mVisibilityApplier).applyImeVisibility( eq(mWindowToken), any(), eq(STATE_HIDE_IME), eq(SoftInputShowHideReason.NOT_SET), eq(mUserId) /* userId */); - verify(mInputMethodManagerService.mWindowManagerInternal).hideIme( - eq(mWindowToken), eq(displayIdToShowIme), and(not(eq(statsToken)), notNull())); + if (!Flags.refactorInsetsController()) { + verify(mInputMethodManagerService.mWindowManagerInternal).hideIme(eq(mWindowToken), + eq(displayIdToShowIme), and(not(eq(statsToken)), notNull())); + } } } diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java index dc0373239547..a27ad9a0f4e6 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java @@ -20,6 +20,7 @@ import static com.android.server.inputmethod.InputMethodSubtypeSwitchingControll import static com.android.server.inputmethod.InputMethodSubtypeSwitchingController.MODE_RECENT; import static com.android.server.inputmethod.InputMethodSubtypeSwitchingController.MODE_STATIC; import static com.android.server.inputmethod.InputMethodSubtypeSwitchingController.SwitchMode; +import static com.android.server.inputmethod.InputMethodUtils.NOT_A_SUBTYPE_INDEX; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -61,7 +62,6 @@ public final class InputMethodSubtypeSwitchingControllerTest { private static final boolean TEST_IS_VR_IME = false; private static final int TEST_IS_DEFAULT_RES_ID = 0; private static final String SYSTEM_LOCALE = "en_US"; - private static final int NOT_A_SUBTYPE_ID = InputMethodUtils.NOT_A_SUBTYPE_ID; @Rule public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); @@ -103,7 +103,7 @@ public final class InputMethodSubtypeSwitchingControllerTest { TEST_FORCE_DEFAULT, supportsSwitchingToNextInputMethod, TEST_IS_VR_IME); if (subtypes == null) { items.add(new ImeSubtypeListItem(imeName, null /* variableName */, imi, - NOT_A_SUBTYPE_ID, null, SYSTEM_LOCALE)); + NOT_A_SUBTYPE_INDEX, null, SYSTEM_LOCALE)); } else { for (int i = 0; i < subtypes.size(); ++i) { final String subtypeLocale = subtypeLocales.get(i); @@ -913,52 +913,100 @@ public final class InputMethodSubtypeSwitchingControllerTest { final var controller = ControllerImpl.createFrom(null /* currentInstance */, List.of(), List.of()); - assertNoAction(controller, false /* forHardware */, items); - assertNoAction(controller, true /* forHardware */, hardwareItems); + assertNextItemNoAction(controller, false /* forHardware */, items, + null /* expectedNext */); + assertNextItemNoAction(controller, true /* forHardware */, hardwareItems, + null /* expectedNext */); } - /** Verifies that a controller with a single item can't take any actions. */ + /** + * Verifies that a controller with a single item can't update the recency, and cannot switch + * away from the item, but allows switching from unknown items to the single item. + */ @RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP) @Test public void testSingleItemList() { final var items = new ArrayList<ImeSubtypeListItem>(); addTestImeSubtypeListItems(items, "LatinIme", "LatinIme", - List.of("en", "fr"), true /* supportsSwitchingToNextInputMethod */); + null, true /* supportsSwitchingToNextInputMethod */); + final var unknownItems = new ArrayList<ImeSubtypeListItem>(); + addTestImeSubtypeListItems(unknownItems, "UnknownIme", "UnknownIme", + List.of("en", "fr", "it"), true /* supportsSwitchingToNextInputMethod */); final var hardwareItems = new ArrayList<ImeSubtypeListItem>(); addTestImeSubtypeListItems(hardwareItems, "HardwareIme", "HardwareIme", - List.of("en", "fr"), true /* supportsSwitchingToNextInputMethod */); + null, true /* supportsSwitchingToNextInputMethod */); + final var unknownHardwareItems = new ArrayList<ImeSubtypeListItem>(); + addTestImeSubtypeListItems(unknownHardwareItems, "HardwareUnknownIme", "HardwareUnknownIme", + List.of("en", "fr", "it"), true /* supportsSwitchingToNextInputMethod */); final var controller = ControllerImpl.createFrom(null /* currentInstance */, - List.of(items.get(0)), List.of(hardwareItems.get(0))); - - assertNoAction(controller, false /* forHardware */, items); - assertNoAction(controller, true /* forHardware */, hardwareItems); + items, hardwareItems); + + assertNextItemNoAction(controller, false /* forHardware */, items, + null /* expectedNext */); + assertNextItemNoAction(controller, false /* forHardware */, unknownItems, + items.get(0)); + assertNextItemNoAction(controller, true /* forHardware */, hardwareItems, + null /* expectedNext */); + assertNextItemNoAction(controller, true /* forHardware */, unknownHardwareItems, + hardwareItems.get(0)); } - /** Verifies that a controller can't take any actions for unknown items. */ + /** + * Verifies that the recency cannot be updated for unknown items, but switching from unknown + * items reaches the most recent known item. + */ @RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP) @Test public void testUnknownItems() { final var items = new ArrayList<ImeSubtypeListItem>(); addTestImeSubtypeListItems(items, "LatinIme", "LatinIme", - List.of("en", "fr"), true /* supportsSwitchingToNextInputMethod */); + List.of("en", "fr", "it"), true /* supportsSwitchingToNextInputMethod */); + + final var english = items.get(0); + final var french = items.get(1); + final var italian = items.get(2); + final var unknownItems = new ArrayList<ImeSubtypeListItem>(); addTestImeSubtypeListItems(unknownItems, "UnknownIme", "UnknownIme", - List.of("en", "fr"), true /* supportsSwitchingToNextInputMethod */); + List.of("en", "fr", "it"), true /* supportsSwitchingToNextInputMethod */); final var hardwareItems = new ArrayList<ImeSubtypeListItem>(); addTestImeSubtypeListItems(hardwareItems, "HardwareIme", "HardwareIme", - List.of("en", "fr"), true /* supportsSwitchingToNextInputMethod */); + List.of("en", "fr", "it"), true /* supportsSwitchingToNextInputMethod */); final var unknownHardwareItems = new ArrayList<ImeSubtypeListItem>(); addTestImeSubtypeListItems(unknownHardwareItems, "HardwareUnknownIme", "HardwareUnknownIme", - List.of("en", "fr"), true /* supportsSwitchingToNextInputMethod */); + List.of("en", "fr", "it"), true /* supportsSwitchingToNextInputMethod */); final var controller = ControllerImpl.createFrom(null /* currentInstance */, items, hardwareItems); - assertNoAction(controller, false /* forHardware */, unknownItems); - assertNoAction(controller, true /* forHardware */, unknownHardwareItems); + assertTrue("Recency updated for french IME", onUserAction(controller, french)); + + final var recencyItems = List.of(french, english, italian); + + assertNextItemNoAction(controller, false /* forHardware */, unknownItems, + french); + assertNextItemNoAction(controller, true /* forHardware */, unknownHardwareItems, + hardwareItems.get(0)); + + // Known items must not be able to switch to unknown items. + assertNextOrder(controller, false /* forHardware */, MODE_STATIC, items, + List.of(items)); + assertNextOrder(controller, false /* forHardware */, MODE_RECENT, recencyItems, + List.of(recencyItems)); + assertNextOrder(controller, false /* forHardware */, MODE_AUTO, true /* forward */, + recencyItems, List.of(recencyItems)); + assertNextOrder(controller, false /* forHardware */, MODE_AUTO, false /* forward */, + items.reversed(), List.of(items.reversed())); + + assertNextOrder(controller, true /* forHardware */, MODE_STATIC, hardwareItems, + List.of(hardwareItems)); + assertNextOrder(controller, true /* forHardware */, MODE_RECENT, hardwareItems, + List.of(hardwareItems)); + assertNextOrder(controller, true /* forHardware */, MODE_AUTO, hardwareItems, + List.of(hardwareItems)); } /** Verifies that the IME name does influence the comparison order. */ @@ -1199,25 +1247,26 @@ public final class InputMethodSubtypeSwitchingControllerTest { } /** - * Verifies that no next items can be found, and the recency cannot be updated for the + * Verifies that the expected next item is returned, and the recency cannot be updated for the * given items. * - * @param controller the controller to verify the items on. - * @param forHardware whether to try finding the next hardware item, or software item. - * @param items the list of items to verify. + * @param controller the controller to verify the items on. + * @param forHardware whether to try finding the next hardware item, or software item. + * @param items the list of items to verify. + * @param expectedNext the expected next item. */ - private void assertNoAction(@NonNull ControllerImpl controller, boolean forHardware, - @NonNull List<ImeSubtypeListItem> items) { + private void assertNextItemNoAction(@NonNull ControllerImpl controller, boolean forHardware, + @NonNull List<ImeSubtypeListItem> items, @Nullable ImeSubtypeListItem expectedNext) { for (var item : items) { for (int mode = MODE_STATIC; mode <= MODE_AUTO; mode++) { assertNextItem(controller, forHardware, false /* onlyCurrentIme */, mode, - false /* forward */, item, null /* expectedNext */); + false /* forward */, item, expectedNext); assertNextItem(controller, forHardware, false /* onlyCurrentIme */, mode, - true /* forward */, item, null /* expectedNext */); + true /* forward */, item, expectedNext); assertNextItem(controller, forHardware, true /* onlyCurrentIme */, mode, - false /* forward */, item, null /* expectedNext */); + false /* forward */, item, expectedNext); assertNextItem(controller, forHardware, true /* onlyCurrentIme */, mode, - true /* forward */, item, null /* expectedNext */); + true /* forward */, item, expectedNext); } assertFalse("User action shouldn't have updated the recency.", diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java index d7af443036bf..c272430d5c78 100644 --- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java +++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java @@ -924,6 +924,54 @@ public class PackageManagerSettingsTests { } @Test + public void testSameVersions_writeReadUsesStaticLibraries() { + Settings settings = makeSettings(); + PackageSetting packageSetting = createPackageSetting(PACKAGE_NAME_1); + packageSetting.setAppId(Process.FIRST_APPLICATION_UID); + + final String libOne = "one"; + final String libTwo = "two"; + final long versionOne = 311; + packageSetting.setUsesStaticLibraries(new String[] { libOne, libTwo }); + packageSetting.setUsesStaticLibrariesVersions(new long[] { versionOne, versionOne }); + settings.mPackages.put(PACKAGE_NAME_1, packageSetting); + + settings.writeLPr(computer, /* sync= */ true); + settings.mPackages.clear(); + + assertThat(settings.readLPw(computer, createFakeUsers()), is(true)); + PackageSetting resultSetting = settings.getPackageLPr(PACKAGE_NAME_1); + assertThat(resultSetting.getUsesStaticLibraries()[0], is(libOne)); + assertThat(resultSetting.getUsesStaticLibraries()[1], is(libTwo)); + assertThat(resultSetting.getUsesStaticLibrariesVersions()[0], is(versionOne)); + assertThat(resultSetting.getUsesStaticLibrariesVersions()[1], is(versionOne)); + } + + @Test + public void testSameLibNames_writeReadUsesStaticLibraries() { + Settings settings = makeSettings(); + PackageSetting packageSetting = createPackageSetting(PACKAGE_NAME_1); + packageSetting.setAppId(Process.FIRST_APPLICATION_UID); + + final String libOne = "one"; + final long versionOne = 311; + final long versionTwo = 330; + packageSetting.setUsesStaticLibraries(new String[] { libOne, libOne}); + packageSetting.setUsesStaticLibrariesVersions(new long[] { versionOne, versionTwo }); + settings.mPackages.put(PACKAGE_NAME_1, packageSetting); + + settings.writeLPr(computer, /* sync= */ true); + settings.mPackages.clear(); + + assertThat(settings.readLPw(computer, createFakeUsers()), is(true)); + PackageSetting resultSetting = settings.getPackageLPr(PACKAGE_NAME_1); + assertThat(resultSetting.getUsesStaticLibraries().length, is(1)); + assertThat(resultSetting.getUsesStaticLibrariesVersions().length, is(1)); + assertThat(resultSetting.getUsesStaticLibraries()[0], is(libOne)); + assertThat(resultSetting.getUsesStaticLibrariesVersions()[0], is(versionTwo)); + } + + @Test public void testWriteReadUsesSdkLibraries() { final Settings settingsUnderTest = makeSettings(); final PackageSetting ps1 = createPackageSetting(PACKAGE_NAME_1); @@ -1008,6 +1056,65 @@ public class PackageManagerSettingsTests { } @Test + public void testSameVersions_writeReadUsesSdkLibraries() { + Settings settings = makeSettings(); + PackageSetting packageSetting = createPackageSetting(PACKAGE_NAME_1); + packageSetting.setAppId(Process.FIRST_APPLICATION_UID); + + final String libOne = "one"; + final String libTwo = "two"; + final long versionOne = 311; + final boolean optional = false; + packageSetting.setUsesSdkLibraries(new String[] { libOne, libTwo }); + packageSetting.setUsesSdkLibrariesVersionsMajor(new long[] { versionOne, versionOne }); + packageSetting.setUsesSdkLibrariesOptional(new boolean[] { optional, optional }); + settings.mPackages.put(PACKAGE_NAME_1, packageSetting); + + settings.writeLPr(computer, /* sync= */ true); + settings.mPackages.clear(); + + assertThat(settings.readLPw(computer, createFakeUsers()), is(true)); + PackageSetting resultSetting = settings.getPackageLPr(PACKAGE_NAME_1); + + assertThat(resultSetting.getUsesSdkLibraries()[0], is(libOne)); + assertThat(resultSetting.getUsesSdkLibraries()[1], is(libTwo)); + assertThat(resultSetting.getUsesSdkLibrariesVersionsMajor()[0], is(versionOne)); + assertThat(resultSetting.getUsesSdkLibrariesVersionsMajor()[1], is(versionOne)); + assertThat(resultSetting.getUsesSdkLibrariesOptional()[0], is(optional)); + assertThat(resultSetting.getUsesSdkLibrariesOptional()[1], is(optional)); + } + + @Test + public void testSameLibNames_writeReadUsesSdkLibraries() { + Settings settings = makeSettings(); + PackageSetting packageSetting = createPackageSetting(PACKAGE_NAME_1); + packageSetting.setAppId(Process.FIRST_APPLICATION_UID); + + final String libOne = "one"; + final long versionOne = 311; + final long versionTwo = 330; + final boolean optionalOne = false; + final boolean optionalTwo = true; + packageSetting.setUsesSdkLibraries(new String[] { libOne, libOne }); + packageSetting.setUsesSdkLibrariesVersionsMajor(new long[] { versionOne, versionTwo }); + packageSetting.setUsesSdkLibrariesOptional(new boolean[] { optionalOne, optionalTwo }); + settings.mPackages.put(PACKAGE_NAME_1, packageSetting); + + settings.writeLPr(computer, /* sync= */ true); + settings.mPackages.clear(); + + assertThat(settings.readLPw(computer, createFakeUsers()), is(true)); + PackageSetting resultSetting = settings.getPackageLPr(PACKAGE_NAME_1); + + assertThat(resultSetting.getUsesSdkLibraries().length, is(1)); + assertThat(resultSetting.getUsesSdkLibrariesVersionsMajor().length, is(1)); + assertThat(resultSetting.getUsesSdkLibrariesOptional().length, is(1)); + assertThat(resultSetting.getUsesSdkLibraries()[0], is(libOne)); + assertThat(resultSetting.getUsesSdkLibrariesVersionsMajor()[0], is(versionTwo)); + assertThat(resultSetting.getUsesSdkLibrariesOptional()[0], is(optionalTwo)); + } + + @Test public void testWriteReadPendingRestore() { Settings settings = makeSettings(); PackageSetting packageSetting = createPackageSetting(PACKAGE_NAME_1); diff --git a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java index b980ca05b609..30de0e8c7981 100644 --- a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java @@ -512,6 +512,9 @@ public final class AlarmManagerServiceTest { when(mPermissionManagerInternal.getAppOpPermissionPackages( SCHEDULE_EXACT_ALARM)).thenReturn(EmptyArray.STRING); + // Initialize timestamps with arbitrary values of time + mNowElapsedTest = 12; + mNowRtcTest = 345; mInjector = new Injector(mMockContext); mService = new AlarmManagerService(mMockContext, mInjector); spyOn(mService); @@ -774,6 +777,61 @@ public final class AlarmManagerServiceTest { } @Test + public void timeChangeBroadcastForward() throws Exception { + final long timeDelta = 12345; + // AlarmManagerService sends the broadcast if real time clock proceeds 1000ms more than boot + // time clock. + mNowRtcTest += timeDelta + 1001; + mNowElapsedTest += timeDelta; + mTestTimer.expire(TIME_CHANGED_MASK); + + verify(mMockContext) + .sendBroadcastAsUser( + argThat((intent) -> intent.getAction() == Intent.ACTION_TIME_CHANGED), + eq(UserHandle.ALL), + isNull(), + any()); + } + + @Test + public void timeChangeBroadcastBackward() throws Exception { + final long timeDelta = 12345; + // AlarmManagerService sends the broadcast if real time clock proceeds 1000ms less than boot + // time clock. + mNowRtcTest += timeDelta - 1001; + mNowElapsedTest += timeDelta; + mTestTimer.expire(TIME_CHANGED_MASK); + + verify(mMockContext) + .sendBroadcastAsUser( + argThat((intent) -> intent.getAction() == Intent.ACTION_TIME_CHANGED), + eq(UserHandle.ALL), + isNull(), + any()); + } + + @Test + public void timeChangeFilterMinorAdjustment() throws Exception { + final long timeDelta = 12345; + // AlarmManagerService does not send the broadcast if real time clock proceeds within 1000ms + // than boot time clock. + mNowRtcTest += timeDelta + 1000; + mNowElapsedTest += timeDelta; + mTestTimer.expire(TIME_CHANGED_MASK); + + mNowRtcTest += timeDelta - 1000; + mNowElapsedTest += timeDelta; + mTestTimer.expire(TIME_CHANGED_MASK); + + verify(mMockContext, never()) + .sendBroadcastAsUser( + argThat((intent) -> intent.getAction() == Intent.ACTION_TIME_CHANGED), + any(), + any(), + any()); + } + + @Test public void testSingleAlarmExpiration() throws Exception { final long triggerTime = mNowElapsedTest + 5000; final PendingIntent alarmPi = getNewMockPendingIntent(); diff --git a/services/tests/mockingservicestests/src/com/android/server/utils/quota/CountQuotaTrackerTest.java b/services/tests/mockingservicestests/src/com/android/server/utils/quota/CountQuotaTrackerTest.java index 0d14c9f02677..1b9f8d1f90c3 100644 --- a/services/tests/mockingservicestests/src/com/android/server/utils/quota/CountQuotaTrackerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/utils/quota/CountQuotaTrackerTest.java @@ -40,6 +40,8 @@ import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.AlarmManager; import android.content.BroadcastReceiver; import android.content.Context; @@ -224,6 +226,45 @@ public class CountQuotaTrackerTest { } } + private LongArrayQueue getEvents(int userId, String packageName, String tag) { + synchronized (mQuotaTracker.mLock) { + return mQuotaTracker.getEvents(userId, packageName, tag); + } + } + + private void updateExecutionStats(final int userId, @NonNull final String packageName, + @Nullable final String tag, @NonNull ExecutionStats stats) { + synchronized (mQuotaTracker.mLock) { + mQuotaTracker.updateExecutionStatsLocked(userId, packageName, tag, stats); + } + } + + private ExecutionStats getExecutionStats(final int userId, @NonNull final String packageName, + @Nullable final String tag) { + synchronized (mQuotaTracker.mLock) { + return mQuotaTracker.getExecutionStatsLocked(userId, packageName, tag); + } + } + + private void maybeScheduleStartAlarm(final int userId, @NonNull final String packageName, + @Nullable final String tag) { + synchronized (mQuotaTracker.mLock) { + mQuotaTracker.maybeScheduleStartAlarmLocked(userId, packageName, tag); + } + } + + private void maybeScheduleCleanupAlarm() { + synchronized (mQuotaTracker.mLock) { + mQuotaTracker.maybeScheduleCleanupAlarmLocked(); + } + } + + private void deleteObsoleteEvents() { + synchronized (mQuotaTracker.mLock) { + mQuotaTracker.deleteObsoleteEventsLocked(); + } + } + @Test public void testDeleteObsoleteEventsLocked() { // Count window size should only apply to event list. @@ -243,9 +284,9 @@ public class CountQuotaTrackerTest { expectedEvents.addLast(now - HOUR_IN_MILLIS); expectedEvents.addLast(now - 1); - mQuotaTracker.deleteObsoleteEventsLocked(); + deleteObsoleteEvents(); - LongArrayQueue remainingEvents = mQuotaTracker.getEvents(TEST_USER_ID, TEST_PACKAGE, + LongArrayQueue remainingEvents = getEvents(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); assertTrue(longArrayQueueEquals(expectedEvents, remainingEvents)); } @@ -270,15 +311,15 @@ public class CountQuotaTrackerTest { removal.putExtra(Intent.EXTRA_UID, TEST_UID); mReceiver.onReceive(mContext, removal); assertNull( - mQuotaTracker.getEvents(TEST_USER_ID, "com.android.test.remove", "tag1")); + getEvents(TEST_USER_ID, "com.android.test.remove", "tag1")); assertNull( - mQuotaTracker.getEvents(TEST_USER_ID, "com.android.test.remove", "tag2")); + getEvents(TEST_USER_ID, "com.android.test.remove", "tag2")); assertNull( - mQuotaTracker.getEvents(TEST_USER_ID, "com.android.test.remove", "tag3")); + getEvents(TEST_USER_ID, "com.android.test.remove", "tag3")); assertTrue(longArrayQueueEquals(expected1, - mQuotaTracker.getEvents(TEST_USER_ID, "com.android.test.stay", "tag1"))); + getEvents(TEST_USER_ID, "com.android.test.stay", "tag1"))); assertTrue(longArrayQueueEquals(expected2, - mQuotaTracker.getEvents(TEST_USER_ID, "com.android.test.stay", "tag2"))); + getEvents(TEST_USER_ID, "com.android.test.stay", "tag2"))); } @Test @@ -298,10 +339,10 @@ public class CountQuotaTrackerTest { Intent removal = new Intent(Intent.ACTION_USER_REMOVED); removal.putExtra(Intent.EXTRA_USER_HANDLE, TEST_USER_ID); mReceiver.onReceive(mContext, removal); - assertNull(mQuotaTracker.getEvents(TEST_USER_ID, TEST_PACKAGE, "tag1")); - assertNull(mQuotaTracker.getEvents(TEST_USER_ID, TEST_PACKAGE, "tag2")); - assertNull(mQuotaTracker.getEvents(TEST_USER_ID, TEST_PACKAGE, "tag3")); - longArrayQueueEquals(expected, mQuotaTracker.getEvents(10, TEST_PACKAGE, "tag4")); + assertNull(getEvents(TEST_USER_ID, TEST_PACKAGE, "tag1")); + assertNull(getEvents(TEST_USER_ID, TEST_PACKAGE, "tag2")); + assertNull(getEvents(TEST_USER_ID, TEST_PACKAGE, "tag3")); + longArrayQueueEquals(expected, getEvents(10, TEST_PACKAGE, "tag4")); } @Test @@ -323,7 +364,7 @@ public class CountQuotaTrackerTest { inputStats.countLimit = expectedStats.countLimit = 3; // Invalid time is now +24 hours since there are no sessions at all for the app. expectedStats.expirationTimeElapsed = now + 24 * HOUR_IN_MILLIS; - mQuotaTracker.updateExecutionStatsLocked(TEST_USER_ID, "com.android.test.not.run", TEST_TAG, + updateExecutionStats(TEST_USER_ID, "com.android.test.not.run", TEST_TAG, inputStats); assertEquals(expectedStats, inputStats); @@ -333,19 +374,19 @@ public class CountQuotaTrackerTest { // Invalid time is now since there was an event exactly windowSizeMs ago. expectedStats.expirationTimeElapsed = now; expectedStats.countInWindow = 1; - mQuotaTracker.updateExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats); + updateExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats); assertEquals(expectedStats, inputStats); inputStats.windowSizeMs = expectedStats.windowSizeMs = 3 * MINUTE_IN_MILLIS; expectedStats.expirationTimeElapsed = now + 2 * MINUTE_IN_MILLIS; expectedStats.countInWindow = 1; - mQuotaTracker.updateExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats); + updateExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats); assertEquals(expectedStats, inputStats); inputStats.windowSizeMs = expectedStats.windowSizeMs = 4 * MINUTE_IN_MILLIS; expectedStats.expirationTimeElapsed = now + 3 * MINUTE_IN_MILLIS; expectedStats.countInWindow = 1; - mQuotaTracker.updateExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats); + updateExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats); assertEquals(expectedStats, inputStats); inputStats.windowSizeMs = expectedStats.windowSizeMs = 49 * MINUTE_IN_MILLIS; @@ -353,13 +394,13 @@ public class CountQuotaTrackerTest { // minutes. expectedStats.expirationTimeElapsed = now + 44 * MINUTE_IN_MILLIS; expectedStats.countInWindow = 2; - mQuotaTracker.updateExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats); + updateExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats); assertEquals(expectedStats, inputStats); inputStats.windowSizeMs = expectedStats.windowSizeMs = 50 * MINUTE_IN_MILLIS; expectedStats.expirationTimeElapsed = now + 45 * MINUTE_IN_MILLIS; expectedStats.countInWindow = 2; - mQuotaTracker.updateExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats); + updateExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats); assertEquals(expectedStats, inputStats); inputStats.windowSizeMs = expectedStats.windowSizeMs = HOUR_IN_MILLIS; @@ -370,28 +411,28 @@ public class CountQuotaTrackerTest { // App is at event count limit but the oldest session is at the edge of the window, so // in quota time is now. expectedStats.inQuotaTimeElapsed = now; - mQuotaTracker.updateExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats); + updateExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats); assertEquals(expectedStats, inputStats); inputStats.windowSizeMs = expectedStats.windowSizeMs = 2 * HOUR_IN_MILLIS; expectedStats.expirationTimeElapsed = now + HOUR_IN_MILLIS; expectedStats.countInWindow = 3; expectedStats.inQuotaTimeElapsed = now + HOUR_IN_MILLIS; - mQuotaTracker.updateExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats); + updateExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats); assertEquals(expectedStats, inputStats); inputStats.windowSizeMs = expectedStats.windowSizeMs = 5 * HOUR_IN_MILLIS; expectedStats.expirationTimeElapsed = now + HOUR_IN_MILLIS; expectedStats.countInWindow = 4; expectedStats.inQuotaTimeElapsed = now + 4 * HOUR_IN_MILLIS; - mQuotaTracker.updateExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats); + updateExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats); assertEquals(expectedStats, inputStats); inputStats.windowSizeMs = expectedStats.windowSizeMs = 6 * HOUR_IN_MILLIS; expectedStats.expirationTimeElapsed = now + 2 * HOUR_IN_MILLIS; expectedStats.countInWindow = 4; expectedStats.inQuotaTimeElapsed = now + 5 * HOUR_IN_MILLIS; - mQuotaTracker.updateExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats); + updateExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats); assertEquals(expectedStats, inputStats); } @@ -428,7 +469,7 @@ public class CountQuotaTrackerTest { expectedStats.countInWindow = 1; mCategorizer.mCategoryToUse = ACTIVE_BUCKET_CATEGORY; assertEquals(expectedStats, - mQuotaTracker.getExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG)); + getExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG)); // Working expectedStats.expirationTimeElapsed = now; @@ -437,7 +478,7 @@ public class CountQuotaTrackerTest { expectedStats.countInWindow = 2; mCategorizer.mCategoryToUse = WORKING_SET_BUCKET_CATEGORY; assertEquals(expectedStats, - mQuotaTracker.getExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG)); + getExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG)); // Frequent expectedStats.expirationTimeElapsed = now + HOUR_IN_MILLIS; @@ -447,7 +488,7 @@ public class CountQuotaTrackerTest { expectedStats.inQuotaTimeElapsed = now + HOUR_IN_MILLIS; mCategorizer.mCategoryToUse = FREQUENT_BUCKET_CATEGORY; assertEquals(expectedStats, - mQuotaTracker.getExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG)); + getExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG)); // Rare expectedStats.expirationTimeElapsed = now + HOUR_IN_MILLIS; @@ -457,7 +498,7 @@ public class CountQuotaTrackerTest { expectedStats.inQuotaTimeElapsed = now + 19 * HOUR_IN_MILLIS; mCategorizer.mCategoryToUse = RARE_BUCKET_CATEGORY; assertEquals(expectedStats, - mQuotaTracker.getExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG)); + getExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG)); } /** @@ -481,7 +522,7 @@ public class CountQuotaTrackerTest { expectedStats.countInWindow = 3; expectedStats.expirationTimeElapsed = 2 * HOUR_IN_MILLIS + 30_000; assertEquals(expectedStats, - mQuotaTracker.getExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG)); + getExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG)); } @Test @@ -556,20 +597,20 @@ public class CountQuotaTrackerTest { mQuotaTracker.setCountLimit(SINGLE_CATEGORY, 5, 24 * HOUR_IN_MILLIS); // No sessions saved yet. - mQuotaTracker.maybeScheduleCleanupAlarmLocked(); + maybeScheduleCleanupAlarm(); verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_CLEANUP), any(), any()); // Test with only one timing session saved. final long now = mInjector.getElapsedRealtime(); logEventAt(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, now - 6 * HOUR_IN_MILLIS); - mQuotaTracker.maybeScheduleCleanupAlarmLocked(); + maybeScheduleCleanupAlarm(); verify(mAlarmManager, timeout(1000).times(1)) .set(anyInt(), eq(now + 18 * HOUR_IN_MILLIS), eq(TAG_CLEANUP), any(), any()); // Test with new (more recent) timing sessions saved. AlarmManger shouldn't be called again. logEventAt(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, now - 3 * HOUR_IN_MILLIS); logEventAt(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, now - HOUR_IN_MILLIS); - mQuotaTracker.maybeScheduleCleanupAlarmLocked(); + maybeScheduleCleanupAlarm(); verify(mAlarmManager, times(1)) .set(anyInt(), eq(now + 18 * HOUR_IN_MILLIS), eq(TAG_CLEANUP), any(), any()); } @@ -587,14 +628,14 @@ public class CountQuotaTrackerTest { mQuotaTracker.setCountLimit(SINGLE_CATEGORY, 10, 8 * HOUR_IN_MILLIS); // No sessions saved yet. - mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); + maybeScheduleStartAlarm(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); verify(mAlarmManager, never()).setWindow( anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); // Test with timing sessions out of window. final long now = mInjector.getElapsedRealtime(); logEventsAt(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, now - 10 * HOUR_IN_MILLIS, 20); - mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); + maybeScheduleStartAlarm(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); verify(mAlarmManager, never()).setWindow( anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); @@ -602,26 +643,26 @@ public class CountQuotaTrackerTest { final long start = now - (6 * HOUR_IN_MILLIS); final long expectedAlarmTime = start + 8 * HOUR_IN_MILLIS; logEventsAt(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, start, 5); - mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); + maybeScheduleStartAlarm(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); verify(mAlarmManager, never()).setWindow( anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); // Add some more sessions, but still in quota. logEventsAt(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, now - 3 * HOUR_IN_MILLIS, 1); logEventsAt(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, now - HOUR_IN_MILLIS, 3); - mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); + maybeScheduleStartAlarm(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); verify(mAlarmManager, never()).setWindow( anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); // Test when out of quota. logEventsAt(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, now - HOUR_IN_MILLIS, 1); - mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); + maybeScheduleStartAlarm(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); verify(mAlarmManager, timeout(1000).times(1)).setWindow( anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); // Alarm already scheduled, so make sure it's not scheduled again. - mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); + maybeScheduleStartAlarm(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); verify(mAlarmManager, times(1)).setWindow( anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); @@ -656,7 +697,7 @@ public class CountQuotaTrackerTest { // Start in ACTIVE bucket. mCategorizer.mCategoryToUse = ACTIVE_BUCKET_CATEGORY; - mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); + maybeScheduleStartAlarm(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); inOrder.verify(mAlarmManager, never()) .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); @@ -665,40 +706,40 @@ public class CountQuotaTrackerTest { // And down from there. final long expectedWorkingAlarmTime = outOfQuotaTime + (2 * HOUR_IN_MILLIS); mCategorizer.mCategoryToUse = WORKING_SET_BUCKET_CATEGORY; - mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); + maybeScheduleStartAlarm(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); inOrder.verify(mAlarmManager, timeout(1000).times(1)) .setWindow(anyInt(), eq(expectedWorkingAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); final long expectedFrequentAlarmTime = outOfQuotaTime + (8 * HOUR_IN_MILLIS); mCategorizer.mCategoryToUse = FREQUENT_BUCKET_CATEGORY; - mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); + maybeScheduleStartAlarm(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); inOrder.verify(mAlarmManager, timeout(1000).times(1)) .setWindow(anyInt(), eq(expectedFrequentAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); final long expectedRareAlarmTime = outOfQuotaTime + (24 * HOUR_IN_MILLIS); mCategorizer.mCategoryToUse = RARE_BUCKET_CATEGORY; - mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); + maybeScheduleStartAlarm(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); inOrder.verify(mAlarmManager, timeout(1000).times(1)) .setWindow(anyInt(), eq(expectedRareAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); // And back up again. mCategorizer.mCategoryToUse = FREQUENT_BUCKET_CATEGORY; - mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); + maybeScheduleStartAlarm(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); inOrder.verify(mAlarmManager, timeout(1000).times(1)) .setWindow(anyInt(), eq(expectedFrequentAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); mCategorizer.mCategoryToUse = WORKING_SET_BUCKET_CATEGORY; - mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); + maybeScheduleStartAlarm(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); inOrder.verify(mAlarmManager, timeout(1000).times(1)) .setWindow(anyInt(), eq(expectedWorkingAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class)); mCategorizer.mCategoryToUse = ACTIVE_BUCKET_CATEGORY; - mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); + maybeScheduleStartAlarm(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); inOrder.verify(mAlarmManager, timeout(1000).times(1)) .cancel(any(AlarmManager.OnAlarmListener.class)); inOrder.verify(mAlarmManager, timeout(1000).times(0)) @@ -745,14 +786,14 @@ public class CountQuotaTrackerTest { mQuotaTracker.setCountLimit(SINGLE_CATEGORY, 10, 2 * HOUR_IN_MILLIS); ExecutionStats stats = - mQuotaTracker.getExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); + getExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); assertEquals(0, stats.countInWindow); for (int i = 0; i < 10; ++i) { mQuotaTracker.noteEvent(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); advanceElapsedClock(10 * SECOND_IN_MILLIS); - mQuotaTracker.updateExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, stats); + updateExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, stats); assertEquals(0, stats.countInWindow); } } @@ -766,14 +807,14 @@ public class CountQuotaTrackerTest { mQuotaTracker.setCountLimit(SINGLE_CATEGORY, 10, 2 * HOUR_IN_MILLIS); ExecutionStats stats = - mQuotaTracker.getExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); + getExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); assertEquals(0, stats.countInWindow); for (int i = 0; i < 10; ++i) { mQuotaTracker.noteEvent(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); advanceElapsedClock(10 * SECOND_IN_MILLIS); - mQuotaTracker.updateExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, stats); + updateExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, stats); assertEquals(i + 1, stats.countInWindow); } } @@ -785,14 +826,14 @@ public class CountQuotaTrackerTest { mQuotaTracker.setCountLimit(SINGLE_CATEGORY, 10, 2 * HOUR_IN_MILLIS); ExecutionStats stats = - mQuotaTracker.getExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); + getExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); assertEquals(0, stats.countInWindow); for (int i = 0; i < 10; ++i) { mQuotaTracker.noteEvent(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); advanceElapsedClock(10 * SECOND_IN_MILLIS); - mQuotaTracker.updateExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, stats); + updateExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, stats); assertEquals(0, stats.countInWindow); } } @@ -806,14 +847,14 @@ public class CountQuotaTrackerTest { mQuotaTracker.setCountLimit(SINGLE_CATEGORY, 10, 2 * HOUR_IN_MILLIS); ExecutionStats stats = - mQuotaTracker.getExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); + getExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); assertEquals(0, stats.countInWindow); for (int i = 0; i < 10; ++i) { mQuotaTracker.noteEvent(TEST_USER_ID, TEST_PACKAGE, TEST_TAG); advanceElapsedClock(10 * SECOND_IN_MILLIS); - mQuotaTracker.updateExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, stats); + updateExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, stats); assertEquals(i + 1, stats.countInWindow); } } 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/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java index dbab54b76a2e..a8e350b05f18 100644 --- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java @@ -19,6 +19,8 @@ package com.android.server.am; import static android.Manifest.permission.INTERACT_ACROSS_PROFILES; import static android.Manifest.permission.INTERACT_ACROSS_USERS; import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL; +import static android.app.ActivityManager.STOP_USER_ON_SWITCH_TRUE; +import static android.app.ActivityManager.STOP_USER_ON_SWITCH_FALSE; import static android.app.ActivityManagerInternal.ALLOW_FULL_ONLY; import static android.app.ActivityManagerInternal.ALLOW_NON_FULL; import static android.app.ActivityManagerInternal.ALLOW_NON_FULL_IN_PROFILE; @@ -201,7 +203,8 @@ public class UserControllerTest { doNothing().when(mInjector).systemServiceManagerOnUserStopped(anyInt()); doNothing().when(mInjector).systemServiceManagerOnUserCompletedEvent( anyInt(), anyInt()); - doNothing().when(mInjector).activityManagerForceStopPackage(anyInt(), anyString()); + doNothing().when(mInjector).activityManagerForceStopUserPackages(anyInt(), + anyString(), anyBoolean()); doNothing().when(mInjector).activityManagerOnUserStopped(anyInt()); doNothing().when(mInjector).clearBroadcastQueueForUser(anyInt()); doNothing().when(mInjector).taskSupervisorRemoveUser(anyInt()); @@ -936,6 +939,61 @@ public class UserControllerTest { new HashSet<>(mUserController.getRunningUsersLU())); } + @Test + public void testEarlyPackageKillEnabledForUserSwitch_enabled() { + mUserController.setInitialConfig(/* userSwitchUiEnabled= */ true, + /* maxRunningUsers= */ 4, /* delayUserDataLocking= */ true, + /* backgroundUserScheduledStopTimeSecs= */ -1); + + assertTrue(mUserController + .isEarlyPackageKillEnabledForUserSwitch(TEST_USER_ID, TEST_USER_ID1)); + } + + @Test + public void testEarlyPackageKillEnabledForUserSwitch_withoutDelayUserDataLocking() { + mUserController.setInitialConfig(/* userSwitchUiEnabled= */ true, + /* maxRunningUsers= */ 4, /* delayUserDataLocking= */ false, + /* backgroundUserScheduledStopTimeSecs= */ -1); + + assertFalse(mUserController + .isEarlyPackageKillEnabledForUserSwitch(TEST_USER_ID, TEST_USER_ID1)); + } + + @Test + public void testEarlyPackageKillEnabledForUserSwitch_withPrevSystemUser() { + mUserController.setInitialConfig(/* userSwitchUiEnabled= */ true, + /* maxRunningUsers= */ 4, /* delayUserDataLocking= */ true, + /* backgroundUserScheduledStopTimeSecs= */ -1); + + assertFalse(mUserController + .isEarlyPackageKillEnabledForUserSwitch(SYSTEM_USER_ID, TEST_USER_ID1)); + } + + @Test + public void testEarlyPackageKillEnabledForUserSwitch_stopUserOnSwitchModeOn() { + mUserController.setInitialConfig(/* userSwitchUiEnabled= */ true, + /* maxRunningUsers= */ 4, /* delayUserDataLocking= */ false, + /* backgroundUserScheduledStopTimeSecs= */ -1); + + mUserController.setStopUserOnSwitch(STOP_USER_ON_SWITCH_TRUE); + + assertTrue(mUserController + .isEarlyPackageKillEnabledForUserSwitch(TEST_USER_ID, TEST_USER_ID1)); + } + + @Test + public void testEarlyPackageKillEnabledForUserSwitch_stopUserOnSwitchModeOff() { + mUserController.setInitialConfig(/* userSwitchUiEnabled= */ true, + /* maxRunningUsers= */ 4, /* delayUserDataLocking= */ true, + /* backgroundUserScheduledStopTimeSecs= */ -1); + + mUserController.setStopUserOnSwitch(STOP_USER_ON_SWITCH_FALSE); + + assertFalse(mUserController + .isEarlyPackageKillEnabledForUserSwitch(TEST_USER_ID, TEST_USER_ID1)); + } + + /** * Test that, in getRunningUsersLU, parents come after their profile, even if the profile was * started afterwards. 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/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java index 3d6884925098..dddab657be14 100644 --- a/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java @@ -108,6 +108,7 @@ import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.CALLS_REAL_METHODS; @@ -165,6 +166,7 @@ import android.os.PowerExemptionManager; import android.os.PowerManager; import android.os.PowerManagerInternal; import android.os.PowerSaveState; +import android.os.Process; import android.os.RemoteException; import android.os.SimpleClock; import android.os.SystemClock; @@ -197,6 +199,7 @@ import androidx.test.filters.FlakyTest; import androidx.test.filters.MediumTest; import androidx.test.runner.AndroidJUnit4; +import com.android.internal.util.ArrayUtils; import com.android.internal.util.test.BroadcastInterceptingContext; import com.android.internal.util.test.BroadcastInterceptingContext.FutureIntent; import com.android.internal.util.test.FsUtil; @@ -2310,6 +2313,70 @@ public class NetworkPolicyManagerServiceTest { assertTrue(mService.isUidNetworkingBlocked(UID_A, false)); } + @SuppressWarnings("GuardedBy") // For not holding mUidRulesFirstLock + @Test + @RequiresFlagsEnabled(Flags.FLAG_NEVER_APPLY_RULES_TO_CORE_UIDS) + public void testRulesNeverAppliedToCoreUids() throws Exception { + clearInvocations(mNetworkManager); + + final int coreAppId = Process.FIRST_APPLICATION_UID - 102; + final int coreUid = UserHandle.getUid(USER_ID, coreAppId); + + // Enable all restrictions and add this core uid to all allowlists. + mService.mDeviceIdleMode = true; + mService.mRestrictPower = true; + setRestrictBackground(true); + expectHasUseRestrictedNetworksPermission(coreUid, true); + enableRestrictedMode(true); + final NetworkPolicyManagerInternal internal = LocalServices.getService( + NetworkPolicyManagerInternal.class); + internal.setLowPowerStandbyActive(true); + internal.setLowPowerStandbyAllowlist(new int[]{coreUid}); + internal.onTempPowerSaveWhitelistChange(coreAppId, true, REASON_OTHER, "testing"); + + when(mPowerExemptionManager.getAllowListedAppIds(anyBoolean())) + .thenReturn(new int[]{coreAppId}); + mPowerAllowlistReceiver.onReceive(mServiceContext, null); + + // A normal uid would undergo a rule change from denied to allowed on all chains, but we + // should not request any rule change for this core uid. + verify(mNetworkManager, never()).setFirewallUidRule(anyInt(), eq(coreUid), anyInt()); + verify(mNetworkManager, never()).setFirewallUidRules(anyInt(), + argThat(ar -> ArrayUtils.contains(ar, coreUid)), any(int[].class)); + } + + @SuppressWarnings("GuardedBy") // For not holding mUidRulesFirstLock + @Test + @RequiresFlagsEnabled(Flags.FLAG_NEVER_APPLY_RULES_TO_CORE_UIDS) + public void testRulesNeverAppliedToUidsWithoutInternetPermission() throws Exception { + clearInvocations(mNetworkManager); + + mService.mInternetPermissionMap.clear(); + expectHasInternetPermission(UID_A, false); + + // Enable all restrictions and add this uid to all allowlists. + mService.mDeviceIdleMode = true; + mService.mRestrictPower = true; + setRestrictBackground(true); + expectHasUseRestrictedNetworksPermission(UID_A, true); + enableRestrictedMode(true); + final NetworkPolicyManagerInternal internal = LocalServices.getService( + NetworkPolicyManagerInternal.class); + internal.setLowPowerStandbyActive(true); + internal.setLowPowerStandbyAllowlist(new int[]{UID_A}); + internal.onTempPowerSaveWhitelistChange(APP_ID_A, true, REASON_OTHER, "testing"); + + when(mPowerExemptionManager.getAllowListedAppIds(anyBoolean())) + .thenReturn(new int[]{APP_ID_A}); + mPowerAllowlistReceiver.onReceive(mServiceContext, null); + + // A normal uid would undergo a rule change from denied to allowed on all chains, but we + // should not request any rule this uid without the INTERNET permission. + verify(mNetworkManager, never()).setFirewallUidRule(anyInt(), eq(UID_A), anyInt()); + verify(mNetworkManager, never()).setFirewallUidRules(anyInt(), + argThat(ar -> ArrayUtils.contains(ar, UID_A)), any(int[].class)); + } + private boolean isUidState(int uid, int procState, int procStateSeq, int capability) { final NetworkPolicyManager.UidState uidState = mService.getUidStateForTest(uid); if (uidState == null) { diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java index d714db99f18f..791215695f57 100644 --- a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java @@ -121,6 +121,9 @@ public final class UserManagerTest { // Making a copy of mUsersToRemove to avoid ConcurrentModificationException mUsersToRemove.stream().toList().forEach(this::removeUser); mUserRemovalWaiter.close(); + + mUserManager.setUserRestriction(UserManager.DISALLOW_GRANT_ADMIN, false, + mContext.getUser()); } private void removeExistingUsers() { @@ -935,6 +938,35 @@ public final class UserManagerTest { @MediumTest @Test + @RequiresFlagsEnabled(android.multiuser.Flags.FLAG_UNICORN_MODE_REFACTORING_FOR_HSUM_READ_ONLY) + public void testSetUserAdminThrowsSecurityException() throws Exception { + UserInfo targetUser = createUser("SecondaryUser", /*flags=*/ 0); + assertThat(targetUser.isAdmin()).isFalse(); + + try { + // 1. Target User Restriction + mUserManager.setUserRestriction(UserManager.DISALLOW_GRANT_ADMIN, true, + targetUser.getUserHandle()); + assertThrows(SecurityException.class, () -> mUserManager.setUserAdmin(targetUser.id)); + + // 2. Current User Restriction + mUserManager.setUserRestriction(UserManager.DISALLOW_GRANT_ADMIN, false, + targetUser.getUserHandle()); + mUserManager.setUserRestriction(UserManager.DISALLOW_GRANT_ADMIN, true, + mContext.getUser()); + assertThrows(SecurityException.class, () -> mUserManager.setUserAdmin(targetUser.id)); + + } finally { + // Ensure restriction is removed even if test fails + mUserManager.setUserRestriction(UserManager.DISALLOW_GRANT_ADMIN, false, + targetUser.getUserHandle()); + mUserManager.setUserRestriction(UserManager.DISALLOW_GRANT_ADMIN, false, + mContext.getUser()); + } + } + + @MediumTest + @Test public void testRevokeUserAdmin() throws Exception { UserInfo userInfo = createUser("Admin", /*flags=*/ UserInfo.FLAG_ADMIN); assertThat(userInfo.isAdmin()).isTrue(); @@ -959,6 +991,37 @@ public final class UserManagerTest { @MediumTest @Test + @RequiresFlagsEnabled(android.multiuser.Flags.FLAG_UNICORN_MODE_REFACTORING_FOR_HSUM_READ_ONLY) + public void testRevokeUserAdminThrowsSecurityException() throws Exception { + UserInfo targetUser = createUser("SecondaryUser", /*flags=*/ 0); + assertThat(targetUser.isAdmin()).isFalse(); + + try { + // 1. Target User Restriction + mUserManager.setUserRestriction(UserManager.DISALLOW_GRANT_ADMIN, true, + targetUser.getUserHandle()); + assertThrows(SecurityException.class, () -> mUserManager + .revokeUserAdmin(targetUser.id)); + + // 2. Current User Restriction + mUserManager.setUserRestriction(UserManager.DISALLOW_GRANT_ADMIN, false, + targetUser.getUserHandle()); + mUserManager.setUserRestriction(UserManager.DISALLOW_GRANT_ADMIN, true, + mContext.getUser()); + assertThrows(SecurityException.class, () -> mUserManager + .revokeUserAdmin(targetUser.id)); + + } finally { + // Ensure restriction is removed even if test fails + mUserManager.setUserRestriction(UserManager.DISALLOW_GRANT_ADMIN, false, + targetUser.getUserHandle()); + mUserManager.setUserRestriction(UserManager.DISALLOW_GRANT_ADMIN, false, + mContext.getUser()); + } + } + + @MediumTest + @Test public void testGetProfileParent() throws Exception { assumeManagedUsersSupported(); int mainUserId = mUserManager.getMainUser().getIdentifier(); @@ -1184,6 +1247,23 @@ public final class UserManagerTest { } } + // Make sure createUser for ADMIN would fail if we have DISALLOW_GRANT_ADMIN. + @MediumTest + @Test + @RequiresFlagsEnabled(android.multiuser.Flags.FLAG_UNICORN_MODE_REFACTORING_FOR_HSUM_READ_ONLY) + public void testCreateAdminUser_disallowGrantAdmin() throws Exception { + final int creatorId = ActivityManager.getCurrentUser(); + final UserHandle creatorHandle = asHandle(creatorId); + mUserManager.setUserRestriction(UserManager.DISALLOW_GRANT_ADMIN, true, creatorHandle); + try { + UserInfo createdInfo = createUser("SecondaryUser", /*flags=*/ UserInfo.FLAG_ADMIN); + assertThat(createdInfo).isNull(); + } finally { + mUserManager.setUserRestriction(UserManager.DISALLOW_ADD_USER, false, + creatorHandle); + } + } + // Make sure createProfile would fail if we have DISALLOW_ADD_CLONE_PROFILE. @MediumTest @Test diff --git a/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java b/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java index fad10f7a7553..551808243640 100644 --- a/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java +++ b/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java @@ -182,7 +182,7 @@ public final class DeviceStateProviderImplTest { + " <device-state>\n" + " <identifier>1</identifier>\n" + " <properties>\n" - + " <property>PROPERTY_POLICY_CANCEL_OVERRIDE_REQUESTS</property>\n" + + " <property>com.android.server.policy.PROPERTY_POLICY_CANCEL_OVERRIDE_REQUESTS</property>\n" + " </properties>\n" + " <conditions/>\n" + " </device-state>\n" @@ -338,11 +338,9 @@ public final class DeviceStateProviderImplTest { + " <identifier>4</identifier>\n" + " <name>THERMAL_TEST</name>\n" + " <properties>\n" - + " <property>PROPERTY_EMULATED_ONLY</property>\n" - + " <property>PROPERTY_POLICY_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL" - + "</property>\n" - + " <property>PROPERTY_POLICY_UNSUPPORTED_WHEN_POWER_SAVE_MODE" - + "</property>\n" + + " <property>com.android.server.policy.PROPERTY_EMULATED_ONLY</property>\n" + + " <property>com.android.server.policy.PROPERTY_POLICY_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL</property>\n" + + " <property>com.android.server.policy.PROPERTY_POLICY_UNSUPPORTED_WHEN_POWER_SAVE_MODE</property>\n" + " </properties>\n" + " </device-state>\n" + "</device-state-config>\n"; diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ConditionProvidersTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ConditionProvidersTest.java index f6e1162a5ada..af7f703e9c31 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ConditionProvidersTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ConditionProvidersTest.java @@ -16,7 +16,6 @@ package com.android.server.notification; -import static android.service.notification.Condition.SOURCE_USER_ACTION; import static android.service.notification.Condition.STATE_FALSE; import static android.service.notification.Condition.STATE_TRUE; @@ -31,13 +30,11 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; -import android.app.Flags; import android.content.ComponentName; import android.content.ServiceConnection; import android.content.pm.IPackageManager; import android.net.Uri; import android.os.IInterface; -import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import android.service.notification.Condition; @@ -150,57 +147,6 @@ public class ConditionProvidersTest extends UiServiceTestCase { } @Test - @EnableFlags(Flags.FLAG_MODES_UI) - public void notifyConditions_appCannotUndoUserEnablement() { - ManagedServices.ManagedServiceInfo msi = mProviders.new ManagedServiceInfo( - mock(IInterface.class), new ComponentName("package", "cls"), 0, false, - mock(ServiceConnection.class), 33, 100); - // First, user enabled mode - Condition[] userConditions = new Condition[] { - new Condition(Uri.parse("a"), "summary", STATE_TRUE, SOURCE_USER_ACTION) - }; - mProviders.notifyConditions("package", msi, userConditions); - verify(mCallback).onConditionChanged(eq(Uri.parse("a")), eq(userConditions[0])); - - // Second, app tries to disable it, but cannot - Condition[] appConditions = new Condition[] { - new Condition(Uri.parse("a"), "summary", STATE_FALSE) - }; - mProviders.notifyConditions("package", msi, appConditions); - verify(mCallback).onConditionChanged(eq(Uri.parse("a")), eq(userConditions[0])); - } - - @Test - @EnableFlags(Flags.FLAG_MODES_UI) - public void notifyConditions_appCanTakeoverUserEnablement() { - ManagedServices.ManagedServiceInfo msi = mProviders.new ManagedServiceInfo( - mock(IInterface.class), new ComponentName("package", "cls"), 0, false, - mock(ServiceConnection.class), 33, 100); - // First, user enabled mode - Condition[] userConditions = new Condition[] { - new Condition(Uri.parse("a"), "summary", STATE_TRUE, SOURCE_USER_ACTION) - }; - mProviders.notifyConditions("package", msi, userConditions); - verify(mCallback).onConditionChanged(eq(Uri.parse("a")), eq(userConditions[0])); - - // Second, app now thinks the rule should be on due it its intelligence - Condition[] appConditions = new Condition[] { - new Condition(Uri.parse("a"), "summary", STATE_TRUE) - }; - mProviders.notifyConditions("package", msi, appConditions); - verify(mCallback).onConditionChanged(eq(Uri.parse("a")), eq(appConditions[0])); - - // Lastly, app can turn rule off when its intelligence think it should be off - appConditions = new Condition[] { - new Condition(Uri.parse("a"), "summary", STATE_FALSE) - }; - mProviders.notifyConditions("package", msi, appConditions); - verify(mCallback).onConditionChanged(eq(Uri.parse("a")), eq(appConditions[0])); - - verifyNoMoreInteractions(mCallback); - } - - @Test public void testRemoveDefaultFromConfig() { final int userId = 0; ComponentName oldDefaultComponent = ComponentName.unflattenFromString("package/Component1"); 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..643ee4aadd80 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 // @@ -2426,6 +2458,74 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { } @Test + public void testBeepVolume_politeNotif_AvalancheStrategy_mixedNotif() throws Exception { + mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS); + mSetFlagsRule.enableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS); + mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS_ATTN_UPDATE); + TestableFlagResolver flagResolver = new TestableFlagResolver(); + flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME1, 50); + flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME2, 0); + initAttentionHelper(flagResolver); + + // Trigger avalanche trigger intent + final Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED); + intent.putExtra("state", false); + mAvalancheBroadcastReceiver.onReceive(getContext(), intent); + + // Regular notification: should beep at 0% volume + NotificationRecord r = getBeepyNotification(); + mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS); + verifyBeepVolume(0.0f); + assertEquals(-1, r.getLastAudiblyAlertedMs()); + Mockito.reset(mRingtonePlayer); + + // Conversation notification + mChannel = new NotificationChannel("test2", "test2", IMPORTANCE_DEFAULT); + NotificationRecord r2 = getConversationNotificationRecord(mId, false /* insistent */, + false /* once */, true /* noisy */, false /* buzzy*/, false /* lights */, true, + true, false, null, Notification.GROUP_ALERT_ALL, false, mUser, mPkg, + "shortcut"); + + // Should beep at 100% volume + mAttentionHelper.buzzBeepBlinkLocked(r2, DEFAULT_SIGNALS); + assertNotEquals(-1, r2.getLastAudiblyAlertedMs()); + verifyBeepVolume(1.0f); + + // Conversation notification on a different channel + mChannel = new NotificationChannel("test3", "test3", IMPORTANCE_DEFAULT); + NotificationRecord r3 = getConversationNotificationRecord(mId, false /* insistent */, + false /* once */, true /* noisy */, false /* buzzy*/, false /* lights */, true, + true, false, null, Notification.GROUP_ALERT_ALL, false, mUser, mPkg, + "shortcut"); + + // Should beep at 50% volume + Mockito.reset(mRingtonePlayer); + mAttentionHelper.buzzBeepBlinkLocked(r3, DEFAULT_SIGNALS); + assertNotEquals(-1, r3.getLastAudiblyAlertedMs()); + verifyBeepVolume(0.5f); + + // 2nd update should beep at 0% volume + Mockito.reset(mRingtonePlayer); + mAttentionHelper.buzzBeepBlinkLocked(r3, DEFAULT_SIGNALS); + verifyBeepVolume(0.0f); + + // Set important conversation + mChannel.setImportantConversation(true); + r3 = getConversationNotificationRecord(mId, false /* insistent */, + false /* once */, true /* noisy */, false /* buzzy*/, false /* lights */, true, + true, false, null, Notification.GROUP_ALERT_ALL, false, mUser, mPkg, + "shortcut"); + + // important conversation should beep at 100% volume + Mockito.reset(mRingtonePlayer); + mAttentionHelper.buzzBeepBlinkLocked(r3, DEFAULT_SIGNALS); + verifyBeepVolume(1.0f); + + verify(mAccessibilityService, times(5)).sendAccessibilityEvent(any(), anyInt()); + assertNotEquals(-1, r3.getLastAudiblyAlertedMs()); + } + + @Test public void testBeepVolume_politeNotif_Avalanche_exemptEmergency() throws Exception { mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS); mSetFlagsRule.enableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS); @@ -2603,6 +2703,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/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java index b955a795e94a..e70ed5f256bf 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java @@ -31,6 +31,10 @@ import static android.service.notification.Condition.SOURCE_USER_ACTION; import static android.service.notification.Condition.STATE_FALSE; import static android.service.notification.Condition.STATE_TRUE; import static android.service.notification.NotificationListenerService.SUPPRESSED_EFFECT_SCREEN_ON; +import static android.service.notification.ZenModeConfig.XML_VERSION_MODES_API; +import static android.service.notification.ZenModeConfig.ZEN_TAG; +import static android.service.notification.ZenModeConfig.ZenRule.OVERRIDE_DEACTIVATE; +import static android.service.notification.ZenModeConfig.ZenRule.OVERRIDE_NONE; import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_IMPORTANT; import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_NONE; import static android.service.notification.ZenPolicy.PEOPLE_TYPE_ANYONE; @@ -283,8 +287,8 @@ public class ZenModeConfigTest extends UiServiceTestCase { // the default value from the zen mode config. Policy policy = config.toNotificationPolicy(zenPolicy); assertEquals(Flags.modesUi() - ? config.manualRule.zenPolicy.getPriorityChannelsAllowed() == STATE_ALLOW - : config.isAllowPriorityChannels(), + ? config.manualRule.zenPolicy.getPriorityChannelsAllowed() == STATE_ALLOW + : config.isAllowPriorityChannels(), policy.allowPriorityChannels()); } @@ -522,7 +526,7 @@ public class ZenModeConfigTest extends UiServiceTestCase { rule.zenMode = INTERRUPTION_FILTER; rule.modified = true; rule.name = NAME; - rule.snoozing = true; + rule.setConditionOverride(OVERRIDE_DEACTIVATE); rule.pkg = OWNER.getPackageName(); rule.zenPolicy = POLICY; @@ -544,7 +548,7 @@ public class ZenModeConfigTest extends UiServiceTestCase { ZenModeConfig.ZenRule parceled = new ZenModeConfig.ZenRule(parcel); assertEquals(rule.pkg, parceled.pkg); - assertEquals(rule.snoozing, parceled.snoozing); + assertEquals(rule.getConditionOverride(), parceled.getConditionOverride()); assertEquals(rule.enabler, parceled.enabler); assertEquals(rule.component, parceled.component); assertEquals(rule.configurationActivity, parceled.configurationActivity); @@ -623,7 +627,7 @@ public class ZenModeConfigTest extends UiServiceTestCase { rule.zenMode = INTERRUPTION_FILTER; rule.modified = true; rule.name = NAME; - rule.snoozing = true; + rule.setConditionOverride(OVERRIDE_DEACTIVATE); rule.pkg = OWNER.getPackageName(); rule.zenPolicy = POLICY; rule.zenDeviceEffects = new ZenDeviceEffects.Builder() @@ -660,7 +664,7 @@ public class ZenModeConfigTest extends UiServiceTestCase { assertEquals(rule.pkg, fromXml.pkg); // always resets on reboot - assertFalse(fromXml.snoozing); + assertEquals(OVERRIDE_NONE, fromXml.getConditionOverride()); //should all match original assertEquals(rule.component, fromXml.component); assertEquals(rule.configurationActivity, fromXml.configurationActivity); @@ -991,6 +995,58 @@ public class ZenModeConfigTest extends UiServiceTestCase { } @Test + @EnableFlags(Flags.FLAG_MODES_UI) + public void testConfigXml_manualRule_upgradeWhenExisting() throws Exception { + // prior to modes_ui, it's possible to have a non-null manual rule that doesn't have much + // data on it because it's meant to indicate that the manual rule is on by merely existing. + ZenModeConfig config = new ZenModeConfig(); + config.manualRule = new ZenModeConfig.ZenRule(); + config.manualRule.enabled = true; + config.manualRule.pkg = "android"; + config.manualRule.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS; + config.manualRule.conditionId = ZenModeConfig.toTimeCondition(mContext, 200, mUserId).id; + config.manualRule.enabler = "test"; + + // write out entire config xml + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + writeConfigXml(config, XML_VERSION_MODES_API, /* forBackup= */ false, baos); + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + ZenModeConfig fromXml = readConfigXml(bais); + + // The result should have a manual rule; it should have a non-null ZenPolicy and a condition + // whose state is true. The conditionId and enabler data should also be preserved. + assertThat(fromXml.manualRule).isNotNull(); + assertThat(fromXml.manualRule.zenPolicy).isNotNull(); + assertThat(fromXml.manualRule.condition).isNotNull(); + assertThat(fromXml.manualRule.condition.state).isEqualTo(STATE_TRUE); + assertThat(fromXml.manualRule.conditionId).isEqualTo(config.manualRule.conditionId); + assertThat(fromXml.manualRule.enabler).isEqualTo("test"); + assertThat(fromXml.isManualActive()).isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_MODES_UI) + public void testConfigXml_manualRule_doesNotTurnOnIfNotUpgrade() throws Exception { + // confirm that if the manual rule is already properly set up for modes_ui, it does not get + // turned on (set to condition with STATE_TRUE) when reading xml. + + // getMutedAllConfig sets up the manual rule with a policy muting everything + ZenModeConfig config = getMutedAllConfig(); + config.manualRule.condition = new Condition(Uri.EMPTY, "", STATE_FALSE, SOURCE_USER_ACTION); + assertThat(config.isManualActive()).isFalse(); + + // write out entire config xml + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + writeConfigXml(config, XML_VERSION_MODES_API, /* forBackup= */ false, baos); + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + ZenModeConfig fromXml = readConfigXml(bais); + + // The result should have a manual rule; it should not be changed from the previous rule. + assertThat(fromXml.manualRule).isEqualTo(config.manualRule); + assertThat(fromXml.isManualActive()).isFalse(); + } + + @Test public void testGetDescription_off() { ZenModeConfig config = new ZenModeConfig(); if (!modesUi()) { @@ -1061,7 +1117,6 @@ public class ZenModeConfigTest extends UiServiceTestCase { rule.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS; rule.modified = true; rule.name = "name"; - rule.snoozing = false; rule.pkg = "b"; config.automaticRules.put("key", rule); @@ -1238,4 +1293,25 @@ public class ZenModeConfigTest extends UiServiceTestCase { parser.nextTag(); return ZenModeConfig.readZenPolicyXml(parser); } + + private void writeConfigXml(ZenModeConfig config, Integer version, boolean forBackup, + ByteArrayOutputStream os) throws IOException { + String tag = ZEN_TAG; + + TypedXmlSerializer out = Xml.newFastSerializer(); + out.setOutput(new BufferedOutputStream(os), "utf-8"); + out.startDocument(null, true); + out.startTag(null, tag); + config.writeXml(out, version, forBackup); + out.endTag(null, tag); + out.endDocument(); + } + + private ZenModeConfig readConfigXml(ByteArrayInputStream is) + throws XmlPullParserException, IOException { + TypedXmlPullParser parser = Xml.newFastPullParser(); + parser.setInput(new BufferedInputStream(is), null); + parser.nextTag(); + return ZenModeConfig.readXml(parser); + } } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java index 9af00218dda4..91eb2edeee13 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java @@ -158,7 +158,10 @@ public class ZenModeDiffTest extends UiServiceTestCase { RuleDiff.FIELD_ZEN_DEVICE_EFFECTS, RuleDiff.FIELD_LEGACY_SUPPRESSED_EFFECTS)); } - if (!(Flags.modesApi() && Flags.modesUi())) { + if (Flags.modesApi() && Flags.modesUi()) { + exemptFields.add(RuleDiff.FIELD_SNOOZING); // Obsolete. + } else { + exemptFields.add(RuleDiff.FIELD_CONDITION_OVERRIDE); exemptFields.add(RuleDiff.FIELD_LEGACY_SUPPRESSED_EFFECTS); } return exemptFields; @@ -339,7 +342,7 @@ public class ZenModeDiffTest extends UiServiceTestCase { rule.zenMode = Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; rule.modified = false; rule.name = "name"; - rule.snoozing = true; + rule.setConditionOverride(ZenModeConfig.ZenRule.OVERRIDE_DEACTIVATE); rule.pkg = "a"; if (android.app.Flags.modesApi()) { rule.allowManualInvocation = true; diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java index 63cf1068f51e..212e61e10448 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java @@ -51,6 +51,7 @@ import static android.provider.Settings.Global.ZEN_MODE_ALARMS; import static android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; import static android.provider.Settings.Global.ZEN_MODE_NO_INTERRUPTIONS; import static android.provider.Settings.Global.ZEN_MODE_OFF; +import static android.service.notification.Condition.SOURCE_CONTEXT; import static android.service.notification.Condition.SOURCE_SCHEDULE; import static android.service.notification.Condition.SOURCE_USER_ACTION; import static android.service.notification.Condition.STATE_FALSE; @@ -61,6 +62,9 @@ import static android.service.notification.ZenModeConfig.ORIGIN_INIT_USER; import static android.service.notification.ZenModeConfig.ORIGIN_UNKNOWN; import static android.service.notification.ZenModeConfig.ORIGIN_USER_IN_APP; import static android.service.notification.ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI; +import static android.service.notification.ZenModeConfig.ZenRule.OVERRIDE_ACTIVATE; +import static android.service.notification.ZenModeConfig.ZenRule.OVERRIDE_DEACTIVATE; +import static android.service.notification.ZenModeConfig.ZenRule.OVERRIDE_NONE; import static android.service.notification.ZenPolicy.PEOPLE_TYPE_CONTACTS; import static android.service.notification.ZenPolicy.PEOPLE_TYPE_NONE; import static android.service.notification.ZenPolicy.PEOPLE_TYPE_STARRED; @@ -1495,6 +1499,30 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test + public void testReadXmlRestore_doesNotEnableManualRule() throws Exception { + setupZenConfig(); + + // Turn on manual zen mode + mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, + ORIGIN_USER_IN_SYSTEMUI, "", "someCaller", SYSTEM_UID); + ZenModeConfig original = mZenModeHelper.mConfig.copy(); + assertThat(original.isManualActive()).isTrue(); + + ByteArrayOutputStream baos = writeXmlAndPurge(null); + TypedXmlPullParser parser = getParserForByteStream(baos); + mZenModeHelper.readXml(parser, true, UserHandle.USER_ALL); + + ZenModeConfig result = mZenModeHelper.getConfig(); + assertThat(result.isManualActive()).isFalse(); + + // confirm that we do still keep policy information, modes_ui only; prior to modes_ui the + // entire rule is intentionally cleared + if (Flags.modesUi()) { + assertThat(result.manualRule.zenPolicy).isNotNull(); + } + } + + @Test public void testWriteXmlWithZenPolicy() throws Exception { final String ruleId = "customRule"; setupZenConfig(); @@ -2975,11 +3003,13 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertWithMessage("Failure for origin " + origin.name()) .that(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF); assertWithMessage("Failure for origin " + origin.name()) - .that(mZenModeHelper.mConfig.automaticRules.get(activeRuleId).snoozing) - .isTrue(); + .that(mZenModeHelper.mConfig.automaticRules + .get(activeRuleId).getConditionOverride()) + .isEqualTo(OVERRIDE_DEACTIVATE); assertWithMessage("Failure for origin " + origin.name()) - .that(mZenModeHelper.mConfig.automaticRules.get(inactiveRuleId).snoozing) - .isFalse(); + .that(mZenModeHelper.mConfig.automaticRules + .get(inactiveRuleId).getConditionOverride()) + .isEqualTo(OVERRIDE_NONE); } } @@ -3014,16 +3044,20 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertWithMessage("Failure for origin " + origin.name()).that( mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_IMPORTANT_INTERRUPTIONS); assertWithMessage("Failure for origin " + origin.name()).that( - config.automaticRules.get(activeRuleId).snoozing).isFalse(); + config.automaticRules.get(activeRuleId).getConditionOverride()) + .isEqualTo(OVERRIDE_NONE); assertWithMessage("Failure for origin " + origin.name()).that( - config.automaticRules.get(inactiveRuleId).snoozing).isFalse(); + config.automaticRules.get(inactiveRuleId).getConditionOverride()) + .isEqualTo(OVERRIDE_NONE); } else { assertWithMessage("Failure for origin " + origin.name()).that( mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF); assertWithMessage("Failure for origin " + origin.name()).that( - config.automaticRules.get(activeRuleId).snoozing).isTrue(); + config.automaticRules.get(activeRuleId).getConditionOverride()) + .isEqualTo(OVERRIDE_DEACTIVATE); assertWithMessage("Failure for origin " + origin.name()).that( - config.automaticRules.get(inactiveRuleId).snoozing).isFalse(); + config.automaticRules.get(inactiveRuleId).getConditionOverride()) + .isEqualTo(OVERRIDE_NONE); } } } @@ -4264,7 +4298,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { rule.zenMode = INTERRUPTION_FILTER_ZR; rule.modified = true; rule.name = NAME; - rule.snoozing = true; + rule.setConditionOverride(OVERRIDE_DEACTIVATE); rule.pkg = OWNER.getPackageName(); rule.zenPolicy = POLICY; @@ -4976,7 +5010,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.updateAutomaticZenRule(createdId, zenRule, ZenModeConfig.ORIGIN_SYSTEM, "", SYSTEM_UID); - assertEquals(false, mZenModeHelper.mConfig.automaticRules.get(createdId).snoozing); + assertEquals(OVERRIDE_NONE, + mZenModeHelper.mConfig.automaticRules.get(createdId).getConditionOverride()); } @Test @@ -5689,7 +5724,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertThat(storedRule.isAutomaticActive()).isFalse(); assertThat(storedRule.isTrueOrUnknown()).isFalse(); assertThat(storedRule.condition).isNull(); - assertThat(storedRule.snoozing).isFalse(); + assertThat(storedRule.getConditionOverride()).isEqualTo(OVERRIDE_NONE); assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF); } @@ -6015,15 +6050,18 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, ZEN_MODE_IMPORTANT_INTERRUPTIONS); assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(1); - assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).snoozing).isFalse(); + assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).getConditionOverride()) + .isEqualTo(OVERRIDE_NONE); mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null, ORIGIN_APP, "test", "test", 0); - assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).snoozing).isTrue(); + assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).getConditionOverride()) + .isEqualTo(OVERRIDE_DEACTIVATE); mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, ZEN_MODE_ALARMS); - assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).snoozing).isFalse(); + assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).getConditionOverride()) + .isEqualTo(OVERRIDE_NONE); assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).condition.state) .isEqualTo(STATE_TRUE); } @@ -6427,6 +6465,160 @@ public class ZenModeHelperTest extends UiServiceTestCase { ORIGIN_UNKNOWN); } + @Test + @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + public void setAutomaticZenRuleState_manualActivation_appliesOverride() { + AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond")) + .setPackage(mPkg) + .build(); + String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, rule, ORIGIN_APP, "adding", + CUSTOM_PKG_UID); + + mZenModeHelper.setAutomaticZenRuleState(ruleId, + new Condition(rule.getConditionId(), "manual-on", STATE_TRUE, SOURCE_USER_ACTION), + ORIGIN_USER_IN_SYSTEMUI, SYSTEM_UID); + + ZenRule zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); + assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_ACTIVATE); + assertThat(zenRule.condition).isNull(); + } + + @Test + @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + public void setAutomaticZenRuleState_manualActivationAndThenDeactivation_removesOverride() { + AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond")) + .setPackage(mPkg) + .build(); + String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, rule, ORIGIN_APP, "adding", + CUSTOM_PKG_UID); + ZenRule zenRule; + + mZenModeHelper.setAutomaticZenRuleState(ruleId, + new Condition(rule.getConditionId(), "manual-on", STATE_TRUE, SOURCE_USER_ACTION), + ORIGIN_USER_IN_SYSTEMUI, SYSTEM_UID); + zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); + assertThat(zenRule.isAutomaticActive()).isTrue(); + assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_ACTIVATE); + assertThat(zenRule.condition).isNull(); + + mZenModeHelper.setAutomaticZenRuleState(ruleId, + new Condition(rule.getConditionId(), "manual-off", STATE_FALSE, SOURCE_USER_ACTION), + ORIGIN_USER_IN_SYSTEMUI, SYSTEM_UID); + zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); + assertThat(zenRule.isAutomaticActive()).isFalse(); + assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_NONE); + assertThat(zenRule.condition).isNull(); + } + + @Test + @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + public void setAutomaticZenRuleState_manualDeactivation_appliesOverride() { + AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond")) + .setPackage(mPkg) + .build(); + String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, rule, ORIGIN_APP, "adding", + CUSTOM_PKG_UID); + + mZenModeHelper.setAutomaticZenRuleState(ruleId, + new Condition(rule.getConditionId(), "auto-on", STATE_TRUE, SOURCE_CONTEXT), + ORIGIN_APP, SYSTEM_UID); + ZenRule zenRuleOn = mZenModeHelper.mConfig.automaticRules.get(ruleId); + assertThat(zenRuleOn.isAutomaticActive()).isTrue(); + assertThat(zenRuleOn.getConditionOverride()).isEqualTo(OVERRIDE_NONE); + assertThat(zenRuleOn.condition).isNotNull(); + + mZenModeHelper.setAutomaticZenRuleState(ruleId, + new Condition(rule.getConditionId(), "manual-off", STATE_FALSE, SOURCE_USER_ACTION), + ORIGIN_USER_IN_SYSTEMUI, SYSTEM_UID); + ZenRule zenRuleOff = mZenModeHelper.mConfig.automaticRules.get(ruleId); + assertThat(zenRuleOff.isAutomaticActive()).isFalse(); + assertThat(zenRuleOff.getConditionOverride()).isEqualTo(OVERRIDE_DEACTIVATE); + assertThat(zenRuleOff.condition).isNotNull(); + } + + @Test + @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + public void setAutomaticZenRuleState_ifManualActive_appCannotDeactivateBeforeActivating() { + AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond")) + .setPackage(mPkg) + .build(); + String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, rule, ORIGIN_APP, "adding", + CUSTOM_PKG_UID); + ZenRule zenRule; + + mZenModeHelper.setAutomaticZenRuleState(ruleId, + new Condition(rule.getConditionId(), "manual-on", STATE_TRUE, SOURCE_USER_ACTION), + ORIGIN_USER_IN_SYSTEMUI, SYSTEM_UID); + zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); + assertThat(zenRule.isAutomaticActive()).isTrue(); + assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_ACTIVATE); + + mZenModeHelper.setAutomaticZenRuleState(ruleId, + new Condition(rule.getConditionId(), "auto-off", STATE_FALSE, SOURCE_CONTEXT), + ORIGIN_APP, CUSTOM_PKG_UID); + zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); + assertThat(zenRule.isAutomaticActive()).isTrue(); + assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_ACTIVATE); + + mZenModeHelper.setAutomaticZenRuleState(ruleId, + new Condition(rule.getConditionId(), "auto-on", STATE_TRUE, SOURCE_CONTEXT), + ORIGIN_APP, CUSTOM_PKG_UID); + zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); + assertThat(zenRule.isAutomaticActive()).isTrue(); + assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_NONE); + mZenModeHelper.setAutomaticZenRuleState(ruleId, + new Condition(rule.getConditionId(), "auto-off", STATE_FALSE, SOURCE_CONTEXT), + ORIGIN_APP, CUSTOM_PKG_UID); + zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); + assertThat(zenRule.isAutomaticActive()).isFalse(); + } + + @Test + @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + public void setAutomaticZenRuleState_ifManualInactive_appCannotReactivateBeforeDeactivating() { + AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond")) + .setPackage(mPkg) + .build(); + String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, rule, ORIGIN_APP, "adding", + CUSTOM_PKG_UID); + ZenRule zenRule; + + mZenModeHelper.setAutomaticZenRuleState(ruleId, + new Condition(rule.getConditionId(), "auto-on", STATE_TRUE, SOURCE_CONTEXT), + ORIGIN_APP, CUSTOM_PKG_UID); + zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); + assertThat(zenRule.isAutomaticActive()).isTrue(); + assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_NONE); + + mZenModeHelper.setAutomaticZenRuleState(ruleId, + new Condition(rule.getConditionId(), "manual-off", STATE_FALSE, SOURCE_USER_ACTION), + ORIGIN_USER_IN_SYSTEMUI, SYSTEM_UID); + zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); + assertThat(zenRule.isAutomaticActive()).isFalse(); + assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_DEACTIVATE); + + mZenModeHelper.setAutomaticZenRuleState(ruleId, + new Condition(rule.getConditionId(), "auto-on", STATE_TRUE, SOURCE_CONTEXT), + ORIGIN_APP, CUSTOM_PKG_UID); + zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); + assertThat(zenRule.isAutomaticActive()).isFalse(); + assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_DEACTIVATE); + + mZenModeHelper.setAutomaticZenRuleState(ruleId, + new Condition(rule.getConditionId(), "auto-off", STATE_FALSE, SOURCE_CONTEXT), + ORIGIN_APP, CUSTOM_PKG_UID); + zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); + assertThat(zenRule.isAutomaticActive()).isFalse(); + assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_NONE); + + mZenModeHelper.setAutomaticZenRuleState(ruleId, + new Condition(rule.getConditionId(), "auto-on", STATE_TRUE, SOURCE_CONTEXT), + ORIGIN_APP, CUSTOM_PKG_UID); + zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); + assertThat(zenRule.isAutomaticActive()).isTrue(); + assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_NONE); + } + private static void addZenRule(ZenModeConfig config, String id, String ownerPkg, int zenMode, @Nullable ZenPolicy zenPolicy) { ZenRule rule = new ZenRule(); diff --git a/services/tests/vibrator/Android.bp b/services/tests/vibrator/Android.bp index 757bcd8e2193..43ad44f057cc 100644 --- a/services/tests/vibrator/Android.bp +++ b/services/tests/vibrator/Android.bp @@ -32,6 +32,7 @@ android_test { "frameworks-base-testutils", "frameworks-services-vibrator-testutils", "junit", + "junit-params", "mockito-target-inline-minus-junit4", "platform-test-annotations", "service-permission.stubs.system_server", diff --git a/services/tests/vibrator/src/com/android/server/vibrator/DeviceAdapterTest.java b/services/tests/vibrator/src/com/android/server/vibrator/DeviceAdapterTest.java index 59d557777f3b..1493253a50d4 100644 --- a/services/tests/vibrator/src/com/android/server/vibrator/DeviceAdapterTest.java +++ b/services/tests/vibrator/src/com/android/server/vibrator/DeviceAdapterTest.java @@ -21,6 +21,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.when; import android.content.ComponentName; +import android.content.Context; import android.content.pm.PackageManagerInternal; import android.hardware.vibrator.IVibrator; import android.os.CombinedVibration; @@ -32,11 +33,12 @@ import android.os.vibrator.PrebakedSegment; import android.os.vibrator.PrimitiveSegment; import android.os.vibrator.RampSegment; import android.os.vibrator.StepSegment; +import android.os.vibrator.VibrationConfig; import android.os.vibrator.VibrationEffectSegment; import android.platform.test.annotations.RequiresFlagsEnabled; import android.util.SparseArray; -import androidx.test.InstrumentationRegistry; +import androidx.test.core.app.ApplicationProvider; import com.android.server.LocalServices; @@ -76,9 +78,10 @@ public class DeviceAdapterTest { LocalServices.removeServiceForTest(PackageManagerInternal.class); LocalServices.addService(PackageManagerInternal.class, mPackageManagerInternalMock); + Context context = ApplicationProvider.getApplicationContext(); mTestLooper = new TestLooper(); - mVibrationSettings = new VibrationSettings( - InstrumentationRegistry.getContext(), new Handler(mTestLooper.getLooper())); + mVibrationSettings = new VibrationSettings(context, new Handler(mTestLooper.getLooper()), + new VibrationConfig(context.getResources())); SparseArray<VibratorController> vibrators = new SparseArray<>(); vibrators.put(EMPTY_VIBRATOR_ID, createEmptyVibratorController(EMPTY_VIBRATOR_ID)); diff --git a/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackCustomizationTest.java b/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackCustomizationTest.java index 2b23b1897f59..e0d05df1de80 100644 --- a/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackCustomizationTest.java +++ b/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackCustomizationTest.java @@ -16,16 +16,17 @@ package com.android.server.vibrator; - import static android.os.VibrationEffect.Composition.PRIMITIVE_TICK; import static android.os.VibrationEffect.EFFECT_CLICK; +import static com.android.internal.R.xml.haptic_feedback_customization; import static com.android.server.vibrator.HapticFeedbackCustomization.CustomizationParserException; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.when; import android.content.res.Resources; @@ -39,10 +40,15 @@ import android.util.SparseArray; import androidx.test.InstrumentationRegistry; import com.android.internal.R; +import com.android.internal.annotations.Keep; + +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; @@ -50,6 +56,7 @@ import org.mockito.junit.MockitoRule; import java.io.File; import java.io.FileOutputStream; +@RunWith(JUnitParamsRunner.class) public class HapticFeedbackCustomizationTest { @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @@ -78,21 +85,35 @@ public class HapticFeedbackCustomizationTest { @Mock private Resources mResourcesMock; @Mock private VibratorInfo mVibratorInfoMock; + @Keep + private static Object[][] hapticFeedbackCustomizationTestArguments() { + // (boolean hasConfigFile, boolean hasRes). + return new Object[][] {{true, true}, {true, false}, {false, true}}; + } + @Before public void setUp() { when(mVibratorInfoMock.areVibrationFeaturesSupported(any())).thenReturn(true); mSetFlagsRule.enableFlags(Flags.FLAG_HAPTIC_FEEDBACK_VIBRATION_OEM_CUSTOMIZATION_ENABLED); + mSetFlagsRule.disableFlags( + Flags.FLAG_LOAD_HAPTIC_FEEDBACK_VIBRATION_CUSTOMIZATION_FROM_RESOURCES); } @Test - public void testParseCustomizations_noCustomization_success() throws Exception { - assertParseCustomizationsSucceeds( - /* xml= */ "<haptic-feedback-constants></haptic-feedback-constants>", - /* expectedCustomizations= */ new SparseArray<>()); + @Parameters(method = "hapticFeedbackCustomizationTestArguments") + public void testParseCustomizations_noCustomization_success( + boolean hasConfigFile, boolean hasRes) throws Exception { + String xml = "<haptic-feedback-constants></haptic-feedback-constants>"; + SparseArray<VibrationEffect> expectedMapping = new SparseArray<>(); + setupParseCustomizations(xml, hasConfigFile, hasRes); + + assertParseCustomizationsSucceeds(xml, expectedMapping, hasConfigFile, hasRes); } @Test - public void testParseCustomizations_featureFlagDisabled_returnsNull() throws Exception { + @Parameters(method = "hapticFeedbackCustomizationTestArguments") + public void testParseCustomizations_featureFlagDisabled_returnsNull( + boolean hasConfigFile, boolean hasRes) throws Exception { mSetFlagsRule.disableFlags(Flags.FLAG_HAPTIC_FEEDBACK_VIBRATION_OEM_CUSTOMIZATION_ENABLED); // Valid customization XML. String xml = "<haptic-feedback-constants>" @@ -100,14 +121,16 @@ public class HapticFeedbackCustomizationTest { + COMPOSITION_VIBRATION_XML + "</constant>" + "</haptic-feedback-constants>"; - setupCustomizationFile(xml); + setupParseCustomizations(xml, hasConfigFile, hasRes); assertThat(HapticFeedbackCustomization.loadVibrations(mResourcesMock, mVibratorInfoMock)) .isNull(); } @Test - public void testParseCustomizations_oneVibrationCustomization_success() throws Exception { + @Parameters(method = "hapticFeedbackCustomizationTestArguments") + public void testParseCustomizations_oneVibrationCustomization_success( + boolean hasConfigFile, boolean hasRes) throws Exception { String xml = "<haptic-feedback-constants>" + "<constant id=\"10\">" + COMPOSITION_VIBRATION_XML @@ -116,11 +139,13 @@ public class HapticFeedbackCustomizationTest { SparseArray<VibrationEffect> expectedMapping = new SparseArray<>(); expectedMapping.put(10, COMPOSITION_VIBRATION); - assertParseCustomizationsSucceeds(xml, expectedMapping); + assertParseCustomizationsSucceeds(xml, expectedMapping, hasConfigFile, hasRes); } @Test - public void testParseCustomizations_oneVibrationSelectCustomization_success() throws Exception { + @Parameters(method = "hapticFeedbackCustomizationTestArguments") + public void testParseCustomizations_oneVibrationSelectCustomization_success( + boolean hasConfigFile, boolean hasRes) throws Exception { String xml = "<haptic-feedback-constants>" + "<constant id=\"10\">" + "<vibration-select>" @@ -131,11 +156,13 @@ public class HapticFeedbackCustomizationTest { SparseArray<VibrationEffect> expectedMapping = new SparseArray<>(); expectedMapping.put(10, COMPOSITION_VIBRATION); - assertParseCustomizationsSucceeds(xml, expectedMapping); + assertParseCustomizationsSucceeds(xml, expectedMapping, hasConfigFile, hasRes); } @Test - public void testParseCustomizations_multipleCustomizations_success() throws Exception { + @Parameters(method = "hapticFeedbackCustomizationTestArguments") + public void testParseCustomizations_multipleCustomizations_success( + boolean hasConfigFile, boolean hasRes) throws Exception { String xml = "<haptic-feedback-constants>" + "<constant id=\"1\">" + COMPOSITION_VIBRATION_XML @@ -162,11 +189,13 @@ public class HapticFeedbackCustomizationTest { expectedMapping.put(150, PREDEFINED_VIBRATION); expectedMapping.put(10, WAVEFORM_VIBARTION); - assertParseCustomizationsSucceeds(xml, expectedMapping); + assertParseCustomizationsSucceeds(xml, expectedMapping, hasConfigFile, hasRes); } @Test - public void testParseCustomizations_multipleCustomizations_noSupportedVibration_success() + @Parameters(method = "hapticFeedbackCustomizationTestArguments") + public void testParseCustomizations_multipleCustomizations_noSupportedVibration_success( + boolean hasConfigFile, boolean hasRes) throws Exception { makeUnsupported(COMPOSITION_VIBRATION, PREDEFINED_VIBRATION, WAVEFORM_VIBARTION); String xml = "<haptic-feedback-constants>" @@ -189,13 +218,16 @@ public class HapticFeedbackCustomizationTest { + "</vibration-select>" + "</constant>" + "</haptic-feedback-constants>"; + SparseArray<VibrationEffect> expectedMapping = new SparseArray<>(); - assertParseCustomizationsSucceeds(xml, new SparseArray<>()); + assertParseCustomizationsSucceeds(xml, expectedMapping, hasConfigFile, hasRes); } @Test - public void testParseCustomizations_multipleCustomizations_someUnsupportedVibration_success() - throws Exception { + @Parameters(method = "hapticFeedbackCustomizationTestArguments") + public void testParseCustomizations_multipleCustomizations_someUnsupportedVibration_success( + boolean hasConfigFile, boolean hasRes) + throws Exception { makeSupported(PREDEFINED_VIBRATION, WAVEFORM_VIBARTION); makeUnsupported(COMPOSITION_VIBRATION); String xml = "<haptic-feedback-constants>" @@ -230,7 +262,7 @@ public class HapticFeedbackCustomizationTest { expectedMapping.put(150, PREDEFINED_VIBRATION); expectedMapping.put(10, PREDEFINED_VIBRATION); - assertParseCustomizationsSucceeds(xml, expectedMapping); + assertParseCustomizationsSucceeds(xml, expectedMapping, hasConfigFile, hasRes); } @Test @@ -252,12 +284,23 @@ public class HapticFeedbackCustomizationTest { } @Test - public void testParseCustomizations_disallowedVibrationForHapticFeedback_throwsException() - throws Exception { + public void testParseCustomizations_noCustomizationResource_returnsNull() throws Exception { + mSetFlagsRule.enableFlags( + Flags.FLAG_LOAD_HAPTIC_FEEDBACK_VIBRATION_CUSTOMIZATION_FROM_RESOURCES); + doThrow(new Resources.NotFoundException()) + .when(mResourcesMock).getXml(haptic_feedback_customization); + + assertThat(HapticFeedbackCustomization.loadVibrations(mResourcesMock, mVibratorInfoMock)) + .isNull(); + } + + @Test + @Parameters(method = "hapticFeedbackCustomizationTestArguments") + public void testParseCustomizations_disallowedVibrationForHapticFeedback_throwsException( + boolean hasConfigFile, boolean hasRes) throws Exception { // The XML content is good, but the serialized vibration is not supported for haptic // feedback usage (i.e. repeating vibration). - assertParseCustomizationsFails( - "<haptic-feedback-constants>" + String xml = "<haptic-feedback-constants>" + "<constant id=\"10\">" + "<vibration-effect>" + "<waveform-effect>" @@ -267,127 +310,139 @@ public class HapticFeedbackCustomizationTest { + "</waveform-effect>" + "</vibration-effect>" + "</constant>" - + "</haptic-feedback-constants>"); + + "</haptic-feedback-constants>"; + + assertParseCustomizationsFails(xml, hasConfigFile, hasRes); } @Test - public void testParseCustomizations_emptyXml_throwsException() throws Exception { - assertParseCustomizationsFails(""); + @Parameters(method = "hapticFeedbackCustomizationTestArguments") + public void testParseCustomizations_emptyXml_throwsException( + boolean hasConfigFile, boolean hasRes) throws Exception { + assertParseCustomizationsFails("", hasConfigFile, hasRes); } @Test - public void testParseCustomizations_noVibrationXml_throwsException() throws Exception { - assertParseCustomizationsFails( - "<haptic-feedback-constants>" + @Parameters(method = "hapticFeedbackCustomizationTestArguments") + public void testParseCustomizations_noVibrationXml_throwsException( + boolean hasConfigFile, boolean hasRes) throws Exception { + String xml = "<haptic-feedback-constants>" + "<constant id=\"1\">" + "</constant>" - + "</haptic-feedback-constants>"); + + "</haptic-feedback-constants>"; + + assertParseCustomizationsFails(xml, hasConfigFile, hasRes); } @Test - public void testParseCustomizations_badEffectId_throwsException() throws Exception { + @Parameters(method = "hapticFeedbackCustomizationTestArguments") + public void testParseCustomizations_badEffectId_throwsException( + boolean hasConfigFile, boolean hasRes) throws Exception { // Negative id - assertParseCustomizationsFails( - "<haptic-feedback-constants>" + String xmlNegativeId = "<haptic-feedback-constants>" + "<constant id=\"-10\">" + COMPOSITION_VIBRATION_XML + "</constant>" - + "</haptic-feedback-constants>"); - + + "</haptic-feedback-constants>"; // Non-numeral id - assertParseCustomizationsFails( - "<haptic-feedback-constants>" + String xmlNonNumericalId = "<haptic-feedback-constants>" + "<constant id=\"xyz\">" + COMPOSITION_VIBRATION_XML + "</constant>" - + "</haptic-feedback-constants>"); + + "</haptic-feedback-constants>"; + + assertParseCustomizationsFails(xmlNegativeId, hasConfigFile, hasRes); + assertParseCustomizationsFails(xmlNonNumericalId, hasConfigFile, hasRes); } @Test - public void testParseCustomizations_malformedXml_throwsException() throws Exception { + @Parameters(method = "hapticFeedbackCustomizationTestArguments") + public void testParseCustomizations_malformedXml_throwsException( + boolean hasConfigFile, boolean hasRes) throws Exception { // No start "<constant>" tag - assertParseCustomizationsFails( - "<haptic-feedback-constants>" + String xmlNoStartConstantTag = "<haptic-feedback-constants>" + COMPOSITION_VIBRATION_XML + "</constant>" - + "</haptic-feedback-constants>"); - + + "</haptic-feedback-constants>"; // No end "<constant>" tag - assertParseCustomizationsFails( - "<haptic-feedback-constants>" + String xmlNoEndConstantTag = "<haptic-feedback-constants>" + "<constant id=\"10\">" + COMPOSITION_VIBRATION_XML - + "</haptic-feedback-constants>"); - + + "</haptic-feedback-constants>"; // No start "<haptic-feedback-constants>" tag - assertParseCustomizationsFails( - "<constant id=\"10\">" + String xmlNoStartCustomizationTag = "<constant id=\"10\">" + COMPOSITION_VIBRATION_XML + "</constant>" - + "</haptic-feedback-constants>"); - + + "</haptic-feedback-constants>"; // No end "<haptic-feedback-constants>" tag - assertParseCustomizationsFails( - "<haptic-feedback-constants>" + String xmlNoEndCustomizationTag = "<haptic-feedback-constants>" + "<constant id=\"10\">" + COMPOSITION_VIBRATION_XML - + "</constant>"); + + "</constant>"; + + assertParseCustomizationsFails(xmlNoStartConstantTag, hasConfigFile, hasRes); + assertParseCustomizationsFails(xmlNoEndConstantTag, hasConfigFile, hasRes); + assertParseCustomizationsFails(xmlNoStartCustomizationTag, hasConfigFile, hasRes); + assertParseCustomizationsFails(xmlNoEndCustomizationTag, hasConfigFile, hasRes); } @Test - public void testParseCustomizations_badVibrationXml_throwsException() throws Exception { - assertParseCustomizationsFails( - "<haptic-feedback-constants>" + @Parameters(method = "hapticFeedbackCustomizationTestArguments") + public void testParseCustomizations_badVibrationXml_throwsException( + boolean hasConfigFile, boolean hasRes) throws Exception { + String xmlBad1 = "<haptic-feedback-constants>" + "<constant id=\"10\">" + "<bad-vibration-effect></bad-vibration-effect>" + "</constant>" - + "</haptic-feedback-constants>"); - - assertParseCustomizationsFails( - "<haptic-feedback-constants>" + + "</haptic-feedback-constants>"; + String xmlBad2 = "<haptic-feedback-constants>" + "<constant id=\"10\">" + "<vibration-effect><predefined-effect name=\"bad-effect\"/></vibration-effect>" + "</constant>" - + "</haptic-feedback-constants>"); - - assertParseCustomizationsFails( - "<haptic-feedback-constants>" + + "</haptic-feedback-constants>"; + String xmlBad3 = "<haptic-feedback-constants>" + "<constant id=\"10\">" + "<vibration-select>" + "<vibration-effect><predefined-effect name=\"bad-effect\"/></vibration-effect>" + "</constant>" - + "</haptic-feedback-constants>"); - - assertParseCustomizationsFails( - "<haptic-feedback-constants>" + + "</haptic-feedback-constants>"; + String xmlBad4 = "<haptic-feedback-constants>" + "<constant id=\"10\">" + "<vibration-effect><predefined-effect name=\"bad-effect\"/></vibration-effect>" + "</vibration-select>" + "</constant>" - + "</haptic-feedback-constants>"); + + "</haptic-feedback-constants>"; + + assertParseCustomizationsFails(xmlBad1, hasConfigFile, hasRes); + assertParseCustomizationsFails(xmlBad2, hasConfigFile, hasRes); + assertParseCustomizationsFails(xmlBad3, hasConfigFile, hasRes); + assertParseCustomizationsFails(xmlBad4, hasConfigFile, hasRes); } @Test - public void testParseCustomizations_badConstantAttribute_throwsException() throws Exception { - assertParseCustomizationsFails( - "<haptic-feedback-constants>" + @Parameters(method = "hapticFeedbackCustomizationTestArguments") + public void testParseCustomizations_badConstantAttribute_throwsException( + boolean hasConfigFile, boolean hasRes) throws Exception { + String xmlBadConstantAttribute1 = "<haptic-feedback-constants>" + "<constant iddddd=\"10\">" + COMPOSITION_VIBRATION_XML + "</constant>" - + "</haptic-feedback-constants>"); - - assertParseCustomizationsFails( - "<haptic-feedback-constants>" + + "</haptic-feedback-constants>"; + String xmlBadConstantAttribute2 = "<haptic-feedback-constants>" + "<constant id=\"10\" unwanted-attr=\"1\">" + COMPOSITION_VIBRATION_XML + "</constant>" - + "</haptic-feedback-constants>"); + + "</haptic-feedback-constants>"; + + assertParseCustomizationsFails(xmlBadConstantAttribute1, hasConfigFile, hasRes); + assertParseCustomizationsFails(xmlBadConstantAttribute2, hasConfigFile, hasRes); } @Test - public void testParseCustomizations_duplicateEffects_throwsException() throws Exception { - assertParseCustomizationsFails( - "<haptic-feedback-constants>" + @Parameters(method = "hapticFeedbackCustomizationTestArguments") + public void testParseCustomizations_duplicateEffects_throwsException( + boolean hasConfigFile, boolean hasRes) throws Exception { + String xmlDuplicateEffect = "<haptic-feedback-constants>" + "<constant id=\"10\">" + COMPOSITION_VIBRATION_XML + "</constant>" @@ -397,30 +452,44 @@ public class HapticFeedbackCustomizationTest { + "<constant id=\"11\">" + PREDEFINED_VIBRATION_XML + "</constant>" - + "</haptic-feedback-constants>"); + + "</haptic-feedback-constants>"; + + assertParseCustomizationsFails(xmlDuplicateEffect, hasConfigFile, hasRes); } - private void assertParseCustomizationsSucceeds( - String xml, SparseArray<VibrationEffect> expectedCustomizations) throws Exception { - setupCustomizationFile(xml); + private void assertParseCustomizationsSucceeds(String xml, + SparseArray<VibrationEffect> expectedCustomizations, boolean hasConfigFile, + boolean hasRes) throws Exception { + setupParseCustomizations(xml, hasConfigFile, hasRes); assertThat(expectedCustomizations.contentEquals( HapticFeedbackCustomization.loadVibrations(mResourcesMock, mVibratorInfoMock))) - .isTrue(); + .isTrue(); } - private void assertParseCustomizationsFails(String xml) throws Exception { - setupCustomizationFile(xml); - assertThrows("Expected haptic feedback customization to fail for " + xml, + private void assertParseCustomizationsFails(String xml, boolean hasConfigFile, boolean hasRes) + throws Exception { + setupParseCustomizations(xml, hasConfigFile, hasRes); + assertThrows("Expected haptic feedback customization to fail", CustomizationParserException.class, () -> HapticFeedbackCustomization.loadVibrations( mResourcesMock, mVibratorInfoMock)); } - private void assertParseCustomizationsFails() throws Exception { - assertThrows("Expected haptic feedback customization to fail", - CustomizationParserException.class, - () -> HapticFeedbackCustomization.loadVibrations( - mResourcesMock, mVibratorInfoMock)); + private void setupParseCustomizations(String xml, boolean hasConfigFile, boolean hasRes) + throws Exception { + clearFileAndResourceSetup(); + if (hasConfigFile) { + setupCustomizationFile(xml); + } + if (hasRes) { + setupCustomizationResource(xml); + } + } + + private void clearFileAndResourceSetup() { + when(mResourcesMock.getString(R.string.config_hapticFeedbackCustomizationFile)) + .thenReturn(null); + when(mResourcesMock.getXml(haptic_feedback_customization)).thenReturn(null); } private void setupCustomizationFile(String xml) throws Exception { @@ -433,6 +502,13 @@ public class HapticFeedbackCustomizationTest { .thenReturn(path); } + private void setupCustomizationResource(String xml) throws Exception { + mSetFlagsRule.enableFlags( + Flags.FLAG_LOAD_HAPTIC_FEEDBACK_VIBRATION_CUSTOMIZATION_FROM_RESOURCES); + when(mResourcesMock.getXml(haptic_feedback_customization)) + .thenReturn(FakeXmlResourceParser.fromXml(xml)); + } + private void makeSupported(VibrationEffect... effects) { for (VibrationEffect effect : effects) { when(mVibratorInfoMock.areVibrationFeaturesSupported(effect)).thenReturn(true); diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibrationScalerTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibrationScalerTest.java index 9ebeaa8eb3fd..470469114dfa 100644 --- a/services/tests/vibrator/src/com/android/server/vibrator/VibrationScalerTest.java +++ b/services/tests/vibrator/src/com/android/server/vibrator/VibrationScalerTest.java @@ -50,10 +50,9 @@ import android.os.vibrator.PrimitiveSegment; import android.os.vibrator.StepSegment; import android.os.vibrator.VibrationConfig; import android.os.vibrator.VibrationEffectSegment; -import android.platform.test.annotations.RequiresFlagsDisabled; -import android.platform.test.annotations.RequiresFlagsEnabled; -import android.platform.test.flag.junit.CheckFlagsRule; -import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import android.provider.Settings; import androidx.test.InstrumentationRegistry; @@ -71,12 +70,13 @@ import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; public class VibrationScalerTest { + private static final float TOLERANCE = 1e-2f; + private static final int TEST_DEFAULT_AMPLITUDE = 255; + private static final float TEST_DEFAULT_SCALE_LEVEL_GAIN = 1.4f; @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule(); @Rule public FakeSettingsProviderRule mSettingsProviderRule = FakeSettingsProvider.rule(); - @Rule - public final CheckFlagsRule mCheckFlagsRule = - DeviceFlagsValueProvider.createCheckFlagsRule(); + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @Mock private PowerManagerInternal mPowerManagerInternalMock; @Mock private PackageManagerInternal mPackageManagerInternalMock; @@ -96,6 +96,10 @@ public class VibrationScalerTest { when(mContextSpy.getContentResolver()).thenReturn(contentResolver); when(mPackageManagerInternalMock.getSystemUiServiceComponent()) .thenReturn(new ComponentName("", "")); + when(mVibrationConfigMock.getDefaultVibrationAmplitude()) + .thenReturn(TEST_DEFAULT_AMPLITUDE); + when(mVibrationConfigMock.getDefaultVibrationScaleLevelGain()) + .thenReturn(TEST_DEFAULT_SCALE_LEVEL_GAIN); LocalServices.removeServiceForTest(PackageManagerInternal.class); LocalServices.addService(PackageManagerInternal.class, mPackageManagerInternalMock); @@ -107,7 +111,7 @@ public class VibrationScalerTest { mVibrationSettings = new VibrationSettings( mContextSpy, new Handler(mTestLooper.getLooper()), mVibrationConfigMock); - mVibrationScaler = new VibrationScaler(mContextSpy, mVibrationSettings); + mVibrationScaler = new VibrationScaler(mVibrationConfigMock, mVibrationSettings); mVibrationSettings.onSystemReady(); } @@ -147,33 +151,76 @@ public class VibrationScalerTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED) - public void testAdaptiveHapticsScale_withAdaptiveHapticsAvailable() { + @DisableFlags(Flags.FLAG_HAPTICS_SCALE_V2_ENABLED) + public void testGetScaleFactor_withLegacyScaling() { + // Default scale gain will be ignored. + when(mVibrationConfigMock.getDefaultVibrationScaleLevelGain()).thenReturn(1.4f); + mVibrationScaler = new VibrationScaler(mVibrationConfigMock, mVibrationSettings); + setDefaultIntensity(USAGE_TOUCH, Vibrator.VIBRATION_INTENSITY_LOW); - setDefaultIntensity(USAGE_RINGTONE, Vibrator.VIBRATION_INTENSITY_LOW); setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_HIGH); - setUserSetting(Settings.System.RING_VIBRATION_INTENSITY, VIBRATION_INTENSITY_HIGH); + assertEquals(1.4f, mVibrationScaler.getScaleFactor(USAGE_TOUCH), TOLERANCE); // VERY_HIGH + + setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_MEDIUM); + assertEquals(1.2f, mVibrationScaler.getScaleFactor(USAGE_TOUCH), TOLERANCE); // HIGH + + setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_LOW); + assertEquals(1f, mVibrationScaler.getScaleFactor(USAGE_TOUCH), TOLERANCE); // NONE + + setDefaultIntensity(USAGE_TOUCH, VIBRATION_INTENSITY_MEDIUM); + assertEquals(0.8f, mVibrationScaler.getScaleFactor(USAGE_TOUCH), TOLERANCE); // LOW + + setDefaultIntensity(USAGE_TOUCH, VIBRATION_INTENSITY_HIGH); + assertEquals(0.6f, mVibrationScaler.getScaleFactor(USAGE_TOUCH), TOLERANCE); // VERY_LOW + + setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_OFF); + // Vibration setting being bypassed will use default setting and not scale. + assertEquals(1f, mVibrationScaler.getScaleFactor(USAGE_TOUCH), TOLERANCE); // NONE + } + + @Test + @EnableFlags(Flags.FLAG_HAPTICS_SCALE_V2_ENABLED) + public void testGetScaleFactor_withScalingV2() { + // Test scale factors for a default gain of 1.4 + when(mVibrationConfigMock.getDefaultVibrationScaleLevelGain()).thenReturn(1.4f); + mVibrationScaler = new VibrationScaler(mVibrationConfigMock, mVibrationSettings); + + setDefaultIntensity(USAGE_TOUCH, Vibrator.VIBRATION_INTENSITY_LOW); + setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_HIGH); + assertEquals(1.95f, mVibrationScaler.getScaleFactor(USAGE_TOUCH), TOLERANCE); // VERY_HIGH + setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_MEDIUM); + assertEquals(1.4f, mVibrationScaler.getScaleFactor(USAGE_TOUCH), TOLERANCE); // HIGH + + setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_LOW); + assertEquals(1f, mVibrationScaler.getScaleFactor(USAGE_TOUCH), TOLERANCE); // NONE + + setDefaultIntensity(USAGE_TOUCH, VIBRATION_INTENSITY_MEDIUM); + assertEquals(0.71f, mVibrationScaler.getScaleFactor(USAGE_TOUCH), TOLERANCE); // LOW + + setDefaultIntensity(USAGE_TOUCH, VIBRATION_INTENSITY_HIGH); + assertEquals(0.51f, mVibrationScaler.getScaleFactor(USAGE_TOUCH), TOLERANCE); // VERY_LOW + + setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_OFF); + // Vibration setting being bypassed will use default setting and not scale. + assertEquals(1f, mVibrationScaler.getScaleFactor(USAGE_TOUCH), TOLERANCE); // NONE + } + + @Test + @EnableFlags(Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED) + public void testAdaptiveHapticsScale_withAdaptiveHapticsAvailable() { mVibrationScaler.updateAdaptiveHapticsScale(USAGE_TOUCH, 0.5f); mVibrationScaler.updateAdaptiveHapticsScale(USAGE_RINGTONE, 0.2f); assertEquals(0.5f, mVibrationScaler.getAdaptiveHapticsScale(USAGE_TOUCH)); assertEquals(0.2f, mVibrationScaler.getAdaptiveHapticsScale(USAGE_RINGTONE)); assertEquals(1f, mVibrationScaler.getAdaptiveHapticsScale(USAGE_NOTIFICATION)); - - setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_OFF); - // Vibration setting being bypassed will apply adaptive haptics scales. assertEquals(0.2f, mVibrationScaler.getAdaptiveHapticsScale(USAGE_RINGTONE)); } @Test - @RequiresFlagsDisabled(Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED) + @DisableFlags(Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED) public void testAdaptiveHapticsScale_flagDisabled_adaptiveHapticScaleAlwaysNone() { - setDefaultIntensity(USAGE_TOUCH, Vibrator.VIBRATION_INTENSITY_LOW); - setDefaultIntensity(USAGE_RINGTONE, Vibrator.VIBRATION_INTENSITY_LOW); - setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_HIGH); - setUserSetting(Settings.System.RING_VIBRATION_INTENSITY, VIBRATION_INTENSITY_HIGH); - mVibrationScaler.updateAdaptiveHapticsScale(USAGE_TOUCH, 0.5f); mVibrationScaler.updateAdaptiveHapticsScale(USAGE_RINGTONE, 0.2f); @@ -233,7 +280,7 @@ public class VibrationScalerTest { } @Test - @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) public void scale_withVendorEffect_setsEffectStrengthBasedOnSettings() { setDefaultIntensity(USAGE_NOTIFICATION, VIBRATION_INTENSITY_LOW); setUserSetting(Settings.System.NOTIFICATION_VIBRATION_INTENSITY, VIBRATION_INTENSITY_HIGH); @@ -269,13 +316,13 @@ public class VibrationScalerTest { StepSegment resolved = getFirstSegment(mVibrationScaler.scale( VibrationEffect.createOneShot(10, VibrationEffect.DEFAULT_AMPLITUDE), USAGE_RINGTONE)); - assertTrue(resolved.getAmplitude() > 0); + assertEquals(TEST_DEFAULT_AMPLITUDE / 255f, resolved.getAmplitude(), TOLERANCE); resolved = getFirstSegment(mVibrationScaler.scale( VibrationEffect.createWaveform(new long[]{10}, new int[]{VibrationEffect.DEFAULT_AMPLITUDE}, -1), USAGE_RINGTONE)); - assertTrue(resolved.getAmplitude() > 0); + assertEquals(TEST_DEFAULT_AMPLITUDE / 255f, resolved.getAmplitude(), TOLERANCE); } @Test @@ -330,7 +377,7 @@ public class VibrationScalerTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED) + @EnableFlags(Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED) public void scale_withAdaptiveHaptics_scalesVibrationsCorrectly() { setDefaultIntensity(USAGE_RINGTONE, VIBRATION_INTENSITY_HIGH); setDefaultIntensity(USAGE_NOTIFICATION, VIBRATION_INTENSITY_HIGH); @@ -351,7 +398,7 @@ public class VibrationScalerTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED) + @EnableFlags(Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED) public void scale_clearAdaptiveHapticsScales_clearsAllCachedScales() { setDefaultIntensity(USAGE_RINGTONE, VIBRATION_INTENSITY_HIGH); setDefaultIntensity(USAGE_NOTIFICATION, VIBRATION_INTENSITY_HIGH); @@ -373,7 +420,7 @@ public class VibrationScalerTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED) + @EnableFlags(Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED) public void scale_removeAdaptiveHapticsScale_removesCachedScale() { setDefaultIntensity(USAGE_RINGTONE, VIBRATION_INTENSITY_HIGH); setDefaultIntensity(USAGE_NOTIFICATION, VIBRATION_INTENSITY_HIGH); @@ -395,7 +442,7 @@ public class VibrationScalerTest { } @Test - @RequiresFlagsEnabled({ + @EnableFlags({ android.os.vibrator.Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED, android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS, }) diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibrationSettingsTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibrationSettingsTest.java index 72ef888aa061..6f06050f55ff 100644 --- a/services/tests/vibrator/src/com/android/server/vibrator/VibrationSettingsTest.java +++ b/services/tests/vibrator/src/com/android/server/vibrator/VibrationSettingsTest.java @@ -119,6 +119,7 @@ public class VibrationSettingsTest { USAGE_PHYSICAL_EMULATION, USAGE_RINGTONE, USAGE_TOUCH, + USAGE_IME_FEEDBACK }; @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule(); @@ -525,7 +526,7 @@ public class VibrationSettingsTest { setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_OFF); for (int usage : ALL_USAGES) { - if (usage == USAGE_TOUCH) { + if (usage == USAGE_TOUCH || usage == USAGE_IME_FEEDBACK) { assertVibrationIgnoredForUsage(usage, Vibration.Status.IGNORED_FOR_SETTINGS); } else { assertVibrationNotIgnoredForUsage(usage); @@ -601,14 +602,14 @@ public class VibrationSettingsTest { @Test public void shouldIgnoreVibration_withKeyboardSettingsOff_shouldIgnoreKeyboardVibration() { + setKeyboardVibrationSettingsSupported(true); setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_MEDIUM); setUserSetting(Settings.System.KEYBOARD_VIBRATION_ENABLED, 0 /* OFF*/); - setKeyboardVibrationSettingsSupported(true); // Keyboard touch ignored. assertVibrationIgnoredForAttributes( new VibrationAttributes.Builder() - .setUsage(USAGE_TOUCH) + .setUsage(USAGE_IME_FEEDBACK) .setCategory(VibrationAttributes.CATEGORY_KEYBOARD) .build(), Vibration.Status.IGNORED_FOR_SETTINGS); @@ -617,7 +618,7 @@ public class VibrationSettingsTest { assertVibrationNotIgnoredForUsage(USAGE_TOUCH); assertVibrationNotIgnoredForAttributes( new VibrationAttributes.Builder() - .setUsage(USAGE_TOUCH) + .setUsage(USAGE_IME_FEEDBACK) .setCategory(VibrationAttributes.CATEGORY_KEYBOARD) .setFlags(VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_OFF) .build()); @@ -625,9 +626,9 @@ public class VibrationSettingsTest { @Test public void shouldIgnoreVibration_withKeyboardSettingsOn_shouldNotIgnoreKeyboardVibration() { + setKeyboardVibrationSettingsSupported(true); setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_OFF); setUserSetting(Settings.System.KEYBOARD_VIBRATION_ENABLED, 1 /* ON */); - setKeyboardVibrationSettingsSupported(true); // General touch ignored. assertVibrationIgnoredForUsage(USAGE_TOUCH, Vibration.Status.IGNORED_FOR_SETTINGS); @@ -635,16 +636,16 @@ public class VibrationSettingsTest { // Keyboard touch not ignored. assertVibrationNotIgnoredForAttributes( new VibrationAttributes.Builder() - .setUsage(USAGE_TOUCH) + .setUsage(USAGE_IME_FEEDBACK) .setCategory(VibrationAttributes.CATEGORY_KEYBOARD) .build()); } @Test - public void shouldIgnoreVibration_notSupportKeyboardVibration_ignoresKeyboardTouchVibration() { + public void shouldIgnoreVibration_notSupportKeyboardVibration_followsTouchFeedbackSettings() { + setKeyboardVibrationSettingsSupported(false); setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_OFF); setUserSetting(Settings.System.KEYBOARD_VIBRATION_ENABLED, 1 /* ON */); - setKeyboardVibrationSettingsSupported(false); // General touch ignored. assertVibrationIgnoredForUsage(USAGE_TOUCH, Vibration.Status.IGNORED_FOR_SETTINGS); @@ -652,7 +653,7 @@ public class VibrationSettingsTest { // Keyboard touch ignored. assertVibrationIgnoredForAttributes( new VibrationAttributes.Builder() - .setUsage(USAGE_TOUCH) + .setUsage(USAGE_IME_FEEDBACK) .setCategory(VibrationAttributes.CATEGORY_KEYBOARD) .build(), Vibration.Status.IGNORED_FOR_SETTINGS); diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java index 3bd56deb32f4..0fbdce4ce61a 100644 --- a/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java +++ b/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java @@ -102,6 +102,8 @@ public class VibrationThreadTest { private static final String PACKAGE_NAME = "package"; private static final VibrationAttributes ATTRS = new VibrationAttributes.Builder().build(); private static final int TEST_RAMP_STEP_DURATION = 5; + private static final int TEST_DEFAULT_AMPLITUDE = 255; + private static final float TEST_DEFAULT_SCALE_LEVEL_GAIN = 1.4f; @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule(); @Rule @@ -133,6 +135,10 @@ public class VibrationThreadTest { when(mVibrationConfigMock.getDefaultVibrationIntensity(anyInt())) .thenReturn(Vibrator.VIBRATION_INTENSITY_MEDIUM); when(mVibrationConfigMock.getRampStepDurationMs()).thenReturn(TEST_RAMP_STEP_DURATION); + when(mVibrationConfigMock.getDefaultVibrationAmplitude()) + .thenReturn(TEST_DEFAULT_AMPLITUDE); + when(mVibrationConfigMock.getDefaultVibrationScaleLevelGain()) + .thenReturn(TEST_DEFAULT_SCALE_LEVEL_GAIN); when(mPackageManagerInternalMock.getSystemUiServiceComponent()) .thenReturn(new ComponentName("", "")); doAnswer(answer -> { @@ -146,7 +152,7 @@ public class VibrationThreadTest { Context context = InstrumentationRegistry.getContext(); mVibrationSettings = new VibrationSettings(context, new Handler(mTestLooper.getLooper()), mVibrationConfigMock); - mVibrationScaler = new VibrationScaler(context, mVibrationSettings); + mVibrationScaler = new VibrationScaler(mVibrationConfigMock, mVibrationSettings); mockVibrators(VIBRATOR_ID); diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibratorControlServiceTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibratorControlServiceTest.java index c496bbb82630..79e272b7ec01 100644 --- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorControlServiceTest.java +++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorControlServiceTest.java @@ -37,6 +37,7 @@ import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import android.content.ComponentName; +import android.content.Context; import android.content.pm.PackageManagerInternal; import android.frameworks.vibrator.ScaleParam; import android.frameworks.vibrator.VibrationParam; @@ -46,6 +47,7 @@ import android.os.IBinder; import android.os.Process; import android.os.test.TestLooper; import android.os.vibrator.Flags; +import android.os.vibrator.VibrationConfig; import android.platform.test.annotations.RequiresFlagsDisabled; import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; @@ -97,8 +99,9 @@ public class VibratorControlServiceTest { LocalServices.addService(PackageManagerInternal.class, mPackageManagerInternalMock); TestLooper testLooper = new TestLooper(); - mVibrationSettings = new VibrationSettings( - ApplicationProvider.getApplicationContext(), new Handler(testLooper.getLooper())); + Context context = ApplicationProvider.getApplicationContext(); + mVibrationSettings = new VibrationSettings(context, new Handler(testLooper.getLooper()), + new VibrationConfig(context.getResources())); mFakeVibratorController = new FakeVibratorController(mTestLooper.getLooper()); mVibratorControlService = new VibratorControlService( diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java index e411a178eca4..f009229e216d 100644 --- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java +++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java @@ -2743,7 +2743,7 @@ public class VibratorManagerServiceTest { } private HalVibration performHapticFeedbackAndWaitUntilFinished(VibratorManagerService service, - int constant, boolean always) throws InterruptedException { + int constant, boolean always) throws InterruptedException { HalVibration vib = service.performHapticFeedbackInternal(UID, Context.DEVICE_ID_DEFAULT, PACKAGE_NAME, constant, "some reason", service, always ? HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING : 0 /* flags */, diff --git a/services/tests/vibrator/utils/com/android/server/vibrator/FakeXmlResourceParser.java b/services/tests/vibrator/utils/com/android/server/vibrator/FakeXmlResourceParser.java new file mode 100644 index 000000000000..ab7d43c66765 --- /dev/null +++ b/services/tests/vibrator/utils/com/android/server/vibrator/FakeXmlResourceParser.java @@ -0,0 +1,330 @@ +/* + * 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.vibrator; + +import android.content.res.XmlResourceParser; +import android.util.Xml; + +import com.android.modules.utils.TypedXmlPullParser; + +import org.xmlpull.v1.XmlPullParserException; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; + +/** + * Wrapper to use TypedXmlPullParser as XmlResourceParser for Resources.getXml(). This is borrowed + * from {@code ZenModeHelperTest}. + */ +public final class FakeXmlResourceParser implements XmlResourceParser { + private final TypedXmlPullParser mParser; + + public FakeXmlResourceParser(TypedXmlPullParser parser) { + this.mParser = parser; + } + + /** Create a {@link FakeXmlResourceParser} given a xml {@link String}. */ + public static XmlResourceParser fromXml(String xml) throws XmlPullParserException { + TypedXmlPullParser parser = Xml.newFastPullParser(); + parser.setInput(new BufferedInputStream(new ByteArrayInputStream(xml.getBytes())), null); + return new FakeXmlResourceParser(parser); + } + + @Override + public int getEventType() throws XmlPullParserException { + return mParser.getEventType(); + } + + @Override + public void setFeature(String name, boolean state) throws XmlPullParserException { + mParser.setFeature(name, state); + } + + @Override + public boolean getFeature(String name) { + return false; + } + + @Override + public void setProperty(String name, Object value) throws XmlPullParserException { + mParser.setProperty(name, value); + } + + @Override + public Object getProperty(String name) { + return mParser.getProperty(name); + } + + @Override + public void setInput(Reader in) throws XmlPullParserException { + mParser.setInput(in); + } + + @Override + public void setInput(InputStream inputStream, String inputEncoding) + throws XmlPullParserException { + mParser.setInput(inputStream, inputEncoding); + } + + @Override + public String getInputEncoding() { + return mParser.getInputEncoding(); + } + + @Override + public void defineEntityReplacementText(String entityName, String replacementText) + throws XmlPullParserException { + mParser.defineEntityReplacementText(entityName, replacementText); + } + + @Override + public int getNamespaceCount(int depth) throws XmlPullParserException { + return mParser.getNamespaceCount(depth); + } + + @Override + public String getNamespacePrefix(int pos) throws XmlPullParserException { + return mParser.getNamespacePrefix(pos); + } + + @Override + public String getNamespaceUri(int pos) throws XmlPullParserException { + return mParser.getNamespaceUri(pos); + } + + @Override + public String getNamespace(String prefix) { + return mParser.getNamespace(prefix); + } + + @Override + public int getDepth() { + return mParser.getDepth(); + } + + @Override + public String getPositionDescription() { + return mParser.getPositionDescription(); + } + + @Override + public int getLineNumber() { + return mParser.getLineNumber(); + } + + @Override + public int getColumnNumber() { + return mParser.getColumnNumber(); + } + + @Override + public boolean isWhitespace() throws XmlPullParserException { + return mParser.isWhitespace(); + } + + @Override + public String getText() { + return mParser.getText(); + } + + @Override + public char[] getTextCharacters(int[] holderForStartAndLength) { + return mParser.getTextCharacters(holderForStartAndLength); + } + + @Override + public String getNamespace() { + return mParser.getNamespace(); + } + + @Override + public String getName() { + return mParser.getName(); + } + + @Override + public String getPrefix() { + return mParser.getPrefix(); + } + + @Override + public boolean isEmptyElementTag() throws XmlPullParserException { + return false; + } + + @Override + public int getAttributeCount() { + return mParser.getAttributeCount(); + } + + @Override + public int next() throws IOException, XmlPullParserException { + return mParser.next(); + } + + @Override + public int nextToken() throws XmlPullParserException, IOException { + return mParser.next(); + } + + @Override + public void require(int type, String namespace, String name) + throws XmlPullParserException, IOException { + mParser.require(type, namespace, name); + } + + @Override + public String nextText() throws XmlPullParserException, IOException { + return mParser.nextText(); + } + + @Override + public String getAttributeNamespace(int index) { + return ""; + } + + @Override + public String getAttributeName(int index) { + return mParser.getAttributeName(index); + } + + @Override + public String getAttributePrefix(int index) { + return mParser.getAttributePrefix(index); + } + + @Override + public String getAttributeType(int index) { + return mParser.getAttributeType(index); + } + + @Override + public boolean isAttributeDefault(int index) { + return mParser.isAttributeDefault(index); + } + + @Override + public String getAttributeValue(int index) { + return mParser.getAttributeValue(index); + } + + @Override + public String getAttributeValue(String namespace, String name) { + return mParser.getAttributeValue(namespace, name); + } + + @Override + public int getAttributeNameResource(int index) { + return 0; + } + + @Override + public int getAttributeListValue(String namespace, String attribute, String[] options, + int defaultValue) { + return 0; + } + + @Override + public boolean getAttributeBooleanValue(String namespace, String attribute, + boolean defaultValue) { + return false; + } + + @Override + public int getAttributeResourceValue(String namespace, String attribute, int defaultValue) { + return 0; + } + + @Override + public int getAttributeIntValue(String namespace, String attribute, int defaultValue) { + return 0; + } + + @Override + public int getAttributeUnsignedIntValue(String namespace, String attribute, + int defaultValue) { + return 0; + } + + @Override + public float getAttributeFloatValue(String namespace, String attribute, + float defaultValue) { + return 0; + } + + @Override + public int getAttributeListValue(int index, String[] options, int defaultValue) { + return 0; + } + + @Override + public boolean getAttributeBooleanValue(int index, boolean defaultValue) { + return false; + } + + @Override + public int getAttributeResourceValue(int index, int defaultValue) { + return 0; + } + + @Override + public int getAttributeIntValue(int index, int defaultValue) { + return 0; + } + + @Override + public int getAttributeUnsignedIntValue(int index, int defaultValue) { + return 0; + } + + @Override + public float getAttributeFloatValue(int index, float defaultValue) { + return 0; + } + + @Override + public String getIdAttribute() { + return null; + } + + @Override + public String getClassAttribute() { + return null; + } + + @Override + public int getIdAttributeResourceValue(int defaultValue) { + return 0; + } + + @Override + public int getStyleAttribute() { + return 0; + } + + @Override + public void close() { + } + + @Override + public int nextTag() throws IOException, XmlPullParserException { + return mParser.nextTag(); + } +} 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/ActivitySnapshotControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivitySnapshotControllerTests.java index 03d30294e1d8..2a53df9f8353 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivitySnapshotControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivitySnapshotControllerTests.java @@ -258,6 +258,6 @@ public class ActivitySnapshotControllerTests extends WindowTestsBase { Surface.ROTATION_0, new Point(100, 100), new Rect() /* contentInsets */, new Rect() /* letterboxInsets*/, false /* isLowResolution */, true /* isRealSnapshot */, WINDOWING_MODE_FULLSCREEN, 0 /* mSystemUiVisibility */, - false /* isTranslucent */, false /* hasImeSurface */); + false /* isTranslucent */, false /* hasImeSurface */, 0 /* uiMode */); } } 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/AppCompatTransparentActivityRobot.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatTransparentActivityRobot.java index 3cfbb9e708f9..5af7093b6b48 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatTransparentActivityRobot.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatTransparentActivityRobot.java @@ -62,6 +62,10 @@ class AppCompatTransparentActivityRobot { consumer.accept(mActivityRobot); } + void setDisplayContentBounds(int left, int top, int right, int bottom) { + mActivityRobot.displayContent().setBounds(left, top, right, bottom); + } + void launchTransparentActivity() { mActivityRobot.launchActivity(/*minAspectRatio */ -1, /* maxAspectRatio */ -1, SCREEN_ORIENTATION_PORTRAIT, /* transparent */ true, 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/DimmerTests.java b/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java index 771e290f60fd..e57e36d36621 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java @@ -59,6 +59,7 @@ public class DimmerTests extends WindowTestsBase { TestWindowContainer(WindowManagerService wm) { super(wm); + setVisibleRequested(true); } @Override diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java index e2524a289b7d..ddadbc41a1c0 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java @@ -115,6 +115,17 @@ public class DisplayWindowSettingsTests extends WindowTestsBase { } @Test + public void testPrimaryDisplayUnchanged_whenWindowingModeAlreadySet_NoFreeformSupport() { + mPrimaryDisplay.getDefaultTaskDisplayArea().setWindowingMode( + WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW); + + mDisplayWindowSettings.applySettingsToDisplayLocked(mPrimaryDisplay); + + assertEquals(WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW, + mPrimaryDisplay.getDefaultTaskDisplayArea().getWindowingMode()); + } + + @Test public void testPrimaryDisplayDefaultToFullscreen_HasFreeformSupport_NonPc_NoDesktopMode() { mWm.mAtmService.mSupportsFreeformWindowManagement = true; diff --git a/services/tests/wmtests/src/com/android/server/wm/FrameRateSelectionPriorityTests.java b/services/tests/wmtests/src/com/android/server/wm/FrameRateSelectionPriorityTests.java index e77c14a60179..eacb8e9d628d 100644 --- a/services/tests/wmtests/src/com/android/server/wm/FrameRateSelectionPriorityTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/FrameRateSelectionPriorityTests.java @@ -82,12 +82,15 @@ public class FrameRateSelectionPriorityTests extends WindowTestsBase { public void setUp() { DisplayInfo di = new DisplayInfo(mDisplayInfo); Mode defaultMode = di.getDefaultMode(); - di.supportedModes = new Mode[] { - new Mode(1, defaultMode.getPhysicalWidth(), defaultMode.getPhysicalHeight(), 90), - new Mode(2, defaultMode.getPhysicalWidth(), defaultMode.getPhysicalHeight(), 70), - new Mode(LOW_MODE_ID, - defaultMode.getPhysicalWidth(), defaultMode.getPhysicalHeight(), 60), - }; + Mode hiMode = new Mode(1, + defaultMode.getPhysicalWidth(), defaultMode.getPhysicalHeight(), 90); + Mode midMode = new Mode(2, + defaultMode.getPhysicalWidth(), defaultMode.getPhysicalHeight(), 70); + Mode lowMode = new Mode(LOW_MODE_ID, + defaultMode.getPhysicalWidth(), defaultMode.getPhysicalHeight(), 60); + + di.supportedModes = new Mode[] { hiMode, midMode }; + di.appsSupportedModes = new Mode[] { hiMode, midMode, lowMode }; di.defaultModeId = 1; mRefreshRatePolicy = new RefreshRatePolicy(mWm, di, mDenylist); when(mDisplayPolicy.getRefreshRatePolicy()).thenReturn(mRefreshRatePolicy); diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxTest.java index ffaa2d820203..400fe8b05526 100644 --- a/services/tests/wmtests/src/com/android/server/wm/LetterboxTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxTest.java @@ -24,6 +24,7 @@ import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; @@ -45,6 +46,12 @@ import org.mockito.invocation.InvocationOnMock; import java.util.function.Supplier; +/** + * Test class for {@link Letterbox}. + * <p> + * Build/Install/Run: + * atest WmTests:LetterboxTest + */ @SmallTest @Presubmit public class LetterboxTest { @@ -53,21 +60,21 @@ public class LetterboxTest { SurfaceControlMocker mSurfaces; SurfaceControl.Transaction mTransaction; - private boolean mAreCornersRounded = false; - private int mColor = Color.BLACK; - private boolean mHasWallpaperBackground = false; - private int mBlurRadius = 0; - private float mDarkScrimAlpha = 0.5f; private SurfaceControl mParentSurface = mock(SurfaceControl.class); + private AppCompatLetterboxOverrides mLetterboxOverrides; @Before public void setUp() throws Exception { mSurfaces = new SurfaceControlMocker(); + mLetterboxOverrides = mock(AppCompatLetterboxOverrides.class); + doReturn(false).when(mLetterboxOverrides).shouldLetterboxHaveRoundedCorners(); + doReturn(Color.valueOf(Color.BLACK)).when(mLetterboxOverrides) + .getLetterboxBackgroundColor(); + doReturn(false).when(mLetterboxOverrides).hasWallpaperBackgroundForLetterbox(); + doReturn(0).when(mLetterboxOverrides).getLetterboxWallpaperBlurRadiusPx(); + doReturn(0.5f).when(mLetterboxOverrides).getLetterboxWallpaperDarkScrimAlpha(); mLetterbox = new Letterbox(mSurfaces, StubTransaction::new, - () -> mAreCornersRounded, () -> Color.valueOf(mColor), - () -> mHasWallpaperBackground, () -> mBlurRadius, () -> mDarkScrimAlpha, - mock(AppCompatReachabilityPolicy.class), - () -> mParentSurface); + mock(AppCompatReachabilityPolicy.class), mLetterboxOverrides, () -> mParentSurface); mTransaction = spy(StubTransaction.class); } @@ -183,7 +190,8 @@ public class LetterboxTest { verify(mTransaction).setColor(mSurfaces.top, new float[]{0, 0, 0}); - mColor = Color.GREEN; + doReturn(Color.valueOf(Color.GREEN)).when(mLetterboxOverrides) + .getLetterboxBackgroundColor(); assertTrue(mLetterbox.needsApplySurfaceChanges()); @@ -200,12 +208,12 @@ public class LetterboxTest { verify(mTransaction).setAlpha(mSurfaces.top, 1.0f); assertFalse(mLetterbox.needsApplySurfaceChanges()); - mHasWallpaperBackground = true; + doReturn(true).when(mLetterboxOverrides).hasWallpaperBackgroundForLetterbox(); assertTrue(mLetterbox.needsApplySurfaceChanges()); applySurfaceChanges(); - verify(mTransaction).setAlpha(mSurfaces.fullWindowSurface, mDarkScrimAlpha); + verify(mTransaction).setAlpha(mSurfaces.fullWindowSurface, /* alpha */ 0.5f); } @Test @@ -234,7 +242,7 @@ public class LetterboxTest { @Test public void testApplySurfaceChanges_cornersRounded_surfaceFullWindowSurfaceCreated() { - mAreCornersRounded = true; + doReturn(true).when(mLetterboxOverrides).shouldLetterboxHaveRoundedCorners(); mLetterbox.layout(new Rect(0, 0, 10, 10), new Rect(0, 1, 10, 10), new Point(1000, 2000)); applySurfaceChanges(); @@ -243,7 +251,7 @@ public class LetterboxTest { @Test public void testApplySurfaceChanges_wallpaperBackground_surfaceFullWindowSurfaceCreated() { - mHasWallpaperBackground = true; + doReturn(true).when(mLetterboxOverrides).hasWallpaperBackgroundForLetterbox(); mLetterbox.layout(new Rect(0, 0, 10, 10), new Rect(0, 1, 10, 10), new Point(1000, 2000)); applySurfaceChanges(); @@ -252,7 +260,7 @@ public class LetterboxTest { @Test public void testNotIntersectsOrFullyContains_cornersRounded() { - mAreCornersRounded = true; + doReturn(true).when(mLetterboxOverrides).shouldLetterboxHaveRoundedCorners(); mLetterbox.layout(new Rect(0, 0, 10, 10), new Rect(0, 1, 10, 10), new Point(0, 0)); applySurfaceChanges(); 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..04997f8da86a 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; @@ -128,7 +123,7 @@ public class LetterboxUiControllerTest extends WindowTestsBase { // Do not apply crop if taskbar is collapsed taskbar.setFrame(TASKBAR_COLLAPSED_BOUNDS); - assertNull(mController.getExpandedTaskbarOrNull(mainWindow)); + assertNull(AppCompatUtils.getExpandedTaskbarOrNull(mainWindow)); mLetterboxedPortraitTaskBounds.set(SCREEN_WIDTH / 4, SCREEN_HEIGHT / 4, SCREEN_WIDTH - SCREEN_WIDTH / 4, SCREEN_HEIGHT - SCREEN_HEIGHT / 4); @@ -150,7 +145,7 @@ public class LetterboxUiControllerTest extends WindowTestsBase { // Apply crop if taskbar is expanded taskbar.setFrame(TASKBAR_EXPANDED_BOUNDS); - assertNotNull(mController.getExpandedTaskbarOrNull(mainWindow)); + assertNotNull(AppCompatUtils.getExpandedTaskbarOrNull(mainWindow)); mLetterboxedPortraitTaskBounds.set(SCREEN_WIDTH / 4, 0, SCREEN_WIDTH - SCREEN_WIDTH / 4, SCREEN_HEIGHT); @@ -174,7 +169,7 @@ public class LetterboxUiControllerTest extends WindowTestsBase { // Apply crop if taskbar is expanded taskbar.setFrame(TASKBAR_EXPANDED_BOUNDS); - assertNotNull(mController.getExpandedTaskbarOrNull(mainWindow)); + assertNotNull(AppCompatUtils.getExpandedTaskbarOrNull(mainWindow)); // With SizeCompat scaling doReturn(true).when(mActivity).inSizeCompatMode(); mainWindow.mInvGlobalScale = scaling; @@ -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/RecentTasksTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java index 33f7035dbf18..b95f621b7f1a 100644 --- a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java @@ -1413,7 +1413,7 @@ public class RecentTasksTest extends WindowTestsBase { Surface.ROTATION_0, taskSize, new Rect() /* contentInsets */, new Rect() /* letterboxInsets*/, false /* isLowResolution */, true /* isRealSnapshot */, WINDOWING_MODE_FULLSCREEN, 0 /* mSystemUiVisibility */, - false /* isTranslucent */, false /* hasImeSurface */); + false /* isTranslucent */, false /* hasImeSurface */, 0 /* uiMode */); } /** diff --git a/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java index 7ebf9ac324d5..3fa38bfe7185 100644 --- a/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java @@ -66,7 +66,6 @@ public class RefreshRatePolicyTest extends WindowTestsBase { private RefreshRatePolicy mPolicy; private HighRefreshRateDenylist mDenylist = mock(HighRefreshRateDenylist.class); - private FrameRateVote mTempFrameRateVote = new FrameRateVote(); private static final FrameRateVote FRAME_RATE_VOTE_NONE = new FrameRateVote(); private static final FrameRateVote FRAME_RATE_VOTE_DENY_LIST = @@ -98,18 +97,14 @@ public class RefreshRatePolicyTest extends WindowTestsBase { @Before public void setUp() { Mode defaultMode = mDisplayInfo.getDefaultMode(); - mDisplayInfo.supportedModes = new Mode[] { - new Mode(HI_MODE_ID, - defaultMode.getPhysicalWidth(), defaultMode.getPhysicalHeight(), - HI_REFRESH_RATE), - new Mode(MID_MODE_ID, - defaultMode.getPhysicalWidth(), defaultMode.getPhysicalHeight(), - MID_REFRESH_RATE), - new Mode(LOW_MODE_ID, - defaultMode.getPhysicalWidth(), defaultMode.getPhysicalHeight(), - LOW_REFRESH_RATE), - }; - mDisplayInfo.appsSupportedModes = mDisplayInfo.supportedModes; + Mode hiMode = new Mode(HI_MODE_ID, + defaultMode.getPhysicalWidth(), defaultMode.getPhysicalHeight(), HI_REFRESH_RATE); + Mode midMode = new Mode(MID_MODE_ID, + defaultMode.getPhysicalWidth(), defaultMode.getPhysicalHeight(), MID_REFRESH_RATE); + Mode lowMode = new Mode(LOW_MODE_ID, + defaultMode.getPhysicalWidth(), defaultMode.getPhysicalHeight(), LOW_REFRESH_RATE); + mDisplayInfo.supportedModes = new Mode[] { hiMode, midMode }; + mDisplayInfo.appsSupportedModes = new Mode[] { hiMode, midMode, lowMode }; mDisplayInfo.defaultModeId = HI_MODE_ID; mPolicy = new RefreshRatePolicy(mWm, mDisplayInfo, mDenylist); } diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java index aa997ac42c66..31488084daa3 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java @@ -247,9 +247,9 @@ public class SizeCompatTests extends WindowTestsBase { .build(); mTask.addChild(translucentActivity); - spyOn(translucentActivity.mLetterboxUiController); - doReturn(true).when(translucentActivity.mLetterboxUiController) - .shouldShowLetterboxUi(any()); + spyOn(translucentActivity.mAppCompatController.getAppCompatLetterboxPolicy()); + doReturn(true).when(translucentActivity.mAppCompatController + .getAppCompatLetterboxPolicy()).shouldShowLetterboxUi(any()); addWindowToActivity(translucentActivity); translucentActivity.mRootWindowContainer.performSurfacePlacement(); 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/TaskSnapshotPersisterTestBase.java b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterTestBase.java index 84c069691e04..1e0cef0514d8 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterTestBase.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterTestBase.java @@ -227,7 +227,7 @@ class TaskSnapshotPersisterTestBase extends WindowTestsBase { // disk. false /* isLowResolution */, mIsRealSnapshot, mWindowingMode, mSystemUiVisibility, mIsTranslucent, - false /* hasImeSurface */); + false /* hasImeSurface */, 0 /* uiMode */); } } } diff --git a/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java b/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java index 4b0668f7a056..d62c626f9a90 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java +++ b/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java @@ -270,12 +270,6 @@ class TestWindowManagerPolicy implements WindowManagerPolicy { } @Override - public boolean performHapticFeedback(int uid, String packageName, int effectId, String reason, - int flags, int privFlags) { - return false; - } - - @Override public void keepScreenOnStartedLw() { } 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 cbf17c408115..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,8 +22,11 @@ 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; import android.platform.test.annotations.Presubmit; import androidx.annotation.NonNull; @@ -207,6 +210,25 @@ public class TransparentPolicyTest extends WindowTestsBase { }); } + @EnableFlags(com.android.window.flags.Flags.FLAG_RESPECT_NON_TOP_VISIBLE_FIXED_ORIENTATION) + @Test + public void testNotRunStrategyToTranslucentActivitiesIfRespectOrientation() { + runTestScenario(robot -> robot.transparentActivity(ta -> ta.applyOnActivity((a) -> { + a.configureTopActivityIgnoreOrientationRequest(false); + // The translucent activity is SCREEN_ORIENTATION_PORTRAIT. + ta.launchTransparentActivityInTask(); + // Though TransparentPolicyState will be started, it won't be considered as running. + ta.checkTopActivityTransparentPolicyStateIsRunning(/* running */ false); + + // If the display changes to ignore orientation request, e.g. unfold, the policy should + // take effect. + a.configureTopActivityIgnoreOrientationRequest(true); + ta.checkTopActivityTransparentPolicyStateIsRunning(/* running */ true); + ta.setDisplayContentBounds(0, 0, 900, 1800); + ta.checkTopActivityHasInheritedBoundsFrom(/* fromTop */ 1); + })), /* displayWidth */ 500, /* displayHeight */ 1000); + } + @Test public void testTranslucentActivitiesDontGoInSizeCompatMode() { runTestScenario((robot) -> { @@ -343,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/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java index 2b611b754edd..18255b8d82f8 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java @@ -1488,7 +1488,7 @@ public class WindowOrganizerTests extends WindowTestsBase { @Test public void testReorderWithParents() { /* - root + default TDA ____|______ | | firstTda secondTda @@ -1496,10 +1496,12 @@ public class WindowOrganizerTests extends WindowTestsBase { firstRootTask secondRootTask */ - final TaskDisplayArea firstTaskDisplayArea = mDisplayContent.getDefaultTaskDisplayArea(); - final TaskDisplayArea secondTaskDisplayArea = createTaskDisplayArea( - mDisplayContent, mRootWindowContainer.mWmService, "TestTaskDisplayArea", + final TaskDisplayArea firstTaskDisplayArea = createTaskDisplayArea( + mDisplayContent, mRootWindowContainer.mWmService, "FirstTaskDisplayArea", FEATURE_VENDOR_FIRST); + final TaskDisplayArea secondTaskDisplayArea = createTaskDisplayArea( + mDisplayContent, mRootWindowContainer.mWmService, "SecondTaskDisplayArea", + FEATURE_VENDOR_FIRST + 1); final Task firstRootTask = firstTaskDisplayArea.createRootTask( WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, false /* onTop */); final Task secondRootTask = secondTaskDisplayArea.createRootTask( @@ -1508,9 +1510,6 @@ public class WindowOrganizerTests extends WindowTestsBase { .setTask(firstRootTask).build(); final ActivityRecord secondActivity = new ActivityBuilder(mAtm) .setTask(secondRootTask).build(); - // This assertion is just a defense to ensure that firstRootTask is not the top most - // by default - assertThat(mDisplayContent.getTopRootTask()).isEqualTo(secondRootTask); WindowContainerTransaction wct = new WindowContainerTransaction(); // Reorder to top @@ -1533,6 +1532,67 @@ public class WindowOrganizerTests extends WindowTestsBase { } @Test + public void testReorderDisplayArea() { + /* + defaultTda + ____|______ + | | + firstTda secondTda + | | + firstRootTask secondRootTask + + */ + final TaskDisplayArea firstTaskDisplayArea = createTaskDisplayArea( + mDisplayContent, mRootWindowContainer.mWmService, "FirstTaskDisplayArea", + FEATURE_VENDOR_FIRST); + final TaskDisplayArea secondTaskDisplayArea = createTaskDisplayArea( + mDisplayContent, mRootWindowContainer.mWmService, "SecondTaskDisplayArea", + FEATURE_VENDOR_FIRST + 1); + final Task firstRootTask = firstTaskDisplayArea.createRootTask( + WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, false /* onTop */); + final Task secondRootTask = secondTaskDisplayArea.createRootTask( + WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, false /* onTop */); + final ActivityRecord firstActivity = new ActivityBuilder(mAtm) + .setTask(firstRootTask).build(); + final ActivityRecord secondActivity = new ActivityBuilder(mAtm) + .setTask(secondRootTask).build(); + + WindowContainerTransaction wct = new WindowContainerTransaction(); + + // Reorder to top + wct.reorder(firstTaskDisplayArea.mRemoteToken.toWindowContainerToken(), true /* onTop */, + true /* includingParents */); + mWm.mAtmService.mWindowOrganizerController.applyTransaction(wct); + assertThat(mDisplayContent.getTopRootTask()).isEqualTo(firstRootTask); + + // Reorder to bottom + wct.reorder(firstTaskDisplayArea.mRemoteToken.toWindowContainerToken(), false /* onTop */, + true /* includingParents */); + mWm.mAtmService.mWindowOrganizerController.applyTransaction(wct); + assertThat(mDisplayContent.getBottomMostTask()).isEqualTo(firstRootTask); + } + + @Test + public void testReparentDisplayAreaUnsupported() { + final TaskDisplayArea firstTaskDisplayArea = createTaskDisplayArea( + mDisplayContent, mRootWindowContainer.mWmService, "FirstTaskDisplayArea", + FEATURE_VENDOR_FIRST); + final TaskDisplayArea secondTaskDisplayArea = createTaskDisplayArea( + mDisplayContent, mRootWindowContainer.mWmService, "SecondTaskDisplayArea", + FEATURE_VENDOR_FIRST + 1); + + WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.reparent(firstTaskDisplayArea.mRemoteToken.toWindowContainerToken(), + secondTaskDisplayArea.mRemoteToken.toWindowContainerToken(), + true /* onTop */ + ); + + assertThrows(UnsupportedOperationException.class, () -> + mWm.mAtmService.mWindowOrganizerController.applyTransaction(wct) + ); + } + + @Test public void testAppearDeferThenVanish() { final ITaskOrganizer organizer = registerMockOrganizer(); final Task rootTask = createRootTask(); diff --git a/telephony/java/android/service/euicc/EuiccService.java b/telephony/java/android/service/euicc/EuiccService.java index 55245419c570..a01a72003570 100644 --- a/telephony/java/android/service/euicc/EuiccService.java +++ b/telephony/java/android/service/euicc/EuiccService.java @@ -267,6 +267,17 @@ public abstract class EuiccService extends Service { "android.service.euicc.extra.RESOLUTION_CONFIRMATION_CODE_RETRIED"; /** + * Bundle key for the {@code resolvedBundle} passed to {@link #onDownloadSubscription( + * int, int, DownloadableSubscription, boolean, boolean, Bundle)}. The value is a + * {@link String} for the package name of the app calling the + * {@link EuiccManager#downloadSubscription(int, DownloadableSubscription, PendingIntent)} API. + * This is to be used by LPA to determine the app that is requesting the download. + * + * @hide + */ + public static final String EXTRA_PACKAGE_NAME = "android.service.euicc.extra.PACKAGE_NAME"; + + /** * Intent extra set for resolution requests containing an int indicating the current card Id. */ public static final String EXTRA_RESOLUTION_CARD_ID = diff --git a/telephony/java/android/telephony/PcoData.java b/telephony/java/android/telephony/PcoData.java index 39e4f2f799d8..3cc32c657fd7 100644 --- a/telephony/java/android/telephony/PcoData.java +++ b/telephony/java/android/telephony/PcoData.java @@ -19,6 +19,8 @@ package android.telephony; import android.os.Parcel; import android.os.Parcelable; +import com.android.internal.telephony.uicc.IccUtils; + import java.util.Arrays; import java.util.Objects; @@ -84,8 +86,8 @@ public class PcoData implements Parcelable { @Override public String toString() { - return "PcoData(" + cid + ", " + bearerProto + ", " + pcoId + ", contents[" + - contents.length + "])"; + return "PcoData(" + cid + ", " + bearerProto + ", " + pcoId + " " + + IccUtils.bytesToHexString(contents) + ")"; } @Override diff --git a/telephony/java/android/telephony/ServiceState.java b/telephony/java/android/telephony/ServiceState.java index db167c0592df..127bbff01575 100644 --- a/telephony/java/android/telephony/ServiceState.java +++ b/telephony/java/android/telephony/ServiceState.java @@ -1211,6 +1211,8 @@ public class ServiceState implements Parcelable { .append(", mIsDataRoamingFromRegistration=") .append(mIsDataRoamingFromRegistration) .append(", mIsIwlanPreferred=").append(mIsIwlanPreferred) + .append(", mIsUsingNonTerrestrialNetwork=") + .append(isUsingNonTerrestrialNetwork()) .append("}").toString(); } } 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/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java index 0bd92705d32f..e657d7faad15 100644 --- a/telephony/java/android/telephony/satellite/SatelliteManager.java +++ b/telephony/java/android/telephony/satellite/SatelliteManager.java @@ -254,7 +254,6 @@ public final class SatelliteManager { */ public static final String KEY_PROVISION_SATELLITE_TOKENS = "provision_satellite"; - /** * The request was successfully processed. */ @@ -412,6 +411,14 @@ public final class SatelliteManager { @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_RESULT_LOCATION_NOT_AVAILABLE = 26; + /** + * Emergency call is in progress. + * + * @hide + */ + @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) + public static final int SATELLITE_RESULT_EMERGENCY_CALL_IN_PROGRESS = 27; + /** @hide */ @IntDef(prefix = {"SATELLITE_RESULT_"}, value = { SATELLITE_RESULT_SUCCESS, @@ -440,7 +447,8 @@ public final class SatelliteManager { SATELLITE_RESULT_ILLEGAL_STATE, SATELLITE_RESULT_MODEM_TIMEOUT, SATELLITE_RESULT_LOCATION_DISABLED, - SATELLITE_RESULT_LOCATION_NOT_AVAILABLE + SATELLITE_RESULT_LOCATION_NOT_AVAILABLE, + SATELLITE_RESULT_EMERGENCY_CALL_IN_PROGRESS }) @Retention(RetentionPolicy.SOURCE) public @interface SatelliteResult {} @@ -2634,7 +2642,7 @@ public final class SatelliteManager { @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN) public void requestProvisionSubscriberIds(@NonNull @CallbackExecutor Executor executor, - @NonNull OutcomeReceiver<List<ProvisionSubscriberId>, SatelliteException> callback) { + @NonNull OutcomeReceiver<List<SatelliteSubscriberInfo>, SatelliteException> callback) { Objects.requireNonNull(executor); Objects.requireNonNull(callback); @@ -2646,10 +2654,10 @@ public final class SatelliteManager { protected void onReceiveResult(int resultCode, Bundle resultData) { if (resultCode == SATELLITE_RESULT_SUCCESS) { if (resultData.containsKey(KEY_REQUEST_PROVISION_SUBSCRIBER_ID_TOKEN)) { - List<ProvisionSubscriberId> list = + List<SatelliteSubscriberInfo> list = resultData.getParcelableArrayList( KEY_REQUEST_PROVISION_SUBSCRIBER_ID_TOKEN, - ProvisionSubscriberId.class); + SatelliteSubscriberInfo.class); executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onResult(list))); } else { @@ -2734,9 +2742,9 @@ public final class SatelliteManager { } /** - * Deliver the list of provisioned satellite subscriber ids. + * Deliver the list of provisioned satellite subscriber infos. * - * @param list List of ProvisionSubscriberId. + * @param list The list of provisioned satellite subscriber infos. * @param executor The executor on which the callback will be called. * @param callback The callback object to which the result will be delivered. * @@ -2745,7 +2753,7 @@ public final class SatelliteManager { */ @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN) - public void provisionSatellite(@NonNull List<ProvisionSubscriberId> list, + public void provisionSatellite(@NonNull List<SatelliteSubscriberInfo> list, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver<Boolean, SatelliteException> callback) { Objects.requireNonNull(executor); diff --git a/telephony/java/android/telephony/satellite/ProvisionSubscriberId.aidl b/telephony/java/android/telephony/satellite/SatelliteSubscriberInfo.aidl index fe46db878909..992c9aeefb40 100644 --- a/telephony/java/android/telephony/satellite/ProvisionSubscriberId.aidl +++ b/telephony/java/android/telephony/satellite/SatelliteSubscriberInfo.aidl @@ -16,4 +16,4 @@ package android.telephony.satellite; -parcelable ProvisionSubscriberId; +parcelable SatelliteSubscriberInfo; diff --git a/telephony/java/android/telephony/satellite/ProvisionSubscriberId.java b/telephony/java/android/telephony/satellite/SatelliteSubscriberInfo.java index 3e6f743e8a93..f26219bd0885 100644 --- a/telephony/java/android/telephony/satellite/ProvisionSubscriberId.java +++ b/telephony/java/android/telephony/satellite/SatelliteSubscriberInfo.java @@ -26,7 +26,7 @@ import com.android.internal.telephony.flags.Flags; import java.util.Objects; /** - * ProvisionSubscriberId + * SatelliteSubscriberInfo * * Satellite Gateway client will use these subscriber ids to register with satellite gateway service * which identify user subscription with unique subscriber ids. These subscriber ids can be any @@ -35,7 +35,7 @@ import java.util.Objects; * @hide */ @FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN) -public final class ProvisionSubscriberId implements Parcelable { +public final class SatelliteSubscriberInfo implements Parcelable { /** provision subscriberId */ @NonNull private String mSubscriberId; @@ -49,14 +49,14 @@ public final class ProvisionSubscriberId implements Parcelable { /** * @hide */ - public ProvisionSubscriberId(@NonNull String subscriberId, @NonNull int carrierId, + public SatelliteSubscriberInfo(@NonNull String subscriberId, @NonNull int carrierId, @NonNull String niddApn) { this.mCarrierId = carrierId; this.mSubscriberId = subscriberId; this.mNiddApn = niddApn; } - private ProvisionSubscriberId(Parcel in) { + private SatelliteSubscriberInfo(Parcel in) { readFromParcel(in); } @@ -72,16 +72,16 @@ public final class ProvisionSubscriberId implements Parcelable { } @FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN) - public static final @android.annotation.NonNull Creator<ProvisionSubscriberId> CREATOR = - new Creator<ProvisionSubscriberId>() { + public static final @android.annotation.NonNull Creator<SatelliteSubscriberInfo> CREATOR = + new Creator<SatelliteSubscriberInfo>() { @Override - public ProvisionSubscriberId createFromParcel(Parcel in) { - return new ProvisionSubscriberId(in); + public SatelliteSubscriberInfo createFromParcel(Parcel in) { + return new SatelliteSubscriberInfo(in); } @Override - public ProvisionSubscriberId[] newArray(int size) { - return new ProvisionSubscriberId[size]; + public SatelliteSubscriberInfo[] newArray(int size) { + return new SatelliteSubscriberInfo[size]; } }; @@ -148,7 +148,7 @@ public final class ProvisionSubscriberId implements Parcelable { public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - ProvisionSubscriberId that = (ProvisionSubscriberId) o; + SatelliteSubscriberInfo that = (SatelliteSubscriberInfo) o; return mSubscriberId.equals(that.mSubscriberId) && mCarrierId == that.mCarrierId && mNiddApn.equals(that.mNiddApn); } 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/android/telephony/satellite/stub/ProvisionSubscriberId.aidl b/telephony/java/android/telephony/satellite/stub/SatelliteSubscriberInfo.aidl index 460de8c8113d..fb44f87ee1ee 100644 --- a/telephony/java/android/telephony/satellite/stub/ProvisionSubscriberId.aidl +++ b/telephony/java/android/telephony/satellite/stub/SatelliteSubscriberInfo.aidl @@ -19,7 +19,7 @@ package android.telephony.satellite.stub; /** * {@hide} */ -parcelable ProvisionSubscriberId { +parcelable SatelliteSubscriberInfo { /** provision subscriberId */ String subscriberId; diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl index 3dbda7ae10a3..89197032dcef 100644 --- a/telephony/java/com/android/internal/telephony/ITelephony.aidl +++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl @@ -78,7 +78,7 @@ import android.telephony.satellite.ISatelliteModemStateCallback; import android.telephony.satellite.NtnSignalStrength; import android.telephony.satellite.SatelliteCapabilities; import android.telephony.satellite.SatelliteDatagram; -import android.telephony.satellite.ProvisionSubscriberId; +import android.telephony.satellite.SatelliteSubscriberInfo; import com.android.ims.internal.IImsServiceFeatureCallback; import com.android.internal.telephony.CellNetworkScanResult; import com.android.internal.telephony.IBooleanConsumer; @@ -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. @@ -3426,13 +3428,13 @@ interface ITelephony { void requestIsProvisioned(in String satelliteSubscriberId, in ResultReceiver result); /** - * Deliver the list of provisioned satellite subscriber ids. + * Deliver the list of provisioned satellite subscriber infos. * - * @param list List of provisioned satellite subscriber ids. + * @param list The list of provisioned satellite subscriber infos. * @param result The result receiver that returns whether deliver success or fail. * @hide */ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(" + "android.Manifest.permission.SATELLITE_COMMUNICATION)") - void provisionSatellite(in List<ProvisionSubscriberId> list, in ResultReceiver result); + void provisionSatellite(in List<SatelliteSubscriberInfo> list, in ResultReceiver result); } diff --git a/test-mock/Android.bp b/test-mock/Android.bp index 59766579eee2..71f303311047 100644 --- a/test-mock/Android.bp +++ b/test-mock/Android.bp @@ -47,6 +47,10 @@ java_sdk_library { compile_dex: true, default_to_stubs: true, dist_group: "android", + + // This module cannot generate stubs from the api signature files as stubs depends on the + // private APIs, which are not visible in the api signature files. + build_from_text_stub: false, } java_library { 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/Android.bp b/tests/Internal/Android.bp index 827ff4fbd989..ad98e47fa8f0 100644 --- a/tests/Internal/Android.bp +++ b/tests/Internal/Android.bp @@ -24,6 +24,7 @@ android_test { "flickerlib-parsers", "perfetto_trace_java_protos", "flickerlib-trace_processor_shell", + "ravenwood-junit", ], java_resource_dirs: ["res"], certificate: "platform", @@ -39,6 +40,7 @@ android_ravenwood_test { "platform-test-annotations", ], srcs: [ + "src/com/android/internal/graphics/ColorUtilsTest.java", "src/com/android/internal/util/ParcellingTests.java", ], auto_gen_config: true, diff --git a/tests/Internal/src/com/android/internal/graphics/ColorUtilsTest.java b/tests/Internal/src/com/android/internal/graphics/ColorUtilsTest.java index d0bb8e3745bc..38a22f2fc2f3 100644 --- a/tests/Internal/src/com/android/internal/graphics/ColorUtilsTest.java +++ b/tests/Internal/src/com/android/internal/graphics/ColorUtilsTest.java @@ -19,14 +19,19 @@ package com.android.internal.graphics; import static org.junit.Assert.assertTrue; import android.graphics.Color; +import android.platform.test.ravenwood.RavenwoodRule; import androidx.test.filters.SmallTest; +import org.junit.Rule; import org.junit.Test; @SmallTest public class ColorUtilsTest { + @Rule + public final RavenwoodRule mRavenwood = new RavenwoodRule(); + @Test public void calculateMinimumBackgroundAlpha_satisfiestContrast() { 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))); + } +} diff --git a/tests/Internal/src/com/android/internal/util/ParcellingTests.java b/tests/Internal/src/com/android/internal/util/ParcellingTests.java index 65a3436a4c5e..fb63422cdf9f 100644 --- a/tests/Internal/src/com/android/internal/util/ParcellingTests.java +++ b/tests/Internal/src/com/android/internal/util/ParcellingTests.java @@ -18,6 +18,7 @@ package com.android.internal.util; import android.os.Parcel; import android.platform.test.annotations.Presubmit; +import android.platform.test.ravenwood.RavenwoodRule; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; @@ -26,6 +27,7 @@ import androidx.test.filters.SmallTest; import com.android.internal.util.Parcelling.BuiltIn.ForInstant; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -38,6 +40,9 @@ import java.time.Instant; @RunWith(JUnit4.class) public class ParcellingTests { + @Rule + public final RavenwoodRule mRavenwood = new RavenwoodRule(); + private Parcel mParcel = Parcel.obtain(); @Test diff --git a/tools/aapt2/Android.bp b/tools/aapt2/Android.bp index 3f9016ba4852..f43cf521edf5 100644 --- a/tools/aapt2/Android.bp +++ b/tools/aapt2/Android.bp @@ -113,6 +113,7 @@ cc_library_host_static { "io/ZipArchive.cpp", "link/AutoVersioner.cpp", "link/FeatureFlagsFilter.cpp", + "link/FlagDisabledResourceRemover.cpp", "link/ManifestFixer.cpp", "link/NoDefaultResourceRemover.cpp", "link/PrivateAttributeMover.cpp", @@ -189,6 +190,8 @@ cc_test_host { "integration-tests/CommandTests/**/*", "integration-tests/ConvertTest/**/*", "integration-tests/DumpTest/**/*", + ":resource-flagging-test-app-apk", + ":resource-flagging-test-app-r-java", ], } diff --git a/tools/aapt2/ResourceParser.cpp b/tools/aapt2/ResourceParser.cpp index 9444dd968f5f..1c85e9ff231b 100644 --- a/tools/aapt2/ResourceParser.cpp +++ b/tools/aapt2/ResourceParser.cpp @@ -690,9 +690,7 @@ bool ResourceParser::ParseResource(xml::XmlPullParser* parser, resource_format = item_iter->second.format; } - // Don't bother parsing the item if it is behind a disabled flag - if (out_resource->flag_status != FlagStatus::Disabled && - !ParseItem(parser, out_resource, resource_format)) { + if (!ParseItem(parser, out_resource, resource_format)) { return false; } return true; diff --git a/tools/aapt2/ResourceParser_test.cpp b/tools/aapt2/ResourceParser_test.cpp index 2e6ad13d99de..b59b16574c42 100644 --- a/tools/aapt2/ResourceParser_test.cpp +++ b/tools/aapt2/ResourceParser_test.cpp @@ -69,13 +69,8 @@ class ResourceParserTest : public ::testing::Test { return TestParse(str, ConfigDescription{}); } - ::testing::AssertionResult TestParse(StringPiece str, ResourceParserOptions parserOptions) { - return TestParse(str, ConfigDescription{}, parserOptions); - } - - ::testing::AssertionResult TestParse( - StringPiece str, const ConfigDescription& config, - ResourceParserOptions parserOptions = ResourceParserOptions()) { + ::testing::AssertionResult TestParse(StringPiece str, const ConfigDescription& config) { + ResourceParserOptions parserOptions; ResourceParser parser(context_->GetDiagnostics(), &table_, android::Source{"test"}, config, parserOptions); @@ -247,19 +242,6 @@ TEST_F(ResourceParserTest, ParseStringTranslatableAttribute) { EXPECT_FALSE(TestParse(R"(<string name="foo4" translatable="yes">Translate</string>)")); } -TEST_F(ResourceParserTest, ParseStringBehindDisabledFlag) { - FeatureFlagProperties flag_properties(true, false); - ResourceParserOptions options; - options.feature_flag_values = {{"falseFlag", flag_properties}}; - ASSERT_TRUE(TestParse( - R"(<string name="foo" android:featureFlag="falseFlag" - xmlns:android="http://schemas.android.com/apk/res/android">foo</string>)", - options)); - - String* str = test::GetValue<String>(&table_, "string/foo"); - ASSERT_THAT(str, IsNull()); -} - TEST_F(ResourceParserTest, IgnoreXliffTagsOtherThanG) { std::string input = R"( <string name="foo" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> diff --git a/tools/aapt2/cmd/Convert.cpp b/tools/aapt2/cmd/Convert.cpp index c132792d374b..6c3eae11eab9 100644 --- a/tools/aapt2/cmd/Convert.cpp +++ b/tools/aapt2/cmd/Convert.cpp @@ -244,6 +244,10 @@ class Context : public IAaptContext { return verbose_; } + void SetVerbose(bool verbose) { + verbose_ = verbose; + } + int GetMinSdkVersion() override { return min_sdk_; } @@ -388,6 +392,8 @@ int ConvertCommand::Action(const std::vector<std::string>& args) { } Context context; + context.SetVerbose(verbose_); + StringPiece path = args[0]; unique_ptr<LoadedApk> apk = LoadedApk::LoadApkFromPath(path, context.GetDiagnostics()); if (apk == nullptr) { diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp index 642a5618b6ad..56f52885b36d 100644 --- a/tools/aapt2/cmd/Link.cpp +++ b/tools/aapt2/cmd/Link.cpp @@ -57,6 +57,7 @@ #include "java/ManifestClassGenerator.h" #include "java/ProguardRules.h" #include "link/FeatureFlagsFilter.h" +#include "link/FlagDisabledResourceRemover.h" #include "link/Linkers.h" #include "link/ManifestFixer.h" #include "link/NoDefaultResourceRemover.h" @@ -1840,11 +1841,57 @@ class Linker { return validate(attr->value); } + class FlagDisabledStringVisitor : public DescendingValueVisitor { + public: + using DescendingValueVisitor::Visit; + + explicit FlagDisabledStringVisitor(android::StringPool& string_pool) + : string_pool_(string_pool) { + } + + void Visit(RawString* value) override { + value->value = string_pool_.MakeRef(""); + } + + void Visit(String* value) override { + value->value = string_pool_.MakeRef(""); + } + + void Visit(StyledString* value) override { + value->value = string_pool_.MakeRef(android::StyleString{{""}, {}}); + } + + private: + DISALLOW_COPY_AND_ASSIGN(FlagDisabledStringVisitor); + android::StringPool& string_pool_; + }; + // Writes the AndroidManifest, ResourceTable, and all XML files referenced by the ResourceTable // to the IArchiveWriter. bool WriteApk(IArchiveWriter* writer, proguard::KeepSet* keep_set, xml::XmlResource* manifest, ResourceTable* table) { TRACE_CALL(); + + FlagDisabledStringVisitor visitor(table->string_pool); + + for (auto& package : table->packages) { + for (auto& type : package->types) { + for (auto& entry : type->entries) { + for (auto& config_value : entry->values) { + if (config_value->flag_status == FlagStatus::Disabled) { + config_value->value->Accept(&visitor); + } + } + } + } + } + + if (!FlagDisabledResourceRemover{}.Consume(context_, table)) { + context_->GetDiagnostics()->Error(android::DiagMessage() + << "failed removing resources behind disabled flags"); + return 1; + } + const bool keep_raw_values = (context_->GetPackageType() == PackageType::kStaticLib) || options_.keep_raw_values; bool result = FlattenXml(context_, *manifest, kAndroidManifestPath, keep_raw_values, @@ -2331,6 +2378,12 @@ class Linker { return 1; }; + if (options_.generate_java_class_path || options_.generate_text_symbols_path) { + if (!GenerateJavaClasses()) { + return 1; + } + } + if (!WriteApk(archive_writer.get(), &proguard_keep_set, manifest_xml.get(), &final_table_)) { return 1; } @@ -2339,12 +2392,6 @@ class Linker { return 1; } - if (options_.generate_java_class_path || options_.generate_text_symbols_path) { - if (!GenerateJavaClasses()) { - return 1; - } - } - if (!WriteProguardFile(options_.generate_proguard_rules_path, proguard_keep_set)) { return 1; } diff --git a/tools/aapt2/integration-tests/FlaggedResourcesTest/Android.bp b/tools/aapt2/integration-tests/FlaggedResourcesTest/Android.bp new file mode 100644 index 000000000000..5932271d4d28 --- /dev/null +++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/Android.bp @@ -0,0 +1,81 @@ +// 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 { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], + default_team: "trendy_team_android_resources", +} + +genrule { + name: "resource-flagging-test-app-compile", + tools: ["aapt2"], + srcs: [ + "res/values/bools.xml", + "res/values/bools2.xml", + "res/values/strings.xml", + ], + out: [ + "values_bools.arsc.flat", + "values_bools2.arsc.flat", + "values_strings.arsc.flat", + ], + cmd: "$(location aapt2) compile $(in) -o $(genDir) " + + "--feature-flags test.package.falseFlag:ro=false,test.package.trueFlag:ro=true", +} + +genrule { + name: "resource-flagging-test-app-apk", + tools: ["aapt2"], + // The first input file in the list must be the manifest + srcs: [ + "AndroidManifest.xml", + ":resource-flagging-test-app-compile", + ], + out: [ + "resapp.apk", + ], + cmd: "$(location aapt2) link -o $(out) --manifest $(in)", +} + +genrule { + name: "resource-flagging-test-app-r-java", + tools: ["aapt2"], + // The first input file in the list must be the manifest + srcs: [ + "AndroidManifest.xml", + ":resource-flagging-test-app-compile", + ], + out: [ + "resource-flagging-java/com/android/intenal/flaggedresources/R.java", + ], + cmd: "$(location aapt2) link -o $(genDir)/resapp.apk --java $(genDir)/resource-flagging-java --manifest $(in)", +} + +java_genrule { + name: "resource-flagging-test-app-apk-as-resource", + srcs: [ + ":resource-flagging-test-app-apk", + ], + out: ["apks_as_resources.res.zip"], + tools: ["soong_zip"], + + cmd: "mkdir -p $(genDir)/res/raw && " + + "cp $(in) $(genDir)/res/raw/$$(basename $(in)) && " + + "$(location soong_zip) -o $(out) -C $(genDir)/res -D $(genDir)/res", +} diff --git a/core/tests/resourceflaggingtests/TestAppAndroidManifest.xml b/tools/aapt2/integration-tests/FlaggedResourcesTest/AndroidManifest.xml index d6cdeb7b5231..d6cdeb7b5231 100644 --- a/core/tests/resourceflaggingtests/TestAppAndroidManifest.xml +++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/AndroidManifest.xml diff --git a/core/tests/resourceflaggingtests/flagged_resources_res/values/bools.xml b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/bools.xml index 8d0146511d1d..3e094fbd669c 100644 --- a/core/tests/resourceflaggingtests/flagged_resources_res/values/bools.xml +++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/bools.xml @@ -7,4 +7,6 @@ <bool name="res2" android:featureFlag="test.package.trueFlag">true</bool> <bool name="res3">false</bool> + + <bool name="res4" android:featureFlag="test.package.falseFlag">true</bool> </resources>
\ No newline at end of file diff --git a/core/tests/resourceflaggingtests/flagged_resources_res/values/bools2.xml b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/bools2.xml index e7563aa0fbdd..e7563aa0fbdd 100644 --- a/core/tests/resourceflaggingtests/flagged_resources_res/values/bools2.xml +++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/bools2.xml diff --git a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/strings.xml b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/strings.xml new file mode 100644 index 000000000000..5c0fca16fe39 --- /dev/null +++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/strings.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:android="http://schemas.android.com/apk/res/android"> + <string name="str">plain string</string> + + <string name="str1" android:featureFlag="test.package.falseFlag">DONTFIND</string> +</resources>
\ No newline at end of file diff --git a/tools/aapt2/link/FlagDisabledResourceRemover.cpp b/tools/aapt2/link/FlagDisabledResourceRemover.cpp new file mode 100644 index 000000000000..e3289e2a173a --- /dev/null +++ b/tools/aapt2/link/FlagDisabledResourceRemover.cpp @@ -0,0 +1,59 @@ +/* + * 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. + */ + +#include "link/FlagDisabledResourceRemover.h" + +#include <algorithm> + +#include "ResourceTable.h" + +using android::ConfigDescription; + +namespace aapt { + +static bool KeepResourceEntry(const std::unique_ptr<ResourceEntry>& entry) { + if (entry->values.empty()) { + return true; + } + const auto end_iter = entry->values.end(); + const auto remove_iter = + std::stable_partition(entry->values.begin(), end_iter, + [](const std::unique_ptr<ResourceConfigValue>& value) -> bool { + return value->flag_status != FlagStatus::Disabled; + }); + + bool keep = remove_iter != entry->values.begin(); + + entry->values.erase(remove_iter, end_iter); + return keep; +} + +bool FlagDisabledResourceRemover::Consume(IAaptContext* context, ResourceTable* table) { + for (auto& pkg : table->packages) { + for (auto& type : pkg->types) { + const auto end_iter = type->entries.end(); + const auto remove_iter = std::stable_partition( + type->entries.begin(), end_iter, [](const std::unique_ptr<ResourceEntry>& entry) -> bool { + return KeepResourceEntry(entry); + }); + + type->entries.erase(remove_iter, end_iter); + } + } + return true; +} + +} // namespace aapt
\ No newline at end of file diff --git a/tools/aapt2/link/FlagDisabledResourceRemover.h b/tools/aapt2/link/FlagDisabledResourceRemover.h new file mode 100644 index 000000000000..2db2cb44c4cc --- /dev/null +++ b/tools/aapt2/link/FlagDisabledResourceRemover.h @@ -0,0 +1,35 @@ +/* + * 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. + */ + +#pragma once + +#include "android-base/macros.h" +#include "process/IResourceTableConsumer.h" + +namespace aapt { + +// Removes any resource that are behind disabled flags. +class FlagDisabledResourceRemover : public IResourceTableConsumer { + public: + FlagDisabledResourceRemover() = default; + + bool Consume(IAaptContext* context, ResourceTable* table) override; + + private: + DISALLOW_COPY_AND_ASSIGN(FlagDisabledResourceRemover); +}; + +} // namespace aapt diff --git a/tools/aapt2/link/FlaggedResources_test.cpp b/tools/aapt2/link/FlaggedResources_test.cpp new file mode 100644 index 000000000000..c901b5866279 --- /dev/null +++ b/tools/aapt2/link/FlaggedResources_test.cpp @@ -0,0 +1,101 @@ +/* + * 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. + */ + +#include "LoadedApk.h" +#include "cmd/Dump.h" +#include "io/StringStream.h" +#include "test/Test.h" +#include "text/Printer.h" + +using ::aapt::io::StringOutputStream; +using ::aapt::text::Printer; +using testing::Eq; +using testing::Ne; + +namespace aapt { + +using FlaggedResourcesTest = CommandTestFixture; + +static android::NoOpDiagnostics noop_diag; + +void DumpStringPoolToString(LoadedApk* loaded_apk, std::string* output) { + StringOutputStream output_stream(output); + Printer printer(&output_stream); + + DumpStringsCommand command(&printer, &noop_diag); + ASSERT_EQ(command.Dump(loaded_apk), 0); + output_stream.Flush(); +} + +void DumpResourceTableToString(LoadedApk* loaded_apk, std::string* output) { + StringOutputStream output_stream(output); + Printer printer(&output_stream); + + DumpTableCommand command(&printer, &noop_diag); + ASSERT_EQ(command.Dump(loaded_apk), 0); + output_stream.Flush(); +} + +void DumpChunksToString(LoadedApk* loaded_apk, std::string* output) { + StringOutputStream output_stream(output); + Printer printer(&output_stream); + + DumpChunks command(&printer, &noop_diag); + ASSERT_EQ(command.Dump(loaded_apk), 0); + output_stream.Flush(); +} + +TEST_F(FlaggedResourcesTest, DisabledStringRemovedFromPool) { + auto apk_path = file::BuildPath({android::base::GetExecutableDirectory(), "resapp.apk"}); + auto loaded_apk = LoadedApk::LoadApkFromPath(apk_path, &noop_diag); + + std::string output; + DumpStringPoolToString(loaded_apk.get(), &output); + + std::string excluded = "DONTFIND"; + ASSERT_EQ(output.find(excluded), std::string::npos); +} + +TEST_F(FlaggedResourcesTest, DisabledResourcesRemovedFromTable) { + auto apk_path = file::BuildPath({android::base::GetExecutableDirectory(), "resapp.apk"}); + auto loaded_apk = LoadedApk::LoadApkFromPath(apk_path, &noop_diag); + + std::string output; + DumpResourceTableToString(loaded_apk.get(), &output); +} + +TEST_F(FlaggedResourcesTest, DisabledResourcesRemovedFromTableChunks) { + auto apk_path = file::BuildPath({android::base::GetExecutableDirectory(), "resapp.apk"}); + auto loaded_apk = LoadedApk::LoadApkFromPath(apk_path, &noop_diag); + + std::string output; + DumpChunksToString(loaded_apk.get(), &output); + + ASSERT_EQ(output.find("res4"), std::string::npos); + ASSERT_EQ(output.find("str1"), std::string::npos); +} + +TEST_F(FlaggedResourcesTest, DisabledResourcesInRJava) { + auto r_path = file::BuildPath({android::base::GetExecutableDirectory(), "resource-flagging-java", + "com", "android", "intenal", "flaggedresources", "R.java"}); + std::string r_contents; + ::android::base::ReadFileToString(r_path, &r_contents); + + ASSERT_NE(r_contents.find("public static final int res4"), std::string::npos); + ASSERT_NE(r_contents.find("public static final int str1"), std::string::npos); +} + +} // namespace aapt diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt index 5dde265c79fb..36bfbefdb086 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt @@ -58,17 +58,19 @@ class HostStubGen(val options: HostStubGenOptions) { // Dump the classes, if specified. options.inputJarDumpFile.ifSet { - PrintWriter(it).use { pw -> allClasses.dump(pw) } - log.i("Dump file created at $it") + log.iTime("Dump file created at $it") { + PrintWriter(it).use { pw -> allClasses.dump(pw) } + } } options.inputJarAsKeepAllFile.ifSet { - PrintWriter(it).use { - pw -> allClasses.forEach { - classNode -> printAsTextPolicy(pw, classNode) + log.iTime("Dump file created at $it") { + PrintWriter(it).use { pw -> + allClasses.forEach { classNode -> + printAsTextPolicy(pw, classNode) + } } } - log.i("Dump file created at $it") } // Build the filters. @@ -91,16 +93,18 @@ class HostStubGen(val options: HostStubGenOptions) { // Dump statistics, if specified. options.statsFile.ifSet { - PrintWriter(it).use { pw -> stats.dumpOverview(pw) } - log.i("Dump file created at $it") + log.iTime("Dump file created at $it") { + PrintWriter(it).use { pw -> stats.dumpOverview(pw) } + } } options.apiListFile.ifSet { - PrintWriter(it).use { pw -> - // TODO, when dumping a jar that's not framework-minus-apex.jar, we need to feed - // framework-minus-apex.jar so that we can dump inherited methods from it. - ApiDumper(pw, allClasses, null, filter).dump() + log.iTime("API list file created at $it") { + PrintWriter(it).use { pw -> + // TODO, when dumping a jar that's not framework-minus-apex.jar, we need to feed + // framework-minus-apex.jar so that we can dump inherited methods from it. + ApiDumper(pw, allClasses, null, filter).dump() + } } - log.i("API list file created at $it") } } @@ -221,47 +225,48 @@ class HostStubGen(val options: HostStubGenOptions) { log.i("Converting %s into [stub: %s, impl: %s] ...", inJar, outStubJar, outImplJar) log.i("ASM CheckClassAdapter is %s", if (enableChecker) "enabled" else "disabled") - val start = System.currentTimeMillis() - - val packageRedirector = PackageRedirectRemapper(options.packageRedirects) + log.iTime("Transforming jar") { + val packageRedirector = PackageRedirectRemapper(options.packageRedirects) - var itemIndex = 0 - var numItemsProcessed = 0 - var numItems = -1 // == Unknown + var itemIndex = 0 + var numItemsProcessed = 0 + var numItems = -1 // == Unknown - log.withIndent { - // Open the input jar file and process each entry. - ZipFile(inJar).use { inZip -> - - numItems = inZip.size() - val shardStart = numItems * shard / numShards - val shardNextStart = numItems * (shard + 1) / numShards - - maybeWithZipOutputStream(outStubJar) { stubOutStream -> - maybeWithZipOutputStream(outImplJar) { implOutStream -> - val inEntries = inZip.entries() - while (inEntries.hasMoreElements()) { - val entry = inEntries.nextElement() - val inShard = (shardStart <= itemIndex) && (itemIndex < shardNextStart) - itemIndex++ - if (!inShard) { - continue - } - convertSingleEntry(inZip, entry, stubOutStream, implOutStream, + log.withIndent { + // Open the input jar file and process each entry. + ZipFile(inJar).use { inZip -> + + numItems = inZip.size() + val shardStart = numItems * shard / numShards + val shardNextStart = numItems * (shard + 1) / numShards + + maybeWithZipOutputStream(outStubJar) { stubOutStream -> + maybeWithZipOutputStream(outImplJar) { implOutStream -> + val inEntries = inZip.entries() + while (inEntries.hasMoreElements()) { + val entry = inEntries.nextElement() + val inShard = (shardStart <= itemIndex) + && (itemIndex < shardNextStart) + itemIndex++ + if (!inShard) { + continue + } + convertSingleEntry( + inZip, entry, stubOutStream, implOutStream, filter, packageRedirector, remapper, - enableChecker, classes, errors, stats) - numItemsProcessed++ + enableChecker, classes, errors, stats + ) + numItemsProcessed++ + } + log.i("Converted all entries.") } - log.i("Converted all entries.") } + outStubJar?.let { log.i("Created stub: $it") } + outImplJar?.let { log.i("Created impl: $it") } } - outStubJar?.let { log.i("Created stub: $it") } - outImplJar?.let { log.i("Created impl: $it") } } + log.i("%d / %d item(s) processed.", numItemsProcessed, numItems) } - val end = System.currentTimeMillis() - log.i("Done transforming the jar in %.1f second(s). %d / %d item(s) processed.", - (end - start) / 1000.0, numItemsProcessed, numItems) } private fun <T> maybeWithZipOutputStream(filename: String?, block: (ZipOutputStream?) -> T): T { diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenLogger.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenLogger.kt index 18065ba56c52..ee4a06fb983d 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenLogger.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenLogger.kt @@ -185,6 +185,16 @@ class HostStubGenLogger { println(LogLevel.Debug, format, *args) } + inline fun <T> iTime(message: String, block: () -> T): T { + val start = System.currentTimeMillis() + val ret = block() + val end = System.currentTimeMillis() + + log.i("%s: took %.1f second(s).", message, (end - start) / 1000.0) + + return ret + } + inline fun forVerbose(block: () -> Unit) { if (isEnabled(LogLevel.Verbose)) { block() diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/ClassNodes.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/ClassNodes.kt index 92906a75b93a..2607df63f146 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/ClassNodes.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/ClassNodes.kt @@ -184,49 +184,50 @@ class ClassNodes { * Load all the classes, without code. */ fun loadClassStructures(inJar: String): ClassNodes { - log.i("Reading class structure from $inJar ...") - val start = System.currentTimeMillis() - - val allClasses = ClassNodes() - - log.withIndent { - ZipFile(inJar).use { inZip -> - val inEntries = inZip.entries() - - while (inEntries.hasMoreElements()) { - val entry = inEntries.nextElement() - - BufferedInputStream(inZip.getInputStream(entry)).use { bis -> - if (entry.name.endsWith(".class")) { - val cr = ClassReader(bis) - val cn = ClassNode() - cr.accept(cn, ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG - or ClassReader.SKIP_FRAMES) - if (!allClasses.addClass(cn)) { - log.w("Duplicate class found: ${cn.name}") - } - } else if (entry.name.endsWith(".dex")) { - // Seems like it's an ART jar file. We can't process it. - // It's a fatal error. - throw InvalidJarFileException( - "$inJar is not a desktop jar file. It contains a *.dex file.") - } else { - // Unknown file type. Skip. - while (bis.available() > 0) { - bis.skip((1024 * 1024).toLong()) + log.iTime("Reading class structure from $inJar") { + val allClasses = ClassNodes() + + log.withIndent { + ZipFile(inJar).use { inZip -> + val inEntries = inZip.entries() + + while (inEntries.hasMoreElements()) { + val entry = inEntries.nextElement() + + BufferedInputStream(inZip.getInputStream(entry)).use { bis -> + if (entry.name.endsWith(".class")) { + val cr = ClassReader(bis) + val cn = ClassNode() + cr.accept( + cn, ClassReader.SKIP_CODE + or ClassReader.SKIP_DEBUG + or ClassReader.SKIP_FRAMES + ) + if (!allClasses.addClass(cn)) { + log.w("Duplicate class found: ${cn.name}") + } + } else if (entry.name.endsWith(".dex")) { + // Seems like it's an ART jar file. We can't process it. + // It's a fatal error. + throw InvalidJarFileException( + "$inJar is not a desktop jar file." + + " It contains a *.dex file." + ) + } else { + // Unknown file type. Skip. + while (bis.available() > 0) { + bis.skip((1024 * 1024).toLong()) + } } } } } } + if (allClasses.size == 0) { + log.w("$inJar contains no *.class files.") + } + return allClasses } - if (allClasses.size == 0) { - log.w("$inJar contains no *.class files.") - } - - val end = System.currentTimeMillis() - log.i("Done reading class structure in %.1f second(s).", (end - start) / 1000.0) - return allClasses } } }
\ No newline at end of file diff --git a/tools/systemfeatures/Android.bp b/tools/systemfeatures/Android.bp new file mode 100644 index 000000000000..2cebfe9790d0 --- /dev/null +++ b/tools/systemfeatures/Android.bp @@ -0,0 +1,63 @@ +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +java_library_host { + name: "systemfeatures-gen-lib", + srcs: [ + "src/**/*.java", + "src/**/*.kt", + ], + static_libs: [ + "guava", + "javapoet", + ], +} + +java_binary_host { + name: "systemfeatures-gen-tool", + main_class: "com.android.systemfeatures.SystemFeaturesGenerator", + static_libs: ["systemfeatures-gen-lib"], +} + +// TODO(b/203143243): Add golden diff test for generated sources. +// Functional runtime behavior is covered in systemfeatures-gen-tests. +genrule { + name: "systemfeatures-gen-tests-srcs", + cmd: "$(location systemfeatures-gen-tool) com.android.systemfeatures.RwNoFeatures --readonly=false > $(location RwNoFeatures.java) && " + + "$(location systemfeatures-gen-tool) com.android.systemfeatures.RoNoFeatures --readonly=true > $(location RoNoFeatures.java) && " + + "$(location systemfeatures-gen-tool) com.android.systemfeatures.RwFeatures --readonly=false --feature=WATCH:1 --feature=WIFI:0 --feature=VULKAN:-1 --feature=AUTO: > $(location RwFeatures.java) && " + + "$(location systemfeatures-gen-tool) com.android.systemfeatures.RoFeatures --readonly=true --feature=WATCH:1 --feature=WIFI:0 --feature=VULKAN:-1 --feature=AUTO: > $(location RoFeatures.java)", + out: [ + "RwNoFeatures.java", + "RoNoFeatures.java", + "RwFeatures.java", + "RoFeatures.java", + ], + tools: ["systemfeatures-gen-tool"], +} + +java_test_host { + name: "systemfeatures-gen-tests", + test_suites: ["general-tests"], + srcs: [ + "tests/**/*.java", + ":systemfeatures-gen-tests-srcs", + ], + test_options: { + unit_test: true, + }, + static_libs: [ + "aconfig-annotations-lib", + "framework-annotations-lib", + "junit", + "objenesis", + "mockito", + "truth", + ], +} diff --git a/tools/systemfeatures/OWNERS b/tools/systemfeatures/OWNERS new file mode 100644 index 000000000000..66c8506f58be --- /dev/null +++ b/tools/systemfeatures/OWNERS @@ -0,0 +1 @@ +include /PERFORMANCE_OWNERS diff --git a/tools/systemfeatures/README.md b/tools/systemfeatures/README.md new file mode 100644 index 000000000000..5836f81e5fd3 --- /dev/null +++ b/tools/systemfeatures/README.md @@ -0,0 +1,11 @@ +# Build-time system feature support + +## Overview + +System features exposed from `PackageManager` are defined and aggregated as +`<feature>` xml attributes across various partitions, and are currently queried +at runtime through the framework. This directory contains tooling that will +support *build-time* queries of select system features, enabling optimizations +like code stripping and conditionally dependencies when so configured. + +### TODO(b/203143243): Expand readme after landing codegen. diff --git a/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt b/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt new file mode 100644 index 000000000000..9bfda451067f --- /dev/null +++ b/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt @@ -0,0 +1,218 @@ +/* + * 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.systemfeatures + +import com.google.common.base.CaseFormat +import com.squareup.javapoet.ClassName +import com.squareup.javapoet.JavaFile +import com.squareup.javapoet.MethodSpec +import com.squareup.javapoet.TypeSpec +import javax.lang.model.element.Modifier + +/* + * Simple Java code generator that takes as input a list of defined features and generates an + * accessory class based on the provided versions. + * + * <p>Example: + * + * <pre> + * <cmd> com.foo.RoSystemFeatures --readonly=true \ + * --feature=WATCH:0 --feature=AUTOMOTIVE: --feature=VULKAN:9348 + * </pre> + * + * This generates a class that has the following signature: + * + * <pre> + * package com.foo; + * public final class RoSystemFeatures { + * @AssumeTrueForR8 + * public static boolean hasFeatureWatch(Context context); + * @AssumeFalseForR8 + * public static boolean hasFeatureAutomotive(Context context); + * @AssumeTrueForR8 + * public static boolean hasFeatureVulkan(Context context); + * public static Boolean maybeHasFeature(String feature, int version); + * } + * </pre> + */ +object SystemFeaturesGenerator { + private const val FEATURE_ARG = "--feature=" + private const val READONLY_ARG = "--readonly=" + private val PACKAGEMANAGER_CLASS = ClassName.get("android.content.pm", "PackageManager") + private val CONTEXT_CLASS = ClassName.get("android.content", "Context") + private val ASSUME_TRUE_CLASS = + ClassName.get("com.android.aconfig.annotations", "AssumeTrueForR8") + private val ASSUME_FALSE_CLASS = + ClassName.get("com.android.aconfig.annotations", "AssumeFalseForR8") + + private fun usage() { + println("Usage: SystemFeaturesGenerator <outputClassName> [options]") + println(" Options:") + println(" --readonly=true|false Whether to encode features as build-time constants") + println(" --feature=\$NAME:\$VER A feature+version pair (blank version == disabled)") + } + + /** Main entrypoint for build-time system feature codegen. */ + @JvmStatic + fun main(args: Array<String>) { + if (args.size < 1) { + usage() + return + } + + var readonly = false + var outputClassName: ClassName? = null + val features = mutableListOf<FeatureInfo>() + for (arg in args) { + when { + arg.startsWith(READONLY_ARG) -> + readonly = arg.substring(READONLY_ARG.length).toBoolean() + arg.startsWith(FEATURE_ARG) -> { + features.add(parseFeatureArg(arg)) + } + else -> outputClassName = ClassName.bestGuess(arg) + } + } + + outputClassName + ?: run { + println("Output class name must be provided.") + usage() + return + } + + val classBuilder = + TypeSpec.classBuilder(outputClassName) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + .addJavadoc("@hide") + + addFeatureMethodsToClass(classBuilder, readonly, features) + addMaybeFeatureMethodToClass(classBuilder, readonly, features) + + // TODO(b/203143243): Add validation of build vs runtime values to ensure consistency. + JavaFile.builder(outputClassName.packageName(), classBuilder.build()) + .build() + .writeTo(System.out) + } + + /* + * Parses a feature argument of the form "--feature=$NAME:$VER", where "$VER" is optional. + * * "--feature=WATCH:0" -> Feature enabled w/ version 0 (default version when enabled) + * * "--feature=WATCH:7" -> Feature enabled w/ version 7 + * * "--feature=WATCH:" -> Feature disabled + */ + private fun parseFeatureArg(arg: String): FeatureInfo { + val featureArgs = arg.substring(FEATURE_ARG.length).split(":") + val name = featureArgs[0].let { if (!it.startsWith("FEATURE_")) "FEATURE_$it" else it } + val version = featureArgs.getOrNull(1)?.toIntOrNull() + return FeatureInfo(name, version) + } + + /* + * Adds per-feature query methods to the class with the form: + * {@code public static boolean hasFeatureX(Context context)}, + * returning the fallback value from PackageManager if not readonly. + */ + private fun addFeatureMethodsToClass( + builder: TypeSpec.Builder, + readonly: Boolean, + features: List<FeatureInfo> + ) { + for (feature in features) { + // Turn "FEATURE_FOO" into "hasFeatureFoo". + val methodName = + "has" + CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, feature.name) + val methodBuilder = + MethodSpec.methodBuilder(methodName) + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .returns(Boolean::class.java) + .addParameter(CONTEXT_CLASS, "context") + + if (readonly) { + val featureEnabled = compareValues(feature.version, 0) >= 0 + methodBuilder.addAnnotation( + if (featureEnabled) ASSUME_TRUE_CLASS else ASSUME_FALSE_CLASS + ) + methodBuilder.addStatement("return $featureEnabled") + } else { + methodBuilder.addStatement( + "return hasFeatureFallback(context, \$T.\$N)", + PACKAGEMANAGER_CLASS, + feature.name + ) + } + builder.addMethod(methodBuilder.build()) + } + + if (!readonly) { + builder.addMethod( + MethodSpec.methodBuilder("hasFeatureFallback") + .addModifiers(Modifier.PRIVATE, Modifier.STATIC) + .returns(Boolean::class.java) + .addParameter(CONTEXT_CLASS, "context") + .addParameter(String::class.java, "featureName") + .addStatement( + "return context.getPackageManager().hasSystemFeature(featureName, 0)" + ) + .build() + ) + } + } + + /* + * Adds a generic query method to the class with the form: {@code public static boolean + * maybeHasFeature(String featureName, int version)}, returning null if the feature version is + * undefined or not readonly. + * + * This method is useful for internal usage within the framework, e.g., from the implementation + * of {@link android.content.pm.PackageManager#hasSystemFeature(Context)}, when we may only + * want a valid result if it's defined as readonly, and we want a custom fallback otherwise + * (e.g., to the existing runtime binder query). + */ + private fun addMaybeFeatureMethodToClass( + builder: TypeSpec.Builder, + readonly: Boolean, + features: List<FeatureInfo> + ) { + val methodBuilder = + MethodSpec.methodBuilder("maybeHasFeature") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addAnnotation(ClassName.get("android.annotation", "Nullable")) + .returns(Boolean::class.javaObjectType) // Use object type for nullability + .addParameter(String::class.java, "featureName") + .addParameter(Int::class.java, "version") + + if (readonly) { + methodBuilder.beginControlFlow("switch (featureName)") + for (feature in features) { + methodBuilder.addCode("case \$T.\$N: ", PACKAGEMANAGER_CLASS, feature.name) + if (feature.version != null) { + methodBuilder.addStatement("return \$L >= version", feature.version) + } else { + methodBuilder.addStatement("return false") + } + } + methodBuilder.addCode("default: ") + methodBuilder.addStatement("break") + methodBuilder.endControlFlow() + } + methodBuilder.addStatement("return null") + builder.addMethod(methodBuilder.build()) + } + + private data class FeatureInfo(val name: String, val version: Int?) +} diff --git a/tools/systemfeatures/tests/Context.java b/tools/systemfeatures/tests/Context.java new file mode 100644 index 000000000000..630bc0771a01 --- /dev/null +++ b/tools/systemfeatures/tests/Context.java @@ -0,0 +1,27 @@ +/* + * 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 android.content; + +import android.content.pm.PackageManager; + +/** Stub for testing. */ +public class Context { + /** @hide */ + public PackageManager getPackageManager() { + return null; + } +} diff --git a/tools/systemfeatures/tests/PackageManager.java b/tools/systemfeatures/tests/PackageManager.java new file mode 100644 index 000000000000..645d500bc762 --- /dev/null +++ b/tools/systemfeatures/tests/PackageManager.java @@ -0,0 +1,30 @@ +/* + * 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 android.content.pm; + +/** Stub for testing */ +public class PackageManager { + public static final String FEATURE_AUTO = "automotive"; + public static final String FEATURE_VULKAN = "vulkan"; + public static final String FEATURE_WATCH = "watch"; + public static final String FEATURE_WIFI = "wifi"; + + /** @hide */ + public boolean hasSystemFeature(String featureName, int version) { + return false; + } +} diff --git a/tools/systemfeatures/tests/SystemFeaturesGeneratorTest.java b/tools/systemfeatures/tests/SystemFeaturesGeneratorTest.java new file mode 100644 index 000000000000..547d2cbd26f9 --- /dev/null +++ b/tools/systemfeatures/tests/SystemFeaturesGeneratorTest.java @@ -0,0 +1,135 @@ +/* + * 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.systemfeatures; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.pm.PackageManager; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +@RunWith(JUnit4.class) +public class SystemFeaturesGeneratorTest { + + @Rule public final MockitoRule mockito = MockitoJUnit.rule(); + + @Mock private Context mContext; + @Mock private PackageManager mPackageManager; + + @Before + public void setUp() { + when(mContext.getPackageManager()).thenReturn(mPackageManager); + } + + @Test + public void testReadonlyDisabledNoDefinedFeatures() { + // Always report null for conditional queries if readonly codegen is disabled. + assertThat(RwNoFeatures.maybeHasFeature(PackageManager.FEATURE_WATCH, 0)).isNull(); + assertThat(RwNoFeatures.maybeHasFeature(PackageManager.FEATURE_WIFI, 0)).isNull(); + assertThat(RwNoFeatures.maybeHasFeature(PackageManager.FEATURE_VULKAN, 0)).isNull(); + assertThat(RwNoFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, 0)).isNull(); + assertThat(RwNoFeatures.maybeHasFeature("com.arbitrary.feature", 0)).isNull(); + } + + @Test + public void testReadonlyNoDefinedFeatures() { + // If no features are explicitly declared as readonly available, always report + // null for conditional queries. + assertThat(RoNoFeatures.maybeHasFeature(PackageManager.FEATURE_WATCH, 0)).isNull(); + assertThat(RoNoFeatures.maybeHasFeature(PackageManager.FEATURE_WIFI, 0)).isNull(); + assertThat(RoNoFeatures.maybeHasFeature(PackageManager.FEATURE_VULKAN, 0)).isNull(); + assertThat(RoNoFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, 0)).isNull(); + assertThat(RoNoFeatures.maybeHasFeature("com.arbitrary.feature", 0)).isNull(); + } + + @Test + public void testReadonlyDisabledWithDefinedFeatures() { + // Always fall back to the PackageManager for defined, explicit features queries. + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH, 0)).thenReturn(true); + assertThat(RwFeatures.hasFeatureWatch(mContext)).isTrue(); + + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH, 0)).thenReturn(false); + assertThat(RwFeatures.hasFeatureWatch(mContext)).isFalse(); + + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI, 0)).thenReturn(true); + assertThat(RwFeatures.hasFeatureWifi(mContext)).isTrue(); + + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_VULKAN, 0)).thenReturn(false); + assertThat(RwFeatures.hasFeatureVulkan(mContext)).isFalse(); + + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_AUTO, 0)).thenReturn(false); + assertThat(RwFeatures.hasFeatureAuto(mContext)).isFalse(); + + // For defined and undefined features, conditional queries should report null (unknown). + assertThat(RwFeatures.maybeHasFeature(PackageManager.FEATURE_WATCH, 0)).isNull(); + assertThat(RwFeatures.maybeHasFeature(PackageManager.FEATURE_WIFI, 0)).isNull(); + assertThat(RwFeatures.maybeHasFeature(PackageManager.FEATURE_VULKAN, 0)).isNull(); + assertThat(RwFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, 0)).isNull(); + assertThat(RwFeatures.maybeHasFeature("com.arbitrary.feature", 0)).isNull(); + } + + @Test + public void testReadonlyWithDefinedFeatures() { + // Always use the build-time feature version for defined, explicit feature queries, never + // falling back to the runtime query. + assertThat(RoFeatures.hasFeatureWatch(mContext)).isTrue(); + assertThat(RoFeatures.hasFeatureWifi(mContext)).isTrue(); + assertThat(RoFeatures.hasFeatureVulkan(mContext)).isFalse(); + assertThat(RoFeatures.hasFeatureAuto(mContext)).isFalse(); + verify(mPackageManager, never()).hasSystemFeature(anyString(), anyInt()); + + // For defined feature types, conditional queries should reflect the build-time versions. + // VERSION=1 + assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_WATCH, -1)).isTrue(); + assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_WATCH, 0)).isTrue(); + assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_WATCH, 100)).isFalse(); + + // VERSION=0 + assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_WIFI, -1)).isTrue(); + assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_WIFI, 0)).isTrue(); + assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_WIFI, 100)).isFalse(); + + // VERSION=-1 + assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_VULKAN, -1)).isTrue(); + assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_VULKAN, 0)).isFalse(); + assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_VULKAN, 100)).isFalse(); + + // DISABLED + assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, -1)).isFalse(); + assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, 0)).isFalse(); + assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, 100)).isFalse(); + + // For undefined types, conditional queries should report null (unknown). + assertThat(RoFeatures.maybeHasFeature("com.arbitrary.feature", -1)).isNull(); + assertThat(RoFeatures.maybeHasFeature("com.arbitrary.feature", 0)).isNull(); + assertThat(RoFeatures.maybeHasFeature("com.arbitrary.feature", 100)).isNull(); + } +} |