diff options
833 files changed, 26513 insertions, 11393 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index 6b8baf8723c1..4e34b63be0d6 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -172,6 +172,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"], @@ -478,6 +479,13 @@ java_aconfig_library { defaults: ["framework-minus-apex-aconfig-java-defaults"], } +cc_aconfig_library { + name: "android.os.vibrator.flags-aconfig-cc", + aconfig_declarations: "android.os.vibrator.flags-aconfig", + host_supported: true, + vendor_available: true, +} + // View aconfig_declarations { name: "android.view.flags-aconfig", @@ -1101,6 +1109,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"], diff --git a/PERFORMANCE_OWNERS b/PERFORMANCE_OWNERS index 48a020130445..02b0a1ec75e7 100644 --- a/PERFORMANCE_OWNERS +++ b/PERFORMANCE_OWNERS @@ -6,3 +6,4 @@ philipcuadra@google.com shayba@google.com jdduke@google.com shombert@google.com +kevinjeon@google.com diff --git a/Ravenwood.bp b/Ravenwood.bp index 87f11242bfbb..5f32ba026b50 100644 --- a/Ravenwood.bp +++ b/Ravenwood.bp @@ -25,25 +25,16 @@ java_library { visibility: ["//visibility:private"], } -// Generate the stub/impl from framework-all, with hidden APIs. -java_genrule { - name: "framework-minus-apex.ravenwood-base", - tools: ["hoststubgen"], - cmd: "$(location hoststubgen) " + - "@$(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) " + +// 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, either by making hostsubgen support sharding natively, or +// making a better build rule. - "--in-jar $(location :framework-minus-apex-for-hoststubgen) " + - "--policy-override-file $(location :ravenwood-framework-policies) " + - "--annotation-allowed-classes-file $(location :ravenwood-annotation-allowed-classes) ", +genrule_defaults { + name: "framework-minus-apex.ravenwood-base_defaults", + defaults: ["ravenwood-internal-only-visibility-genrule"], + tools: ["hoststubgen"], srcs: [ ":framework-minus-apex-for-hoststubgen", ":ravenwood-framework-policies", @@ -52,27 +43,120 @@ java_genrule { ], out: [ "ravenwood.jar", + "hoststubgen_framework-minus-apex.log", + ], +} - // Following files are created just as FYI. +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: 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: 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: 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: 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: framework_minus_apex_cmd + " --num-shards 10 --shard-index 4", +} + +java_genrule { + name: "framework-minus-apex.ravenwood-base_X5", + defaults: ["framework-minus-apex.ravenwood-base_defaults"], + cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 5", +} + +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", +} + +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_X8", + defaults: ["framework-minus-apex.ravenwood-base_defaults"], + cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 8", +} + +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", +} + +// 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) " + + + "--gen-keep-all-file $(location hoststubgen_framework-minus-apex_keep_all.txt) " + + "--gen-input-dump-file $(location hoststubgen_framework-minus-apex_dump.txt) ", + + out: [ "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", ], - defaults: ["ravenwood-internal-only-visibility-genrule"], } -// Extract the impl jar from "framework-minus-apex.ravenwood-base" for subsequent build rules. -// Note this emits a "device side" output, so that ravenwood tests can (implicitly) -// depend on it. +// Marge all the sharded jars java_genrule { name: "framework-minus-apex.ravenwood", - defaults: ["ravenwood-internal-only-visibility-genrule"], - cmd: "cp $(in) $(out)", + defaults: ["ravenwood-internal-only-visibility-java"], + cmd: "$(location merge_zips) $(out) $(in)", + tools: ["merge_zips"], srcs: [ - ":framework-minus-apex.ravenwood-base{ravenwood.jar}", + ":framework-minus-apex.ravenwood-base_X0{ravenwood.jar}", + ":framework-minus-apex.ravenwood-base_X1{ravenwood.jar}", + ":framework-minus-apex.ravenwood-base_X2{ravenwood.jar}", + ":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", @@ -139,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"], @@ -151,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"], @@ -159,7 +245,8 @@ java_library { "framework-minus-apex.ravenwood", ], sdk_version: "core_platform", - jarjar_rules: ":ravenwood-framework-jarjar-rules", + // See b/313930116. Jarjar is too slow on this jar. We use HostStubGen to do the rename. + // jarjar_rules: ":ravenwood-framework-jarjar-rules", } java_genrule { diff --git a/ZYGOTE_OWNERS b/ZYGOTE_OWNERS index f6d15e03a892..6918c16840dd 100644 --- a/ZYGOTE_OWNERS +++ b/ZYGOTE_OWNERS @@ -1,4 +1,4 @@ chriswailes@google.com +hboehm@google.com maco@google.com -narayan@google.com ngeoffray@google.com 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/cmds/idmap2/idmap2d/Idmap2Service.cpp b/cmds/idmap2/idmap2d/Idmap2Service.cpp index f264125cfde5..6902d6db6751 100644 --- a/cmds/idmap2/idmap2d/Idmap2Service.cpp +++ b/cmds/idmap2/idmap2d/Idmap2Service.cpp @@ -78,6 +78,11 @@ PolicyBitmask ConvertAidlArgToPolicyBitmask(int32_t arg) { namespace android::os { +template <typename T> +const T* Idmap2Service::GetPointer(const OwningPtr<T>& ptr) { + return std::visit([](auto&& ptr) { return ptr.get(); }, ptr); +} + Status Idmap2Service::getIdmapPath(const std::string& overlay_path, int32_t user_id ATTRIBUTE_UNUSED, std::string* _aidl_return) { assert(_aidl_return); @@ -224,7 +229,7 @@ idmap2::Result<Idmap2Service::TargetResourceContainerPtr> Idmap2Service::GetTarg if (is_framework || (item.dev == st.st_dev && item.inode == st.st_ino && item.size == st.st_size && item.mtime.tv_sec == st.st_mtim.tv_sec && item.mtime.tv_nsec == st.st_mtim.tv_nsec)) { - return {item.apk.get()}; + return {item.apk}; } container_cache_.erase(cache_it); } @@ -238,14 +243,14 @@ idmap2::Result<Idmap2Service::TargetResourceContainerPtr> Idmap2Service::GetTarg return {std::move(*target)}; } - const auto res = target->get(); + auto res = std::shared_ptr(std::move(*target)); std::lock_guard lock(container_cache_mutex_); container_cache_.emplace(target_path, CachedContainer { .dev = dev_t(st.st_dev), .inode = ino_t(st.st_ino), .size = st.st_size, .mtime = st.st_mtim, - .apk = std::move(*target) + .apk = res }); return {res}; } diff --git a/cmds/idmap2/idmap2d/Idmap2Service.h b/cmds/idmap2/idmap2d/Idmap2Service.h index a69fa6119974..272ec6be3bac 100644 --- a/cmds/idmap2/idmap2d/Idmap2Service.h +++ b/cmds/idmap2/idmap2d/Idmap2Service.h @@ -85,7 +85,7 @@ class Idmap2Service : public BinderService<Idmap2Service>, public BnIdmap2 { ino_t inode; int64_t size; struct timespec mtime; - std::unique_ptr<idmap2::TargetResourceContainer> apk; + std::shared_ptr<idmap2::TargetResourceContainer> apk; }; std::unordered_map<std::string, CachedContainer> container_cache_; std::mutex container_cache_mutex_; @@ -95,24 +95,15 @@ class Idmap2Service : public BinderService<Idmap2Service>, public BnIdmap2 { std::mutex frro_iter_mutex_; template <typename T> - using MaybeUniquePtr = std::variant<std::unique_ptr<T>, T*>; + using OwningPtr = std::variant<std::unique_ptr<T>, std::shared_ptr<T>>; - using TargetResourceContainerPtr = MaybeUniquePtr<idmap2::TargetResourceContainer>; + using TargetResourceContainerPtr = OwningPtr<idmap2::TargetResourceContainer>; idmap2::Result<TargetResourceContainerPtr> GetTargetContainer(const std::string& target_path); template <typename T> - WARN_UNUSED static const T* GetPointer(const MaybeUniquePtr<T>& ptr); + WARN_UNUSED static const T* GetPointer(const OwningPtr<T>& ptr); }; -template <typename T> -const T* Idmap2Service::GetPointer(const MaybeUniquePtr<T>& ptr) { - auto u = std::get_if<T*>(&ptr); - if (u != nullptr) { - return *u; - } - return std::get<std::unique_ptr<T>>(ptr).get(); -} - } // namespace android::os #endif // IDMAP2_IDMAP2D_IDMAP2SERVICE_H_ diff --git a/core/api/current.txt b/core/api/current.txt index 354e26b2eb02..ea039a7103a3 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -59034,7 +59034,7 @@ package android.widget { } public static interface CompoundButton.OnCheckedChangeListener { - method public void onCheckedChanged(android.widget.CompoundButton, boolean); + method public void onCheckedChanged(@NonNull android.widget.CompoundButton, boolean); } public abstract class CursorAdapter extends android.widget.BaseAdapter implements android.widget.Filterable android.widget.ThemedSpinnerAdapter { @@ -60099,7 +60099,7 @@ package android.widget { } public static interface RadioGroup.OnCheckedChangeListener { - method public void onCheckedChanged(android.widget.RadioGroup, @IdRes int); + method public void onCheckedChanged(@NonNull android.widget.RadioGroup, @IdRes int); } public class RatingBar extends android.widget.AbsSeekBar { diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 15e5706db9d1..445a57220757 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -11981,6 +11981,7 @@ package android.provider { field public static final String ACTION_MANAGE_APP_OVERLAY_PERMISSION = "android.settings.MANAGE_APP_OVERLAY_PERMISSION"; field public static final String ACTION_MANAGE_DOMAIN_URLS = "android.settings.MANAGE_DOMAIN_URLS"; field public static final String ACTION_MANAGE_MORE_DEFAULT_APPS_SETTINGS = "android.settings.MANAGE_MORE_DEFAULT_APPS_SETTINGS"; + field @FlaggedApi("android.nfc.nfc_action_manage_services_settings") public static final String ACTION_MANAGE_OTHER_NFC_SERVICES_SETTINGS = "android.settings.MANAGE_OTHER_NFC_SERVICES_SETTINGS"; field public static final String ACTION_NOTIFICATION_POLICY_ACCESS_DETAIL_SETTINGS = "android.settings.NOTIFICATION_POLICY_ACCESS_DETAIL_SETTINGS"; field public static final String ACTION_REQUEST_ENABLE_CONTENT_CAPTURE = "android.settings.REQUEST_ENABLE_CONTENT_CAPTURE"; field public static final String ACTION_SHOW_ADMIN_SUPPORT_DETAILS = "android.settings.SHOW_ADMIN_SUPPORT_DETAILS"; diff --git a/core/java/android/app/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/AppCompatTaskInfo.java b/core/java/android/app/AppCompatTaskInfo.java index a6d3f9dba080..81e9df66e18a 100644 --- a/core/java/android/app/AppCompatTaskInfo.java +++ b/core/java/android/app/AppCompatTaskInfo.java @@ -18,63 +18,21 @@ package android.app; import static android.app.TaskInfo.PROPERTY_VALUE_UNSET; +import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.os.Parcel; import android.os.Parcelable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + /** * Stores App Compat information about a particular Task. * @hide */ public class AppCompatTaskInfo implements Parcelable { /** - * Whether the direct top activity is eligible for letterbox education. - */ - public boolean topActivityEligibleForLetterboxEducation; - - /** - * Whether the letterbox education is enabled. - */ - public boolean isLetterboxEducationEnabled; - - /** - * Whether the direct top activity is in size compat mode on foreground. - */ - public boolean topActivityInSizeCompat; - - /** - * Whether the double tap is enabled. - */ - public boolean isLetterboxDoubleTapEnabled; - - /** - * Whether the user aspect ratio settings button is enabled. - */ - public boolean topActivityEligibleForUserAspectRatioButton; - - /** - * Whether the user has forced the activity to be fullscreen through the user aspect ratio - * settings. - */ - public boolean isUserFullscreenOverrideEnabled; - - /** - * Whether the system has forced the activity to be fullscreen - */ - public boolean isSystemFullscreenOverrideEnabled; - - /** - * Hint about the letterbox state of the top activity. - */ - public boolean topActivityBoundsLetterboxed; - - /** - * Whether the update comes from a letterbox double-tap action from the user or not. - */ - public boolean isFromLetterboxDoubleTap; - - /** * If {@link #isLetterboxDoubleTapEnabled} it contains the current letterbox vertical position * or {@link TaskInfo#PROPERTY_VALUE_UNSET} otherwise. */ @@ -115,6 +73,57 @@ public class AppCompatTaskInfo implements Parcelable { */ public CameraCompatTaskInfo cameraCompatTaskInfo = CameraCompatTaskInfo.create(); + /** Constant indicating no top activity flag has been set. */ + private static final int FLAG_UNDEFINED = 0x0; + /** Constant base value for top activity flag. */ + private static final int FLAG_BASE = 0x1; + /** Top activity flag for whether letterbox education is enabled. */ + private static final int FLAG_LETTERBOX_EDU_ENABLED = FLAG_BASE; + /** Top activity flag for whether activity is eligible for letterbox education. */ + private static final int FLAG_ELIGIBLE_FOR_LETTERBOX_EDU = FLAG_BASE << 1; + /** Top activity flag for whether activity bounds are letterboxed. */ + private static final int FLAG_LETTERBOXED = FLAG_BASE << 2; + /** Top activity flag for whether activity is in size compat mode. */ + private static final int FLAG_IN_SIZE_COMPAT = FLAG_BASE << 3; + /** Top activity flag for whether letterbox double tap is enabled. */ + private static final int FLAG_LETTERBOX_DOUBLE_TAP_ENABLED = FLAG_BASE << 4; + /** Top activity flag for whether the update comes from a letterbox double tap action. */ + private static final int FLAG_IS_FROM_LETTERBOX_DOUBLE_TAP = FLAG_BASE << 5; + /** Top activity flag for whether activity is eligible for user aspect ratio button. */ + private static final int FLAG_ELIGIBLE_FOR_USER_ASPECT_RATIO_BUTTON = FLAG_BASE << 6; + /** Top activity flag for whether has activity has been overridden to fullscreen by system. */ + private static final int FLAG_FULLSCREEN_OVERRIDE_SYSTEM = FLAG_BASE << 7; + /** Top activity flag for whether has activity has been overridden to fullscreen by user. */ + private static final int FLAG_FULLSCREEN_OVERRIDE_USER = FLAG_BASE << 8; + + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = true, value = { + FLAG_UNDEFINED, + FLAG_BASE, + FLAG_LETTERBOX_EDU_ENABLED, + FLAG_ELIGIBLE_FOR_LETTERBOX_EDU, + FLAG_LETTERBOXED, + FLAG_IN_SIZE_COMPAT, + FLAG_LETTERBOX_DOUBLE_TAP_ENABLED, + FLAG_IS_FROM_LETTERBOX_DOUBLE_TAP, + FLAG_ELIGIBLE_FOR_USER_ASPECT_RATIO_BUTTON, + FLAG_FULLSCREEN_OVERRIDE_SYSTEM, + FLAG_FULLSCREEN_OVERRIDE_USER + }) + public @interface TopActivityFlag {} + + @TopActivityFlag + private int mTopActivityFlags; + + @TopActivityFlag + private static final int FLAGS_ORGANIZER_INTERESTED = FLAG_IS_FROM_LETTERBOX_DOUBLE_TAP + | FLAG_ELIGIBLE_FOR_USER_ASPECT_RATIO_BUTTON | FLAG_FULLSCREEN_OVERRIDE_SYSTEM + | FLAG_FULLSCREEN_OVERRIDE_USER; + + @TopActivityFlag + private static final int FLAGS_COMPAT_UI_INTERESTED = FLAGS_ORGANIZER_INTERESTED + | FLAG_IN_SIZE_COMPAT | FLAG_ELIGIBLE_FOR_LETTERBOX_EDU | FLAG_LETTERBOX_EDU_ENABLED; + private AppCompatTaskInfo() { // Do nothing } @@ -150,9 +159,8 @@ public class AppCompatTaskInfo implements Parcelable { * @return {@code true} if the task has some compat ui. */ public boolean hasCompatUI() { - return topActivityInSizeCompat || topActivityEligibleForLetterboxEducation - || isLetterboxDoubleTapEnabled - || topActivityEligibleForUserAspectRatioButton; + return isTopActivityInSizeCompat() || eligibleForLetterboxEducation() + || isLetterboxDoubleTapEnabled() || eligibleForUserAspectRatioButton(); } /** @@ -163,6 +171,142 @@ public class AppCompatTaskInfo implements Parcelable { } /** + * @return {@code true} if the letterbox education is enabled. + */ + public boolean isLetterboxEducationEnabled() { + return isTopActivityFlagEnabled(FLAG_LETTERBOX_EDU_ENABLED); + } + + /** + * Sets the top activity flag for whether letterbox education is enabled. + */ + public void setLetterboxEducationEnabled(boolean enable) { + setTopActivityFlag(FLAG_LETTERBOX_EDU_ENABLED, enable); + } + + /** + * @return {@code true} if the direct top activity is eligible for letterbox education. + */ + public boolean eligibleForLetterboxEducation() { + return isTopActivityFlagEnabled(FLAG_ELIGIBLE_FOR_LETTERBOX_EDU); + } + + /** + * Sets the top activity flag to be eligible for letterbox education. + */ + public void setEligibleForLetterboxEducation(boolean enable) { + setTopActivityFlag(FLAG_ELIGIBLE_FOR_LETTERBOX_EDU, enable); + } + + /** + * @return {@code true} if the direct top activity is eligible for the user aspect ratio + * settings button. + */ + public boolean eligibleForUserAspectRatioButton() { + return isTopActivityFlagEnabled(FLAG_ELIGIBLE_FOR_USER_ASPECT_RATIO_BUTTON); + } + + /** + * Sets the top activity flag to be eligible for the user aspect ratio settings button. + */ + public void setEligibleForUserAspectRatioButton(boolean enable) { + setTopActivityFlag(FLAG_ELIGIBLE_FOR_USER_ASPECT_RATIO_BUTTON, enable); + } + + /** + * @return {@code true} if double tap to reposition letterboxed app is enabled. + */ + public boolean isLetterboxDoubleTapEnabled() { + return isTopActivityFlagEnabled(FLAG_LETTERBOX_DOUBLE_TAP_ENABLED); + } + + /** + * Sets the top activity flag to enable double tap to reposition letterboxed app. + */ + public void setLetterboxDoubleTapEnabled(boolean enable) { + setTopActivityFlag(FLAG_LETTERBOX_DOUBLE_TAP_ENABLED, enable); + } + + /** + * @return {@code true} if the update comes from a letterbox double-tap action from the user. + */ + public boolean isFromLetterboxDoubleTap() { + return isTopActivityFlagEnabled(FLAG_IS_FROM_LETTERBOX_DOUBLE_TAP); + } + + /** + * Sets the top activity flag for whether the update comes from a letterbox double-tap action + * from the user. + */ + public void setIsFromLetterboxDoubleTap(boolean enable) { + setTopActivityFlag(FLAG_IS_FROM_LETTERBOX_DOUBLE_TAP, enable); + } + + /** + * @return {@code true} if the user has forced the activity to be fullscreen through the + * user aspect ratio settings. + */ + public boolean isUserFullscreenOverrideEnabled() { + return isTopActivityFlagEnabled(FLAG_FULLSCREEN_OVERRIDE_USER); + } + + /** + * Sets the top activity flag for whether the user has forced the activity to be fullscreen + * through the user aspect ratio settings. + */ + public void setUserFullscreenOverrideEnabled(boolean enable) { + setTopActivityFlag(FLAG_FULLSCREEN_OVERRIDE_USER, enable); + } + + /** + * @return {@code true} if the system has forced the activity to be fullscreen. + */ + public boolean isSystemFullscreenOverrideEnabled() { + return isTopActivityFlagEnabled(FLAG_FULLSCREEN_OVERRIDE_SYSTEM); + } + + /** + * Sets the top activity flag for whether the system has forced the activity to be fullscreen. + */ + public void setSystemFullscreenOverrideEnabled(boolean enable) { + setTopActivityFlag(FLAG_FULLSCREEN_OVERRIDE_SYSTEM, enable); + } + + /** + * @return {@code true} if the direct top activity is in size compat mode on foreground. + */ + public boolean isTopActivityInSizeCompat() { + return isTopActivityFlagEnabled(FLAG_IN_SIZE_COMPAT); + } + + /** + * Sets the top activity flag for whether the direct top activity is in size compat mode + * on foreground. + */ + public void setTopActivityInSizeCompat(boolean enable) { + setTopActivityFlag(FLAG_IN_SIZE_COMPAT, enable); + } + + /** + * @return {@code true} if the top activity bounds are letterboxed. + */ + public boolean isTopActivityLetterboxed() { + return isTopActivityFlagEnabled(FLAG_LETTERBOXED); + } + + /** + * Sets the top activity flag for whether the top activity bounds are letterboxed. + */ + public void setTopActivityLetterboxed(boolean enable) { + setTopActivityFlag(FLAG_LETTERBOXED, enable); + } + + /** Clear all top activity flags and set to false. */ + public void clearTopActivityFlags() { + mTopActivityFlags = FLAG_UNDEFINED; + } + + /** * @return {@code true} if the app compat parameters that are important for task organizers * are equal. */ @@ -170,9 +314,8 @@ public class AppCompatTaskInfo implements Parcelable { if (that == null) { return false; } - return isFromLetterboxDoubleTap == that.isFromLetterboxDoubleTap - && topActivityEligibleForUserAspectRatioButton - == that.topActivityEligibleForUserAspectRatioButton + return (mTopActivityFlags & FLAGS_ORGANIZER_INTERESTED) + == (that.mTopActivityFlags & FLAGS_ORGANIZER_INTERESTED) && topActivityLetterboxVerticalPosition == that.topActivityLetterboxVerticalPosition && topActivityLetterboxWidth == that.topActivityLetterboxWidth && topActivityLetterboxHeight == that.topActivityLetterboxHeight @@ -180,8 +323,6 @@ public class AppCompatTaskInfo implements Parcelable { && topActivityLetterboxAppHeight == that.topActivityLetterboxAppHeight && topActivityLetterboxHorizontalPosition == that.topActivityLetterboxHorizontalPosition - && isUserFullscreenOverrideEnabled == that.isUserFullscreenOverrideEnabled - && isSystemFullscreenOverrideEnabled == that.isSystemFullscreenOverrideEnabled && cameraCompatTaskInfo.equalsForTaskOrganizer(that.cameraCompatTaskInfo); } @@ -192,13 +333,8 @@ public class AppCompatTaskInfo implements Parcelable { if (that == null) { return false; } - return topActivityInSizeCompat == that.topActivityInSizeCompat - && isFromLetterboxDoubleTap == that.isFromLetterboxDoubleTap - && topActivityEligibleForUserAspectRatioButton - == that.topActivityEligibleForUserAspectRatioButton - && topActivityEligibleForLetterboxEducation - == that.topActivityEligibleForLetterboxEducation - && isLetterboxEducationEnabled == that.isLetterboxEducationEnabled + return (mTopActivityFlags & FLAGS_COMPAT_UI_INTERESTED) + == (that.mTopActivityFlags & FLAGS_COMPAT_UI_INTERESTED) && topActivityLetterboxVerticalPosition == that.topActivityLetterboxVerticalPosition && topActivityLetterboxHorizontalPosition == that.topActivityLetterboxHorizontalPosition @@ -206,8 +342,6 @@ public class AppCompatTaskInfo implements Parcelable { && topActivityLetterboxHeight == that.topActivityLetterboxHeight && topActivityLetterboxAppWidth == that.topActivityLetterboxAppWidth && topActivityLetterboxAppHeight == that.topActivityLetterboxAppHeight - && isUserFullscreenOverrideEnabled == that.isUserFullscreenOverrideEnabled - && isSystemFullscreenOverrideEnabled == that.isSystemFullscreenOverrideEnabled && cameraCompatTaskInfo.equalsForCompatUi(that.cameraCompatTaskInfo); } @@ -215,21 +349,13 @@ public class AppCompatTaskInfo implements Parcelable { * Reads the AppCompatTaskInfo from a parcel. */ void readFromParcel(Parcel source) { - isLetterboxEducationEnabled = source.readBoolean(); - topActivityInSizeCompat = source.readBoolean(); - topActivityEligibleForLetterboxEducation = source.readBoolean(); - isLetterboxDoubleTapEnabled = source.readBoolean(); - topActivityEligibleForUserAspectRatioButton = source.readBoolean(); - topActivityBoundsLetterboxed = source.readBoolean(); - isFromLetterboxDoubleTap = source.readBoolean(); + mTopActivityFlags = source.readInt(); topActivityLetterboxVerticalPosition = source.readInt(); topActivityLetterboxHorizontalPosition = source.readInt(); topActivityLetterboxWidth = source.readInt(); topActivityLetterboxHeight = source.readInt(); topActivityLetterboxAppWidth = source.readInt(); topActivityLetterboxAppHeight = source.readInt(); - isUserFullscreenOverrideEnabled = source.readBoolean(); - isSystemFullscreenOverrideEnabled = source.readBoolean(); cameraCompatTaskInfo = source.readTypedObject(CameraCompatTaskInfo.CREATOR); } @@ -238,35 +364,25 @@ public class AppCompatTaskInfo implements Parcelable { */ @Override public void writeToParcel(Parcel dest, int flags) { - dest.writeBoolean(isLetterboxEducationEnabled); - dest.writeBoolean(topActivityInSizeCompat); - dest.writeBoolean(topActivityEligibleForLetterboxEducation); - dest.writeBoolean(isLetterboxDoubleTapEnabled); - dest.writeBoolean(topActivityEligibleForUserAspectRatioButton); - dest.writeBoolean(topActivityBoundsLetterboxed); - dest.writeBoolean(isFromLetterboxDoubleTap); + dest.writeInt(mTopActivityFlags); dest.writeInt(topActivityLetterboxVerticalPosition); dest.writeInt(topActivityLetterboxHorizontalPosition); dest.writeInt(topActivityLetterboxWidth); dest.writeInt(topActivityLetterboxHeight); dest.writeInt(topActivityLetterboxAppWidth); dest.writeInt(topActivityLetterboxAppHeight); - dest.writeBoolean(isUserFullscreenOverrideEnabled); - dest.writeBoolean(isSystemFullscreenOverrideEnabled); dest.writeTypedObject(cameraCompatTaskInfo, flags); } @Override public String toString() { - return "AppCompatTaskInfo { topActivityInSizeCompat=" + topActivityInSizeCompat - + " topActivityEligibleForLetterboxEducation= " - + topActivityEligibleForLetterboxEducation - + "isLetterboxEducationEnabled= " + isLetterboxEducationEnabled - + " isLetterboxDoubleTapEnabled= " + isLetterboxDoubleTapEnabled - + " topActivityEligibleForUserAspectRatioButton= " - + topActivityEligibleForUserAspectRatioButton - + " topActivityBoundsLetterboxed= " + topActivityBoundsLetterboxed - + " isFromLetterboxDoubleTap= " + isFromLetterboxDoubleTap + return "AppCompatTaskInfo { topActivityInSizeCompat=" + isTopActivityInSizeCompat() + + " eligibleForLetterboxEducation= " + eligibleForLetterboxEducation() + + " isLetterboxEducationEnabled= " + isLetterboxEducationEnabled() + + " isLetterboxDoubleTapEnabled= " + isLetterboxDoubleTapEnabled() + + " eligibleForUserAspectRatioButton= " + eligibleForUserAspectRatioButton() + + " topActivityBoundsLetterboxed= " + isTopActivityLetterboxed() + + " isFromLetterboxDoubleTap= " + isFromLetterboxDoubleTap() + " topActivityLetterboxVerticalPosition= " + topActivityLetterboxVerticalPosition + " topActivityLetterboxHorizontalPosition= " + topActivityLetterboxHorizontalPosition @@ -274,9 +390,17 @@ public class AppCompatTaskInfo implements Parcelable { + " topActivityLetterboxHeight=" + topActivityLetterboxHeight + " topActivityLetterboxAppWidth=" + topActivityLetterboxAppWidth + " topActivityLetterboxAppHeight=" + topActivityLetterboxAppHeight - + " isUserFullscreenOverrideEnabled=" + isUserFullscreenOverrideEnabled - + " isSystemFullscreenOverrideEnabled=" + isSystemFullscreenOverrideEnabled + + " isUserFullscreenOverrideEnabled=" + isUserFullscreenOverrideEnabled() + + " isSystemFullscreenOverrideEnabled=" + isSystemFullscreenOverrideEnabled() + " cameraCompatTaskInfo=" + cameraCompatTaskInfo.toString() + "}"; } + + private void setTopActivityFlag(@TopActivityFlag int flag, boolean enable) { + mTopActivityFlags = enable ? (mTopActivityFlags | flag) : (mTopActivityFlags & ~flag); + } + + private boolean isTopActivityFlagEnabled(@TopActivityFlag int flag) { + return (mTopActivityFlags & flag) == flag; + } } diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index 5214d2c9c02a..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; } } @@ -11013,7 +11018,8 @@ public class AppOpsManager { if (access != null) { NoteOpEvent existingAccess = accessEvents.get(key); - if (existingAccess == null || existingAccess.getDuration() == -1) { + if (existingAccess == null || existingAccess.getDuration() == -1 + || existingAccess.getDuration() < access.getDuration()) { accessEvents.append(key, access); } else if (existingAccess.mProxy == null && access.mProxy != null ) { existingAccess.mProxy = access.mProxy; diff --git a/core/java/android/app/ComponentOptions.java b/core/java/android/app/ComponentOptions.java index b3fc0588022b..0819833451ff 100644 --- a/core/java/android/app/ComponentOptions.java +++ b/core/java/android/app/ComponentOptions.java @@ -105,17 +105,10 @@ public class ComponentOptions { public @NonNull ComponentOptions setPendingIntentBackgroundActivityStartMode( @BackgroundActivityStartMode int state) { switch (state) { - case MODE_BACKGROUND_ACTIVITY_START_ALLOWED: - if (mPendingIntentBalAllowed != MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS) { - // do not overwrite ALWAYS with ALLOWED for backwards compatibility, - // if setPendingIntentBackgroundActivityLaunchAllowedByPermission is used - // before this method. - mPendingIntentBalAllowed = state; - } - break; case MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED: case MODE_BACKGROUND_ACTIVITY_START_DENIED: case MODE_BACKGROUND_ACTIVITY_START_COMPAT: + case MODE_BACKGROUND_ACTIVITY_START_ALLOWED: case MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS: case MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE: mPendingIntentBalAllowed = state; @@ -139,35 +132,6 @@ public class ComponentOptions { return mPendingIntentBalAllowed; } - /** - * Get PendingIntent activity is allowed to be started in the background if the caller - * has BAL permission. - * @hide - * @deprecated check for #MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS - */ - @Deprecated - public boolean isPendingIntentBackgroundActivityLaunchAllowedByPermission() { - return mPendingIntentBalAllowed == MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS; - } - - /** - * Set PendingIntent activity can be launched from background if caller has BAL permission. - * @hide - * @deprecated use #MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS - */ - @Deprecated - public void setPendingIntentBackgroundActivityLaunchAllowedByPermission(boolean allowed) { - if (allowed) { - setPendingIntentBackgroundActivityStartMode( - MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS); - } else { - if (getPendingIntentBackgroundActivityStartMode() - == MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS) { - setPendingIntentBackgroundActivityStartMode(MODE_BACKGROUND_ACTIVITY_START_ALLOWED); - } - } - } - /** @hide */ public Bundle toBundle() { Bundle b = new Bundle(); 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/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/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/content/Context.java b/core/java/android/content/Context.java index 8365840b1efb..9aebfc8e5fd7 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -6676,6 +6676,16 @@ public abstract class Context { public static final String BLOCKED_NUMBERS_SERVICE = "blocked_numbers"; /** + * Use with {@link #getSystemService(String)} to retrieve the + * {@link com.android.internal.protolog.ProtoLogService} for registering ProtoLog clients. + * + * @see #getSystemService(String) + * @see com.android.internal.protolog.ProtoLogService + * @hide + */ + public static final String PROTOLOG_SERVICE = "protolog"; + + /** * Determine whether the given permission is allowed for a particular * process and user ID running in the system. * diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index 111e6a8e93ef..cb57c7bd565d 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -7485,7 +7485,7 @@ public class Intent implements Parcelable, Cloneable { /** * This flag is only used for split-screen multi-window mode. The new activity will be displayed - * adjacent to the one launching it. This can only be used in conjunction with + * adjacent to the one launching it if possible. This can only be used in conjunction with * {@link #FLAG_ACTIVITY_NEW_TASK}. Also, setting {@link #FLAG_ACTIVITY_MULTIPLE_TASK} is * required if you want a new instance of an existing activity to be created. */ diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig index 835459e09ca2..59fca3bc8cb1 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" @@ -381,8 +391,12 @@ flag { } flag { - name: "unicorn_mode_refactoring_for_hsum" + name: "unicorn_mode_refactoring_for_hsum_read_only" namespace: "multiuser" - description: "Refactorings related to unicorn mode to work on HSUM mode" + 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/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/camera2/impl/CameraDeviceImpl.java b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java index 7f3c49dbb580..1e7f70bf5a72 100644 --- a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java +++ b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java @@ -918,7 +918,7 @@ public class CameraDeviceImpl extends CameraDevice checkIfCameraClosedOrInError(); for (String physicalId : physicalCameraIdSet) { - if (physicalId == getId()) { + if (Objects.equals(physicalId, getId())) { throw new IllegalStateException("Physical id matches the logical id!"); } } 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 00ce9491784b..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; @@ -2796,6 +2789,9 @@ public class InputMethodService extends AbstractInputMethodService { * <p>This dismisses the {@link #getStylusHandwritingWindow ink window} and stops intercepting * stylus {@code MotionEvent}s. * + * <p>Connectionless handwriting sessions should be finished using {@link + * #finishConnectionlessStylusHandwriting(CharSequence)}. + * * <p>Note for IME developers: Call this method at any time to finish the current handwriting * session. Generally, this should be invoked after a short timeout, giving the user enough time * to start the next stylus stroke, if any. By default, system will time-out after few seconds. @@ -2803,9 +2799,6 @@ public class InputMethodService extends AbstractInputMethodService { * * <p>Handwriting session will be finished by framework on next {@link #onFinishInput()}. */ - // TODO(b/300979854): Once connectionless APIs are finalised, update documentation to add: - // <p>Connectionless handwriting sessions should be finished using {@link - // #finishConnectionlessStylusHandwriting(CharSequence)}. public final void finishStylusHandwriting() { if (DEBUG) Log.v(TAG, "finishStylusHandwriting()"); if (mInkWindow == null) { @@ -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/inputmethodservice/navigationbar/KeyButtonView.java b/core/java/android/inputmethodservice/navigationbar/KeyButtonView.java index 540243c4fe92..bfa95f7a75c7 100644 --- a/core/java/android/inputmethodservice/navigationbar/KeyButtonView.java +++ b/core/java/android/inputmethodservice/navigationbar/KeyButtonView.java @@ -22,6 +22,7 @@ import static android.view.KeyEvent.KEYCODE_UNKNOWN; import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK; import static android.view.accessibility.AccessibilityNodeInfo.ACTION_LONG_CLICK; +import android.annotation.Nullable; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; @@ -126,15 +127,29 @@ public class KeyButtonView extends ImageView implements ButtonInterface { @Override public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); - if (mCode != KEYCODE_UNKNOWN) { + if (isClickable()) { info.addAction(new AccessibilityNodeInfo.AccessibilityAction(ACTION_CLICK, null)); if (isLongClickable()) { info.addAction( - new AccessibilityNodeInfo.AccessibilityAction(ACTION_LONG_CLICK, null)); + new AccessibilityNodeInfo.AccessibilityAction(ACTION_LONG_CLICK, + getAccessibilityLongClickActionLabel())); } } } + /** + * Gets the accessibility long click action label for the button, or {@code null} for no label. + */ + @Nullable + private CharSequence getAccessibilityLongClickActionLabel() { + if (Flags.imeSwitcherRevamp() + && getId() == com.android.internal.R.id.input_method_nav_ime_switcher) { + return getContext().getText( + com.android.internal.R.string.input_method_ime_switch_long_click_action_desc); + } + return null; + } + @Override protected void onWindowVisibilityChanged(int visibility) { super.onWindowVisibilityChanged(visibility); 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/UserManager.java b/core/java/android/os/UserManager.java index 392b6eb6d51b..28f2c2530ae9 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -2391,10 +2391,15 @@ public class UserManager { */ public static final int USER_OPERATION_ERROR_DISABLED_USER = 8; /** - * Indicates user operation failed because user is disabled on the device. + * Indicates user operation failed because private space is disabled on the device. * @hide */ public static final int USER_OPERATION_ERROR_PRIVATE_PROFILE = 9; + /** + * Indicates user operation failed because user is restricted on the device. + * @hide + */ + public static final int USER_OPERATION_ERROR_USER_RESTRICTED = 10; /** * Result returned from various user operations. @@ -2413,6 +2418,7 @@ public class UserManager { USER_OPERATION_ERROR_USER_ACCOUNT_ALREADY_EXISTS, USER_OPERATION_ERROR_DISABLED_USER, USER_OPERATION_ERROR_PRIVATE_PROFILE, + USER_OPERATION_ERROR_USER_RESTRICTED, }) public @interface UserOperationResult {} @@ -4818,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 = { @@ -4838,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 67c346401804..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 @@ -20,13 +13,6 @@ flag { flag { namespace: "haptics" - name: "keyboard_category_enabled" - description: "Enables the independent keyboard vibration settings feature" - bug: "289107579" -} - -flag { - namespace: "haptics" name: "adaptive_haptics_enabled" description: "Enables the adaptive haptics feature" bug: "305961689" @@ -85,3 +71,45 @@ flag { purpose: PURPOSE_FEATURE } } + +flag { + namespace: "haptics" + name: "fix_audio_coupled_haptics_scaling" + description: "Fix the audio-coupled haptics scaling to use same function as VibrationEffect" + bug: "356144312" + metadata { + 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/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/DeviceEffectsApplier.java b/core/java/android/service/notification/DeviceEffectsApplier.java index 5194cdd47933..2472860ae14b 100644 --- a/core/java/android/service/notification/DeviceEffectsApplier.java +++ b/core/java/android/service/notification/DeviceEffectsApplier.java @@ -16,8 +16,6 @@ package android.service.notification; -import android.service.notification.ZenModeConfig.ConfigChangeOrigin; - /** * Responsible for making any service calls needed to apply the set of {@link ZenDeviceEffects} that * make sense for the current platform. @@ -43,5 +41,5 @@ public interface DeviceEffectsApplier { * changing as a result of an explicit user action, then it makes sense to * apply them immediately regardless. */ - void apply(ZenDeviceEffects effects, @ConfigChangeOrigin int source); + void apply(ZenDeviceEffects effects, @ZenModeConfig.ConfigOrigin int source); } diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java index 7ca248da3dd8..224379b4fd98 100644 --- a/core/java/android/service/notification/ZenModeConfig.java +++ b/core/java/android/service/notification/ZenModeConfig.java @@ -25,6 +25,8 @@ 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.Condition.STATE_TRUE; +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; @@ -112,68 +114,83 @@ public class ZenModeConfig implements Parcelable { private static final String TAG = "ZenModeConfig"; /** - * The {@link ZenModeConfig} is being updated because of an unknown reason. + * The {@link ZenModeConfig} is updated because of an unknown reason. */ - public static final int UPDATE_ORIGIN_UNKNOWN = 0; + public static final int ORIGIN_UNKNOWN = 0; /** - * The {@link ZenModeConfig} is being updated because of system initialization (i.e. load from + * The {@link ZenModeConfig} is updated because of system initialization (i.e. load from * storage, on device boot). */ - public static final int UPDATE_ORIGIN_INIT = 1; + public static final int ORIGIN_INIT = 1; - /** The {@link ZenModeConfig} is being updated (replaced) because of a user switch or unlock. */ - public static final int UPDATE_ORIGIN_INIT_USER = 2; + /** The {@link ZenModeConfig} is updated (replaced) because of a user switch or unlock. */ + public static final int ORIGIN_INIT_USER = 2; - /** The {@link ZenModeConfig} is being updated because of a user action, for example: + /** + * The {@link ZenModeConfig} is updated because of a <em>user action</em> performed from a + * system surface, such as: * <ul> - * <li>{@link NotificationManager#setAutomaticZenRuleState} with a - * {@link Condition#source} equal to {@link Condition#SOURCE_USER_ACTION}.</li> - * <li>Adding, updating, or removing a rule from Settings.</li> - * <li>Directly activating or deactivating/snoozing a rule through some UI affordance (e.g. - * Quick Settings).</li> + * <li>Adding, updating, or removing a rule from Settings. + * <li>Activating or deactivating a rule through the System (e.g. from Settings/Modes). + * <li>Activating or deactivating a rule through SystemUi (e.g. with Quick Settings). * </ul> + * + * <p>This does <em>not</em> include user actions from apps ({@link #ORIGIN_USER_IN_APP} nor + * non-user actions from the system ({@link #ORIGIN_SYSTEM}). */ - public static final int UPDATE_ORIGIN_USER = 3; + public static final int ORIGIN_USER_IN_SYSTEMUI = 3; /** - * The {@link ZenModeConfig} is being "independently" updated by an app, and not as a result of - * a user's action inside that app (for example, activating an {@link AutomaticZenRule} based on - * a previously set schedule). + * The {@link ZenModeConfig} is updated by an app, but (probably) not as a result of a user + * action (for example, activating an {@link AutomaticZenRule} based on a previously set + * schedule). + * + * <p>Note that {@code ORIGIN_APP} is the only option for all public APIs except + * {@link NotificationManager#setAutomaticZenRuleState} -- apps cannot claim to be adding or + * updating a rule on behalf of the user. */ - public static final int UPDATE_ORIGIN_APP = 4; + public static final int ORIGIN_APP = 4; /** - * The {@link ZenModeConfig} is being updated by the System or SystemUI. Note that this only - * includes cases where the call is coming from the System/SystemUI but the change is not due to - * a user action (e.g. automatically activating a schedule-based rule). If the change is a - * result of a user action (e.g. activating a rule by tapping on its QS tile) then - * {@link #UPDATE_ORIGIN_USER} is used instead. + * The {@link ZenModeConfig} is updated by the System (or SystemUI). This only includes cases + * where the call is coming from the System/SystemUI but the change is not due to a user action + * (e.g. automatically activating a schedule-based rule, or some service toggling Do Not + * Disturb). See {@link #ORIGIN_USER_IN_SYSTEMUI}. */ - public static final int UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI = 5; + public static final int ORIGIN_SYSTEM = 5; /** * The {@link ZenModeConfig} is being updated (replaced) because the user's DND configuration * is being restored from a backup. */ - public static final int UPDATE_ORIGIN_RESTORE_BACKUP = 6; - - @IntDef(prefix = { "UPDATE_ORIGIN_" }, value = { - UPDATE_ORIGIN_UNKNOWN, - UPDATE_ORIGIN_INIT, - UPDATE_ORIGIN_INIT_USER, - UPDATE_ORIGIN_USER, - UPDATE_ORIGIN_APP, - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, - UPDATE_ORIGIN_RESTORE_BACKUP + public static final int ORIGIN_RESTORE_BACKUP = 6; + + /** + * The {@link ZenModeConfig} is updated from an app, and the app reports it's the result + * of a user action (e.g. tapping a button in the Wellbeing App to start Bedtime Mode). + * Corresponds to {@link NotificationManager#setAutomaticZenRuleState} with a + * {@link Condition#source} equal to {@link Condition#SOURCE_USER_ACTION}.</li> + */ + public static final int ORIGIN_USER_IN_APP = 7; + + @IntDef(prefix = { "ORIGIN_" }, value = { + ORIGIN_UNKNOWN, + ORIGIN_INIT, + ORIGIN_INIT_USER, + ORIGIN_USER_IN_SYSTEMUI, + ORIGIN_APP, + ORIGIN_SYSTEM, + ORIGIN_RESTORE_BACKUP, + ORIGIN_USER_IN_APP }) @Retention(RetentionPolicy.SOURCE) - public @interface ConfigChangeOrigin {} + public @interface ConfigOrigin {} public static final int SOURCE_ANYONE = Policy.PRIORITY_SENDERS_ANY; public static final int SOURCE_CONTACT = Policy.PRIORITY_SENDERS_CONTACTS; public static final int SOURCE_STAR = Policy.PRIORITY_SENDERS_STARRED; - public static final int MAX_SOURCE = SOURCE_STAR; + private static final int MAX_SOURCE = SOURCE_STAR; private static final int DEFAULT_SOURCE = SOURCE_STAR; private static final int DEFAULT_CALLS_SOURCE = SOURCE_STAR; @@ -439,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; } } @@ -942,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, @@ -1019,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))) { @@ -1043,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, "", STATE_TRUE); + } + } + return rt; + } } throw new IllegalStateException("Failed to reach END_DOCUMENT"); } @@ -1174,7 +1210,7 @@ public class ZenModeConfig implements Parcelable { } if (Flags.modesUi()) { rt.disabledOrigin = safeInt(parser, RULE_ATT_DISABLED_ORIGIN, - UPDATE_ORIGIN_UNKNOWN); + ORIGIN_UNKNOWN); rt.legacySuppressedEffects = safeInt(parser, RULE_ATT_LEGACY_SUPPRESSED_EFFECTS, 0); } @@ -2537,7 +2573,8 @@ public class ZenModeConfig implements Parcelable { @ZenDeviceEffects.ModifiableField public int zenDeviceEffectsUserModifiedFields; @Nullable public Instant deletionInstant; // Only set on deleted rules. @FlaggedApi(Flags.FLAG_MODES_UI) - @ConfigChangeOrigin public int disabledOrigin = UPDATE_ORIGIN_UNKNOWN; + @ConfigOrigin + public int disabledOrigin = ORIGIN_UNKNOWN; // The obsolete suppressed effects in NM.Policy (SCREEN_ON, SCREEN_OFF) cannot be put in a // ZenPolicy, so we store them here, only for the manual rule. @FlaggedApi(Flags.FLAG_MODES_UI) 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 6b354a0e232f..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,22 +135,28 @@ 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; } private void maybeUpdateCurrentConfig(int deviceId, int source, int axis) { if (mAxis != axis || mSource != source || mDeviceId != deviceId) { + mSource = source; + mAxis = axis; + mDeviceId = deviceId; + if (mDisabledIfViewPlaysScrollHaptics && (source == InputDevice.SOURCE_ROTARY_ENCODER) && mViewConfig.isViewBasedRotaryEncoderHapticScrollFeedbackEnabled()) { mHapticScrollFeedbackEnabled = false; return; } - mSource = source; - mAxis = axis; - mDeviceId = deviceId; mHapticScrollFeedbackEnabled = mViewConfig.isHapticScrollFeedbackEnabled(deviceId, axis, source); 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/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index 4ab67581a44e..b2810155098f 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -2887,6 +2887,11 @@ public final class InputMethodManager { * initiation delegation was previously requested using * {@link #prepareStylusHandwritingDelegation(View)} from the delegator. * + * <p>Otherwise, if the delegator view previously started delegation using {@link + * #startConnectionlessStylusHandwritingForDelegation(View, CursorAnchorInfo, Executor, + * ConnectionlessHandwritingCallback)}, requests the IME to commit the recognised handwritten + * text from the connectionless session to the delegate view. + * * <p>Note: If delegator and delegate are in different application packages, use * {@link #acceptStylusHandwritingDelegation(View, String)} instead.</p> * @@ -2895,14 +2900,9 @@ public final class InputMethodManager { * {@link #prepareStylusHandwritingDelegation(View)} and delegation is accepted * @see #prepareStylusHandwritingDelegation(View) * @see #acceptStylusHandwritingDelegation(View, String) + * @see #startConnectionlessStylusHandwritingForDelegation(View, CursorAnchorInfo, Executor, + * ConnectionlessHandwritingCallback) */ - // TODO(b/300979854): Once connectionless APIs are finalised, update documentation to add: - // <p>Otherwise, if the delegator view previously started delegation using {@link - // #startConnectionlessStylusHandwritingForDelegation(View, ResultReceiver, CursorAnchorInfo)}, - // requests the IME to commit the recognised handwritten text from the connectionless session to - // the delegate view. - // @see #startConnectionlessStylusHandwritingForDelegation(View, ResultReceiver, - // CursorAnchorInfo) public boolean acceptStylusHandwritingDelegation(@NonNull View delegateView) { return startStylusHandwritingInternal( delegateView, delegateView.getContext().getOpPackageName(), @@ -2915,6 +2915,11 @@ public final class InputMethodManager { * {@link #prepareStylusHandwritingDelegation(View, String)} from the delegator and the view * belongs to a specified delegate package. * + * <p>Otherwise, if the delegator view previously started delegation using {@link + * #startConnectionlessStylusHandwritingForDelegation(View, CursorAnchorInfo, String, Executor, + * ConnectionlessHandwritingCallback)}, requests the IME to commit the recognised handwritten + * text from the connectionless session to the delegate view. + * * <p>Note: If delegator and delegate are in the same application package, use * {@link #acceptStylusHandwritingDelegation(View)} instead.</p> * @@ -2924,15 +2929,10 @@ public final class InputMethodManager { * #prepareStylusHandwritingDelegation(View, String)} and delegation is accepted * @see #prepareStylusHandwritingDelegation(View, String) * @see #acceptStylusHandwritingDelegation(View) + * @see #startConnectionlessStylusHandwritingForDelegation(View, CursorAnchorInfo, String, + * Executor, ConnectionlessHandwritingCallback) * TODO (b/293640003): deprecate this method once flag is enabled. */ - // TODO(b/300979854): Once connectionless APIs are finalised, update documentation to add: - // <p>Otherwise, if the delegator view previously started delegation using {@link - // #startConnectionlessStylusHandwritingForDelegation(View, ResultReceiver, CursorAnchorInfo, - // String)}, requests the IME to commit the recognised handwritten text from the connectionless - // session to the delegate view. - // @see #startConnectionlessStylusHandwritingForDelegation(View, ResultReceiver, - // CursorAnchorInfo, String) public boolean acceptStylusHandwritingDelegation( @NonNull View delegateView, @NonNull String delegatorPackageName) { Objects.requireNonNull(delegatorPackageName); @@ -2946,6 +2946,11 @@ public final class InputMethodManager { * {@link #prepareStylusHandwritingDelegation(View, String)} from the delegator and the view * belongs to a specified delegate package. * + * <p>Otherwise, if the delegator view previously started delegation using {@link + * #startConnectionlessStylusHandwritingForDelegation(View, CursorAnchorInfo, String, Executor, + * ConnectionlessHandwritingCallback)}, requests the IME to commit the recognised handwritten + * text from the connectionless session to the delegate view. + * * @param delegateView delegate view capable of receiving input via {@link InputConnection} * on which {@link #startStylusHandwriting(View)} will be called. * @param delegatorPackageName package name of the delegator that handled initial stylus stroke. @@ -2957,6 +2962,8 @@ public final class InputMethodManager { * The framework only holds a weak reference. * @see #prepareStylusHandwritingDelegation(View, String) * @see #acceptStylusHandwritingDelegation(View) + * @see #startConnectionlessStylusHandwritingForDelegation(View, CursorAnchorInfo, String, + * Executor, ConnectionlessHandwritingCallback) */ @FlaggedApi(Flags.FLAG_USE_ZERO_JANK_PROXY) public void acceptStylusHandwritingDelegation( @@ -2977,6 +2984,11 @@ public final class InputMethodManager { * #prepareStylusHandwritingDelegation(View, String)} from the delegator and the view belongs to * a specified delegate package. * + * <p>Otherwise, if the delegator view previously started delegation using {@link + * #startConnectionlessStylusHandwritingForDelegation(View, CursorAnchorInfo, String, Executor, + * ConnectionlessHandwritingCallback)}, requests the IME to commit the recognised handwritten + * text from the connectionless session to the delegate view. + * * <p>Note: If delegator and delegate are in the same application package, use {@link * #acceptStylusHandwritingDelegation(View)} instead. * @@ -2988,15 +3000,9 @@ public final class InputMethodManager { * hold a reference to the callback. The framework only holds a weak reference. * @see #prepareStylusHandwritingDelegation(View, String) * @see #acceptStylusHandwritingDelegation(View) + * @see #startConnectionlessStylusHandwritingForDelegation(View, CursorAnchorInfo, String, + * Executor, ConnectionlessHandwritingCallback) */ - // TODO(b/300979854): Once connectionless APIs are finalised, update documentation to add: - // <p>Otherwise, if the delegator view previously started delegation using {@link - // #startConnectionlessStylusHandwritingForDelegation(View, ResultReceiver, CursorAnchorInfo, - // String)}, requests the IME to commit the recognised handwritten text from the connectionless - // session to the delegate view. - // @see #startConnectionlessStylusHandwritingForDelegation(View, ResultReceiver, - // CursorAnchorInfo, String) - // @FlaggedApi(FLAG_HOME_SCREEN_HANDWRITING_DELEGATOR) public void acceptStylusHandwritingDelegation( @NonNull View delegateView, @NonNull String delegatorPackageName, 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..a6ae948604a5 100644 --- a/core/java/android/window/flags/windowing_sdk.aconfig +++ b/core/java/android/window/flags/windowing_sdk.aconfig @@ -19,13 +19,6 @@ flag { flag { namespace: "windowing_sdk" - name: "fullscreen_dim_flag" - description: "Whether to allow showing fullscreen dim on ActivityEmbedding split" - bug: "293797706" -} - -flag { - namespace: "windowing_sdk" name: "activity_embedding_interactive_divider_flag" description: "Whether the interactive divider feature is enabled" bug: "293654166" diff --git a/core/java/com/android/internal/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/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/java/com/android/internal/widget/MessagingMessage.java b/core/java/com/android/internal/widget/MessagingMessage.java index ad90a63ab187..a59ee77cc693 100644 --- a/core/java/com/android/internal/widget/MessagingMessage.java +++ b/core/java/com/android/internal/widget/MessagingMessage.java @@ -105,7 +105,10 @@ public interface MessagingMessage extends MessagingLinearLayout.MessagingChild { } default void removeMessage(ArrayList<MessagingLinearLayout.MessagingChild> toRecycle) { - getGroup().removeMessage(this, toRecycle); + final MessagingGroup group = getGroup(); + if (group != null) { + group.removeMessage(this, toRecycle); + } } default void setMessagingGroup(MessagingGroup group) { @@ -132,7 +135,12 @@ public interface MessagingMessage extends MessagingLinearLayout.MessagingChild { @Override default void hideAnimated() { setIsHidingAnimated(true); - getGroup().performRemoveAnimation(getView(), () -> setIsHidingAnimated(false)); + final MessagingGroup group = getGroup(); + if (group != null) { + group.performRemoveAnimation(getView(), () -> setIsHidingAnimated(false)); + } else { + setIsHidingAnimated(false); + } } default boolean hasOverlappingRendering() { 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 5a4d6b14a52b..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,25 +17,35 @@ <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"> - <com.android.internal.widget.MaxHeightFrameLayout - android:layout_width="320dp" + <LinearLayout + android:layout_width="wrap_content" android:layout_height="0dp" android:layout_weight="1" - android:maxHeight="373dp"> + android:orientation="horizontal"> - <com.android.internal.widget.RecyclerView - android:id="@+id/list" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:paddingVertical="8dp" - android:clipToPadding="false" - android:layoutManager="com.android.internal.widget.LinearLayoutManager"/> + <!-- TODO(b/357644229): Enable shrinking width without three levels of nesting. --> + <com.android.internal.widget.MaxHeightFrameLayout + android:layout_width="320dp" + android:layout_height="match_parent" + android:layout_weight="1" + android:maxHeight="373dp"> + + <com.android.internal.widget.RecyclerView + android:id="@+id/list" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:paddingVertical="8dp" + android:clipToPadding="false" + android:layoutManager="com.android.internal.widget.LinearLayoutManager"/> - </com.android.internal.widget.MaxHeightFrameLayout> + </com.android.internal.widget.MaxHeightFrameLayout> + + </LinearLayout> <LinearLayout style="?android:attr/buttonBarStyle" @@ -51,7 +61,8 @@ <Space android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_weight="1"/> + android:layout_weight="1" + android:importantForAccessibility="no"/> <Button style="?attr/buttonBarButtonStyle" diff --git a/core/res/res/layout/input_method_switch_item_new.xml b/core/res/res/layout/input_method_switch_item_new.xml index 16a97c4b39ee..09ed65093b39 100644 --- a/core/res/res/layout/input_method_switch_item_new.xml +++ b/core/res/res/layout/input_method_switch_item_new.xml @@ -31,7 +31,8 @@ android:layout_marginTop="8dp" android:layout_marginEnd="24dp" android:layout_marginBottom="12dp" - android:visibility="gone"/> + android:visibility="gone" + android:importantForAccessibility="no"/> <TextView android:id="@+id/header_text" @@ -81,7 +82,8 @@ android:layout_marginStart="12dp" android:src="@drawable/ic_check_24dp" android:tint="?attr/materialColorOnSurface" - android:visibility="gone"/> + android:visibility="gone" + android:importantForAccessibility="no"/> </LinearLayout> 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/strings.xml b/core/res/res/values/strings.xml index 9104379cfbed..cb58339f5ef3 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -3277,6 +3277,9 @@ <string name="input_method_nav_back_button_desc">Back</string> <!-- Content description of the switch input method button for accessibility (not shown on the screen). [CHAR LIMIT=NONE] --> <string name="input_method_ime_switch_button_desc">Switch input method</string> + <!-- Accessibility text for the long click action on the switch input method button. This will + be used following "Double-tap and hold to..." [CHAR LIMIT=NONE] --> + <string name="input_method_ime_switch_long_click_action_desc">Open input method picker</string> <!-- If the device is getting low on internal storage, a notification is shown to the user. This is the title of that notification. --> <string name="low_internal_storage_view_title">Storage space running out</string> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 8f4018f38403..0d16e9c939d9 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -2245,6 +2245,7 @@ <java-symbol type="string" name="heavy_weight_notification_detail" /> <java-symbol type="string" name="image_wallpaper_component" /> <java-symbol type="string" name="input_method_binding_label" /> + <java-symbol type="string" name="input_method_ime_switch_long_click_action_desc" /> <java-symbol type="string" name="launch_warning_original" /> <java-symbol type="string" name="launch_warning_replace" /> <java-symbol type="string" name="launch_warning_title" /> @@ -5461,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 3dfeb7f0fc05..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() { @@ -428,9 +915,94 @@ public final class HapticScrollFeedbackProviderTest { assertFeedbackCount(mView, HapticFeedbackConstants.SCROLL_LIMIT, 1); } + @Test + public void testNonRotaryInputFeedbackNotBlockedByRotaryUnavailability() { + when(mMockViewConfig.isViewBasedRotaryEncoderHapticScrollFeedbackEnabled()) + .thenReturn(true); + setHapticScrollFeedbackEnabled(true); + setHapticScrollTickInterval(5); + mProvider = new HapticScrollFeedbackProvider(mView, mMockViewConfig, + /* disabledIfViewPlaysScrollHaptics= */ true); + + // Expect one feedback here. Touch input should provide feedback since scroll feedback has + // been enabled via `setHapticScrollFeedbackEnabled(true)`. + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_TOUCHSCREEN, MotionEvent.AXIS_Y, + /* deltaInPixels= */ 10); + // Because `isViewBasedRotaryEncoderHapticScrollFeedbackEnabled()` is false and + // `disabledIfViewPlaysScrollHaptics` is true, the scroll progress from rotary encoders will + // produce no feedback. + mProvider.onScrollProgress( + INPUT_DEVICE_2, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ 20); + // This event from the touch screen should produce feedback. The rotary encoder event's + // inability to not play scroll feedback should not impact this touch input. + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_TOUCHSCREEN, MotionEvent.AXIS_Y, + /* deltaInPixels= */ 30); + + 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); } } @@ -440,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); } } @@ -460,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); @@ -475,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/data/keyboards/Vendor_054c_Product_05c4.idc b/data/keyboards/Vendor_054c_Product_05c4.idc index 2da622745baf..45b5207c6730 100644 --- a/data/keyboards/Vendor_054c_Product_05c4.idc +++ b/data/keyboards/Vendor_054c_Product_05c4.idc @@ -51,7 +51,7 @@ sensor.gyroscope.power = 0.8 # fingers, it prevents tapping to click because it thinks the finger's moving # too fast. # -# Since this touchpad doesn't seem to have to drumroll issues, we can safely +# Since this touchpad doesn't seem to have drumroll issues, we can safely # disable drumroll detection. gestureProp.Drumroll_Suppression_Enable = 0 @@ -60,3 +60,11 @@ gestureProp.Drumroll_Suppression_Enable = 0 # from the palm classifier to increase the usable area of the pad. gestureProp.Palm_Edge_Zone_Width = 0 gestureProp.Tap_Exclusion_Border_Width = 0 + +# Touchpad is small, scale up the pointer movements to make it more practical +# to use. +gestureProp.Point_X_Out_Scale = 2.5 +gestureProp.Point_Y_Out_Scale = 2.5 + +# TODO(b/351326684): Ideally "Scroll X Out Scale" and "Scroll Y Out Scale" +# should be adjusted as well. Currently not supported in IDC files. diff --git a/data/keyboards/Vendor_054c_Product_09cc.idc b/data/keyboards/Vendor_054c_Product_09cc.idc index 2a1a4fc62b24..45b5207c6730 100644 --- a/data/keyboards/Vendor_054c_Product_09cc.idc +++ b/data/keyboards/Vendor_054c_Product_09cc.idc @@ -60,3 +60,11 @@ gestureProp.Drumroll_Suppression_Enable = 0 # from the palm classifier to increase the usable area of the pad. gestureProp.Palm_Edge_Zone_Width = 0 gestureProp.Tap_Exclusion_Border_Width = 0 + +# Touchpad is small, scale up the pointer movements to make it more practical +# to use. +gestureProp.Point_X_Out_Scale = 2.5 +gestureProp.Point_Y_Out_Scale = 2.5 + +# TODO(b/351326684): Ideally "Scroll X Out Scale" and "Scroll Y Out Scale" +# should be adjusted as well. Currently not supported in IDC files. diff --git a/data/keyboards/Vendor_054c_Product_0ce6.idc b/data/keyboards/Vendor_054c_Product_0ce6.idc new file mode 100644 index 000000000000..48027e715649 --- /dev/null +++ b/data/keyboards/Vendor_054c_Product_0ce6.idc @@ -0,0 +1,31 @@ +# 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. + +# +# Sony Playstation(R) DualSense 5 Controller +# + +## Touchpad ## + +# Because of the way this touchpad is positioned, touches around the edges are +# no more likely to be palms than ones in the middle, so remove the edge zones +# from the palm classifier to increase the usable area of the pad. +gestureProp.Palm_Edge_Zone_Width = 0 +gestureProp.Tap_Exclusion_Border_Width = 0 + +gestureProp.Point_X_Out_Scale = 2.0 +gestureProp.Point_Y_Out_Scale = 2.0 + +# TODO(b/351326684): Ideally "Scroll X Out Scale" and "Scroll Y Out Scale" +# should be adjusted as well. Currently not supported in IDC files. diff --git a/data/keyboards/Vendor_054c_Product_0df2.idc b/data/keyboards/Vendor_054c_Product_0df2.idc new file mode 100644 index 000000000000..4bcf0bef458a --- /dev/null +++ b/data/keyboards/Vendor_054c_Product_0df2.idc @@ -0,0 +1,31 @@ +# 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. + +# +# Sony Playstation(R) DualSense Edge 5 Controller +# + +## Touchpad ## + +# Because of the way this touchpad is positioned, touches around the edges are +# no more likely to be palms than ones in the middle, so remove the edge zones +# from the palm classifier to increase the usable area of the pad. +gestureProp.Palm_Edge_Zone_Width = 0 +gestureProp.Tap_Exclusion_Border_Width = 0 + +gestureProp.Point_X_Out_Scale = 2.0 +gestureProp.Point_Y_Out_Scale = 2.0 + +# TODO(b/351326684): Ideally "Scroll X Out Scale" and "Scroll Y Out Scale" +# should be adjusted as well. Currently not supported in IDC files. 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..25e710784a8f 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -2725,15 +2725,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/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/OWNERS b/libs/WindowManager/Shell/OWNERS index cb422eab372b..2e19d52fb4bb 100644 --- a/libs/WindowManager/Shell/OWNERS +++ b/libs/WindowManager/Shell/OWNERS @@ -1,5 +1,5 @@ xutan@google.com # Give submodule owners in shell resource approval -per-file res*/*/*.xml = atsjenk@google.com, hwwang@google.com, jorgegil@google.com, lbill@google.com, madym@google.com, vaniadesmonda@google.com, pbdr@google.com, tkachenkoi@google.com, mpodolian@google.com, liranb@google.com +per-file res*/*/*.xml = atsjenk@google.com, hwwang@google.com, jorgegil@google.com, lbill@google.com, madym@google.com, vaniadesmonda@google.com, pbdr@google.com, tkachenkoi@google.com, mpodolian@google.com, liranb@google.com, pragyabajoria@google.com, uysalorhan@google.com, gsennton@google.com per-file res*/*/tv_*.xml = bronger@google.com 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/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml index 0a8166fb1a8d..df5f1e46a03c 100644 --- a/libs/WindowManager/Shell/res/values/strings.xml +++ b/libs/WindowManager/Shell/res/values/strings.xml @@ -139,6 +139,10 @@ <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> <!-- 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/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java index f14f4198c3f2..7275c6494140 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java @@ -16,9 +16,14 @@ package com.android.wm.shell.back; +import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.view.RemoteAnimationTarget.MODE_CLOSING; import static android.view.RemoteAnimationTarget.MODE_OPENING; +import static android.view.WindowManager.TRANSIT_CLOSE_PREPARE_BACK_NAVIGATION; import static android.window.TransitionInfo.FLAG_BACK_GESTURE_ANIMATED; +import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; +import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP; +import static android.window.TransitionInfo.FLAG_SHOW_WALLPAPER; import static com.android.internal.jank.InteractionJankMonitor.CUJ_PREDICTIVE_BACK_HOME; import static com.android.window.flags.Flags.migratePredictiveBackTransition; @@ -31,6 +36,8 @@ import android.annotation.Nullable; import android.annotation.SuppressLint; import android.app.ActivityTaskManager; import android.app.IActivityTaskManager; +import android.app.TaskInfo; +import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; import android.content.res.Configuration; @@ -837,8 +844,9 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont mShellExecutor.executeDelayed(mAnimationTimeoutRunnable, MAX_ANIMATION_DURATION); // The next callback should be {@link #onBackAnimationFinished}. + final boolean migrateBackToTransition = migratePredictiveBackTransition(); if (mCurrentTracker.getTriggerBack()) { - if (migratePredictiveBackTransition()) { + if (migrateBackToTransition) { // notify core gesture is commit if (shouldTriggerCloseTransition()) { mBackTransitionHandler.mCloseTransitionRequested = true; @@ -856,6 +864,10 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont // start post animation dispatchOnBackInvoked(mActiveCallback); } else { + if (migrateBackToTransition + && mBackTransitionHandler.mPrepareOpenTransition != null) { + mBackTransitionHandler.createClosePrepareTransition(); + } tryDispatchOnBackCancelled(mActiveCallback); } } @@ -960,6 +972,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont mShellBackAnimationRegistry.resetDefaultCrossActivity(); cancelLatencyTracking(); mReceivedNullNavigationInfo = false; + mBackTransitionHandler.mLastTrigger = triggerBack; if (mBackNavigationInfo != null) { mPreviousNavigationType = mBackNavigationInfo.getType(); mBackNavigationInfo.onBackNavigationFinished(triggerBack); @@ -1128,12 +1141,18 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont Runnable mOnAnimationFinishCallback; boolean mCloseTransitionRequested; - boolean mOpeningRunning; SurfaceControl.Transaction mFinishOpenTransaction; Transitions.TransitionFinishCallback mFinishOpenTransitionCallback; QueuedTransition mQueuedTransition = null; + boolean mLastTrigger; + // The Transition to make behindActivity become visible + IBinder mPrepareOpenTransition; + // The Transition to make behindActivity become invisible, if prepare open exist and + // animation is canceled, start a close prepare transition to finish the whole transition. + IBinder mClosePrepareTransition; + TransitionInfo mOpenTransitionInfo; void onAnimationFinished() { - if (!mCloseTransitionRequested) { + if (!mCloseTransitionRequested && mClosePrepareTransition == null) { applyFinishOpenTransition(); } if (mOnAnimationFinishCallback != null) { @@ -1158,7 +1177,8 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont mFinishOpenTransitionCallback.onTransitionFinished(null); mFinishOpenTransitionCallback = null; } - mOpeningRunning = false; + mOpenTransitionInfo = null; + mPrepareOpenTransition = null; } private void applyAndFinish(@NonNull SurfaceControl.Transaction st, @@ -1178,21 +1198,42 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont @NonNull Transitions.TransitionFinishCallback finishCallback) { // Both mShellExecutor and Transitions#mMainExecutor are ShellMainThread, so we don't // need to post to ShellExecutor when called. + if (info.getType() == WindowManager.TRANSIT_CLOSE_PREPARE_BACK_NAVIGATION) { + // only consume it if this transition hasn't being processed. + if (mClosePrepareTransition != null) { + mClosePrepareTransition = null; + applyAndFinish(st, ft, finishCallback); + return true; + } + return false; + } + if (info.getType() != WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION && !isGestureBackTransition(info)) { return false; } + + if (shouldCancelAnimation(info)) { + return false; + } + if (mApps == null || mApps.length == 0) { if (mBackNavigationInfo != null && mShellBackAnimationRegistry .isWaitingAnimation(mBackNavigationInfo.getType())) { // Waiting for animation? Queue update to wait for animation start. consumeQueuedTransitionIfNeeded(); mQueuedTransition = new QueuedTransition(info, st, ft, finishCallback); - } else { + return true; + } else if (mLastTrigger) { // animation was done, consume directly applyAndFinish(st, ft, finishCallback); + return true; + } else { + // animation was cancelled but transition haven't happen, we must handle it + if (mClosePrepareTransition == null && mCurrentTracker.isFinished()) { + createClosePrepareTransition(); + } } - return true; } if (handlePrepareTransition(info, st, ft, finishCallback)) { @@ -1201,12 +1242,131 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont return handleCloseTransition(info, st, ft, finishCallback); } + void createClosePrepareTransition() { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.restoreBackNavi(); + mClosePrepareTransition = mTransitions.startTransition( + TRANSIT_CLOSE_PREPARE_BACK_NAVIGATION, wct, mBackTransitionHandler); + } + private void mergePendingTransitions(TransitionInfo info) { + if (mOpenTransitionInfo == null) { + return; + } + // Copy initial changes to final transition + final TransitionInfo init = mOpenTransitionInfo; + // find prepare open target + boolean openShowWallpaper = false; + ComponentName openComponent = null; + int tmpSize; + int openTaskId = INVALID_TASK_ID; + for (int j = init.getChanges().size() - 1; j >= 0; --j) { + final TransitionInfo.Change change = init.getChanges().get(j); + if (change.hasFlags(FLAG_BACK_GESTURE_ANIMATED)) { + openComponent = findComponentName(change); + openTaskId = findTaskId(change); + if (change.hasFlags(FLAG_SHOW_WALLPAPER)) { + openShowWallpaper = true; + } + break; + } + } + if (openComponent == null && openTaskId == INVALID_TASK_ID) { + // shouldn't happen. + return; + } + // find first non-prepare open target + boolean isOpen = false; + tmpSize = info.getChanges().size(); + for (int j = 0; j < tmpSize; ++j) { + final TransitionInfo.Change change = info.getChanges().get(j); + final ComponentName firstNonOpen = findComponentName(change); + final int firstTaskId = findTaskId(change); + if ((firstNonOpen != null && firstNonOpen != openComponent) + || (firstTaskId != INVALID_TASK_ID && firstTaskId != openTaskId)) { + // this is original close target, potential be close, but cannot determine from + // it + if (change.hasFlags(FLAG_BACK_GESTURE_ANIMATED)) { + isOpen = !TransitionUtil.isClosingMode(change.getMode()); + } else { + isOpen = TransitionUtil.isOpeningMode(change.getMode()); + break; + } + } + } + + if (!isOpen) { + // Close transition, the transition info should be: + // init info(open A & wallpaper) + // current info(close B target) + // remove init info(open/change A target & wallpaper) + boolean moveToTop = false; + for (int j = info.getChanges().size() - 1; j >= 0; --j) { + final TransitionInfo.Change change = info.getChanges().get(j); + if (isSameChangeTarget(openComponent, openTaskId, change)) { + moveToTop = change.hasFlags(FLAG_MOVED_TO_TOP); + info.getChanges().remove(j); + } else if ((openShowWallpaper && change.hasFlags(FLAG_IS_WALLPAPER)) + || !change.hasFlags(FLAG_BACK_GESTURE_ANIMATED)) { + info.getChanges().remove(j); + } + } + tmpSize = info.getChanges().size(); + for (int i = 0; i < tmpSize; ++i) { + final TransitionInfo.Change change = init.getChanges().get(i); + if (moveToTop) { + if (isSameChangeTarget(openComponent, openTaskId, change)) { + change.setFlags(change.getFlags() | FLAG_MOVED_TO_TOP); + } + } + info.getChanges().add(i, change); + } + } else { + // Open transition, the transition info should be: + // init info(open A & wallpaper) + // current info(open C target + close B target + close A & wallpaper) + + // If close target isn't back navigated, filter out close A & wallpaper because the + // (open C + close B) pair didn't participant prepare close + boolean nonBackOpen = false; + boolean nonBackClose = false; + tmpSize = info.getChanges().size(); + for (int j = 0; j < tmpSize; ++j) { + final TransitionInfo.Change change = info.getChanges().get(j); + if (!change.hasFlags(FLAG_BACK_GESTURE_ANIMATED) + && canBeTransitionTarget(change)) { + final int mode = change.getMode(); + nonBackOpen |= TransitionUtil.isOpeningMode(mode); + nonBackClose |= TransitionUtil.isClosingMode(mode); + } + } + if (nonBackClose && nonBackOpen) { + for (int j = info.getChanges().size() - 1; j >= 0; --j) { + final TransitionInfo.Change change = info.getChanges().get(j); + if (isSameChangeTarget(openComponent, openTaskId, change)) { + info.getChanges().remove(j); + } else if ((openShowWallpaper && change.hasFlags(FLAG_IS_WALLPAPER))) { + info.getChanges().remove(j); + } + } + } + } + ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Back animation transition, merge pending " + + "transitions result=%s", info); + } + @Override public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback) { - if (!isGestureBackTransition(info)) { - if (mOpeningRunning) { + if (mClosePrepareTransition == transition) { + mClosePrepareTransition = null; + } + // try to handle unexpected transition + mergePendingTransitions(info); + + if (!isGestureBackTransition(info) || shouldCancelAnimation(info) + || !mCloseTransitionRequested) { + if (mPrepareOpenTransition != null) { applyFinishOpenTransition(); } if (mQueuedTransition != null) { @@ -1222,7 +1382,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont // animation was done applyFinishOpenTransition(); mCloseTransitionRequested = false; - } // else, let queued transition to play + } // let queued transition finish. } else { // we are animating, wait until animation finish mOnAnimationFinishCallback = () -> { @@ -1233,6 +1393,56 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } } + // Cancel close animation if something happen unexpected, let another handler to handle + private boolean shouldCancelAnimation(@NonNull TransitionInfo info) { + final boolean noCloseAllowed = + info.getType() == WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION; + boolean unableToHandle = false; + boolean filterTargets = false; + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change c = info.getChanges().get(i); + final boolean backGestureAnimated = c.hasFlags(FLAG_BACK_GESTURE_ANIMATED); + if (!backGestureAnimated && !c.hasFlags(FLAG_IS_WALLPAPER)) { + // something we cannot handle? + unableToHandle = true; + filterTargets = true; + } else if (noCloseAllowed && backGestureAnimated + && TransitionUtil.isClosingMode(c.getMode())) { + // Prepare back navigation shouldn't contain close change, unless top app + // request close. + unableToHandle = true; + } + } + if (!unableToHandle) { + return false; + } + if (!filterTargets) { + return true; + } + if (TransitionUtil.isOpeningType(info.getType()) + || TransitionUtil.isClosingType(info.getType())) { + boolean removeWallpaper = false; + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change c = info.getChanges().get(i); + // filter out opening target, keep original closing target in this transition + if (c.hasFlags(FLAG_BACK_GESTURE_ANIMATED) + && TransitionUtil.isOpeningMode(c.getMode())) { + info.getChanges().remove(i); + removeWallpaper |= c.hasFlags(FLAG_SHOW_WALLPAPER); + } + } + if (removeWallpaper) { + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change c = info.getChanges().get(i); + if (c.hasFlags(FLAG_IS_WALLPAPER)) { + info.getChanges().remove(i); + } + } + } + } + return true; + } + /** * Check whether this transition is prepare for predictive back animation, which could * happen when core make an activity become visible. @@ -1247,9 +1457,11 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } SurfaceControl openingLeash = null; - for (int i = mApps.length - 1; i >= 0; --i) { - if (mApps[i].mode == MODE_OPENING) { - openingLeash = mApps[i].leash; + if (mApps != null) { + for (int i = mApps.length - 1; i >= 0; --i) { + if (mApps[i].mode == MODE_OPENING) { + openingLeash = mApps[i].leash; + } } } if (openingLeash != null) { @@ -1259,13 +1471,14 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont final Point offset = c.getEndRelOffset(); st.setPosition(c.getLeash(), offset.x, offset.y); st.reparent(c.getLeash(), openingLeash); + st.setAlpha(c.getLeash(), 1.0f); } } } st.apply(); mFinishOpenTransaction = ft; mFinishOpenTransitionCallback = finishCallback; - mOpeningRunning = true; + mOpenTransitionInfo = info; return true; } @@ -1288,6 +1501,10 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont @NonNull SurfaceControl.Transaction st, @NonNull SurfaceControl.Transaction ft, @NonNull Transitions.TransitionFinishCallback finishCallback) { + if (info.getType() == WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION + || !mCloseTransitionRequested) { + return false; + } SurfaceControl openingLeash = null; SurfaceControl closingLeash = null; for (int i = mApps.length - 1; i >= 0; --i) { @@ -1325,7 +1542,12 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont public WindowContainerTransaction handleRequest( @NonNull IBinder transition, @NonNull TransitionRequestInfo request) { - if (request.getType() == WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION) { + final int type = request.getType(); + if (type == WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION) { + mPrepareOpenTransition = transition; + return new WindowContainerTransaction(); + } + if (type == WindowManager.TRANSIT_CLOSE_PREPARE_BACK_NAVIGATION) { return new WindowContainerTransaction(); } if (TransitionUtil.isClosingType(request.getType()) && mCloseTransitionRequested) { @@ -1369,4 +1591,36 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } } } + + private static ComponentName findComponentName(TransitionInfo.Change change) { + final ComponentName componentName = change.getActivityComponent(); + if (componentName != null) { + return componentName; + } + final TaskInfo taskInfo = change.getTaskInfo(); + if (taskInfo != null) { + return taskInfo.topActivity; + } + return null; + } + + private static int findTaskId(TransitionInfo.Change change) { + final TaskInfo taskInfo = change.getTaskInfo(); + if (taskInfo != null) { + return taskInfo.taskId; + } + return INVALID_TASK_ID; + } + + private static boolean isSameChangeTarget(ComponentName topActivity, int taskId, + TransitionInfo.Change change) { + final ComponentName openChange = findComponentName(change); + final int firstTaskId = findTaskId(change); + return (openChange != null && openChange == topActivity) + || (firstTaskId != INVALID_TASK_ID && firstTaskId == taskId); + } + + private static boolean canBeTransitionTarget(TransitionInfo.Change change) { + return findComponentName(change) != null || findTaskId(change) != INVALID_TASK_ID; + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt index 4e0c82b9628f..c7e8df980a8b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt @@ -170,7 +170,7 @@ abstract class CrossActivityBackAnimation( initialTouchPos.set(backMotionEvent.touchX, backMotionEvent.touchY) transaction.setAnimationTransaction() - isLetterboxed = closingTarget!!.taskInfo.appCompatTaskInfo.topActivityBoundsLetterboxed + isLetterboxed = closingTarget!!.taskInfo.appCompatTaskInfo.isTopActivityLetterboxed enteringHasSameLetterbox = isLetterboxed && closingTarget!!.localBounds.equals(enteringTarget!!.localBounds) 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 434b512a585c..b6da761b0f9c 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 @@ -174,6 +174,7 @@ public class BubbleData { BubbleBarUpdate getInitialState() { BubbleBarUpdate bubbleBarUpdate = BubbleBarUpdate.createInitialState(); bubbleBarUpdate.shouldShowEducation = shouldShowEducation; + bubbleBarUpdate.showOverflow = !overflowBubbles.isEmpty(); for (int i = 0; i < bubbles.size(); i++) { bubbleBarUpdate.currentBubbleList.add(bubbles.get(i).asBubbleBarBubble()); } 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 8f8b77b3cf01..efa1031bf814 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 @@ -2004,6 +2004,7 @@ public class BubbleStackView extends FrameLayout // and then remove our views (removing the icon view triggers the removal of the // bubble window so do that at the end of the animation so we see the scrim animate). BadgedImageView iconView = bubble.getIconView(); + final BubbleViewProvider expandedBubbleBeforeScrim = mExpandedBubble; showScrim(false, () -> { mRemovingLastBubbleWhileExpanded = false; bubble.cleanupExpandedView(); @@ -2012,7 +2013,17 @@ public class BubbleStackView extends FrameLayout } bubble.cleanupViews(); // cleans up the icon view updateExpandedView(); // resets state for no expanded bubble - mExpandedBubble = null; + // Bubble keys may not have changed if we receive an update to the same bubble. + // Compare bubble object instances to see if the expanded bubble has changed. + if (expandedBubbleBeforeScrim == mExpandedBubble) { + // Only clear expanded bubble if it has not changed since the scrim animation + // started. + // Scrim animation can take some time run and it is possible for a new bubble + // to be added while the animation is running. This causes the expanded + // bubble to change. Make sure we only clear the expanded bubble if it did + // not change between when the scrim animation started and completed. + mExpandedBubble = null; + } }); logBubbleEvent(bubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__DISMISSED); return; 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..b7834dbcf07f 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 @@ -32,6 +32,7 @@ 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 com.android.wm.shell.R; @@ -188,12 +189,30 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView // Handle view needs to draw on top of task view. bringChildToFront(mHandleView); + + mHandleView.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_expand_menu))); + } + }); } 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 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 c02c9cf3fd72..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); } @@ -238,7 +243,7 @@ public class CompatUIController implements OnDisplaysChangedListener, public void onCompatInfoChanged(@NonNull CompatUIInfo compatUIInfo) { final TaskInfo taskInfo = compatUIInfo.getTaskInfo(); final ShellTaskOrganizer.TaskListener taskListener = compatUIInfo.getListener(); - if (taskInfo != null && !taskInfo.appCompatTaskInfo.topActivityInSizeCompat) { + if (taskInfo != null && !taskInfo.appCompatTaskInfo.isTopActivityInSizeCompat()) { mSetOfTaskIdsShowingRestartDialog.remove(taskInfo.taskId); } @@ -256,16 +261,16 @@ public class CompatUIController implements OnDisplaysChangedListener, // basically cancel all the onboarding flow. We don't have to ignore events in case // the app is in size compat mode. if (mIsFirstReachabilityEducationRunning) { - if (!taskInfo.appCompatTaskInfo.isFromLetterboxDoubleTap - && !taskInfo.appCompatTaskInfo.topActivityInSizeCompat) { + if (!taskInfo.appCompatTaskInfo.isFromLetterboxDoubleTap() + && !taskInfo.appCompatTaskInfo.isTopActivityInSizeCompat()) { return; } mIsFirstReachabilityEducationRunning = false; } - if (taskInfo.appCompatTaskInfo.topActivityBoundsLetterboxed) { - if (taskInfo.appCompatTaskInfo.isLetterboxEducationEnabled) { + if (taskInfo.appCompatTaskInfo.isTopActivityLetterboxed()) { + if (taskInfo.appCompatTaskInfo.isLetterboxEducationEnabled()) { createOrUpdateLetterboxEduLayout(taskInfo, taskListener); - } else if (!taskInfo.appCompatTaskInfo.isFromLetterboxDoubleTap) { + } else if (!taskInfo.appCompatTaskInfo.isFromLetterboxDoubleTap()) { // In this case the app is letterboxed and the letterbox education // is disabled. In this case we need to understand if it's the first // time we show the reachability education. When this is happening @@ -282,7 +287,7 @@ public class CompatUIController implements OnDisplaysChangedListener, // We activate the first reachability education if the double-tap is enabled. // If the double tap is not enabled (e.g. thin letterbox) we just set the value // of the education being seen. - if (taskInfo.appCompatTaskInfo.isLetterboxDoubleTapEnabled) { + if (taskInfo.appCompatTaskInfo.isLetterboxDoubleTapEnabled()) { mIsFirstReachabilityEducationRunning = true; createOrUpdateReachabilityEduLayout(taskInfo, taskListener); return; @@ -293,7 +298,7 @@ public class CompatUIController implements OnDisplaysChangedListener, createOrUpdateCompatLayout(taskInfo, taskListener); createOrUpdateRestartDialogLayout(taskInfo, taskListener); if (mCompatUIConfiguration.getHasSeenLetterboxEducation(taskInfo.userId)) { - if (taskInfo.appCompatTaskInfo.isLetterboxDoubleTapEnabled) { + if (taskInfo.appCompatTaskInfo.isLetterboxDoubleTapEnabled()) { createOrUpdateReachabilityEduLayout(taskInfo, taskListener); } // The user aspect ratio button should not be handled when a new TaskInfo is @@ -305,7 +310,7 @@ public class CompatUIController implements OnDisplaysChangedListener, } return; } - if (!taskInfo.appCompatTaskInfo.isFromLetterboxDoubleTap) { + if (!taskInfo.appCompatTaskInfo.isFromLetterboxDoubleTap()) { createOrUpdateUserAspectRatioSettingsLayout(taskInfo, taskListener); } } @@ -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/CompatUIWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java index 271c07d4011d..8ce7837e451f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java @@ -82,7 +82,7 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract { onRestartButtonClicked) { super(context, taskInfo, syncQueue, taskListener, displayLayout); mCallback = callback; - mHasSizeCompat = taskInfo.appCompatTaskInfo.topActivityInSizeCompat; + mHasSizeCompat = taskInfo.appCompatTaskInfo.isTopActivityInSizeCompat(); if (DESKTOP_WINDOWING_MODE.isEnabled(mContext) && DesktopModeFlags.DYNAMIC_INITIAL_BOUNDS.isEnabled(context)) { // Don't show the SCM button for freeform tasks @@ -138,7 +138,7 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract { public boolean updateCompatInfo(TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener, boolean canShow) { final boolean prevHasSizeCompat = mHasSizeCompat; - mHasSizeCompat = taskInfo.appCompatTaskInfo.topActivityInSizeCompat; + mHasSizeCompat = taskInfo.appCompatTaskInfo.isTopActivityInSizeCompat(); if (DESKTOP_WINDOWING_MODE.isEnabled(mContext) && DesktopModeFlags.DYNAMIC_INITIAL_BOUNDS.isEnabled(mContext)) { // Don't show the SCM button for freeform tasks 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 623feada0172..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,8 +109,9 @@ class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract { R.dimen.letterbox_education_dialog_margin); mDockStateReader = dockStateReader; mCompatUIConfiguration = compatUIConfiguration; + mCompatUIStatusManager = compatUIStatusManager; mEligibleForLetterboxEducation = - taskInfo.appCompatTaskInfo.topActivityEligibleForLetterboxEducation; + taskInfo.appCompatTaskInfo.eligibleForLetterboxEducation(); } @Override @@ -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,14 +206,14 @@ class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract { @Override public void release() { mAnimationController.cancelAnimation(); + mCompatUIStatusManager.onEducationHidden(); super.release(); } @Override public boolean updateCompatInfo(TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener, boolean canShow) { - mEligibleForLetterboxEducation = - taskInfo.appCompatTaskInfo.topActivityEligibleForLetterboxEducation; + mEligibleForLetterboxEducation = taskInfo.appCompatTaskInfo.eligibleForLetterboxEducation(); return super.updateCompatInfo(taskInfo, taskListener, canShow); } 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/ReachabilityEduWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduWindowManager.java index 07082a558744..06f2dd1a3b17 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduWindowManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduWindowManager.java @@ -91,7 +91,7 @@ class ReachabilityEduWindowManager extends CompatUIWindowManagerAbstract { Function<Integer, Integer> disappearTimeSupplier) { super(context, taskInfo, syncQueue, taskListener, displayLayout); final AppCompatTaskInfo appCompatTaskInfo = taskInfo.appCompatTaskInfo; - mIsLetterboxDoubleTapEnabled = appCompatTaskInfo.isLetterboxDoubleTapEnabled; + mIsLetterboxDoubleTapEnabled = appCompatTaskInfo.isLetterboxDoubleTapEnabled(); mLetterboxVerticalPosition = appCompatTaskInfo.topActivityLetterboxVerticalPosition; mLetterboxHorizontalPosition = appCompatTaskInfo.topActivityLetterboxHorizontalPosition; mTopActivityLetterboxWidth = appCompatTaskInfo.topActivityLetterboxWidth; @@ -148,12 +148,12 @@ class ReachabilityEduWindowManager extends CompatUIWindowManagerAbstract { final int prevTopActivityLetterboxWidth = mTopActivityLetterboxWidth; final int prevTopActivityLetterboxHeight = mTopActivityLetterboxHeight; final AppCompatTaskInfo appCompatTaskInfo = taskInfo.appCompatTaskInfo; - mIsLetterboxDoubleTapEnabled = appCompatTaskInfo.isLetterboxDoubleTapEnabled; + mIsLetterboxDoubleTapEnabled = appCompatTaskInfo.isLetterboxDoubleTapEnabled(); mLetterboxVerticalPosition = appCompatTaskInfo.topActivityLetterboxVerticalPosition; mLetterboxHorizontalPosition = appCompatTaskInfo.topActivityLetterboxHorizontalPosition; mTopActivityLetterboxWidth = appCompatTaskInfo.topActivityLetterboxWidth; mTopActivityLetterboxHeight = appCompatTaskInfo.topActivityLetterboxHeight; - mHasUserDoubleTapped = appCompatTaskInfo.isFromLetterboxDoubleTap; + mHasUserDoubleTapped = appCompatTaskInfo.isFromLetterboxDoubleTap(); if (!super.updateCompatInfo(taskInfo, taskListener, canShow)) { return false; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManager.java index 8fb4bdbea933..3f67172ca636 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManager.java @@ -238,14 +238,14 @@ class UserAspectRatioSettingsWindowManager extends CompatUIWindowManagerAbstract // App is not visibly letterboxed if it covers status bar/bottom insets or matches the // stable bounds, so don't show the button if (stableBounds.height() <= letterboxHeight && stableBounds.width() <= letterboxWidth - && !taskInfo.isUserFullscreenOverrideEnabled) { + && !taskInfo.isUserFullscreenOverrideEnabled()) { return false; } - return taskInfo.topActivityEligibleForUserAspectRatioButton - && (taskInfo.topActivityBoundsLetterboxed - || taskInfo.isUserFullscreenOverrideEnabled) - && !taskInfo.isSystemFullscreenOverrideEnabled + return taskInfo.eligibleForUserAspectRatioButton() + && (taskInfo.isTopActivityLetterboxed() + || taskInfo.isUserFullscreenOverrideEnabled()) + && !taskInfo.isSystemFullscreenOverrideEnabled() && Intent.ACTION_MAIN.equals(intent.getAction()) && intent.hasCategory(Intent.CATEGORY_LAUNCHER) && (!mUserAspectRatioButtonShownChecker.get() || isShowingButton()); 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/DesktopModeUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt index 026094cd6f2a..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) } } @@ -146,7 +162,7 @@ fun maximizeSizeGivenAspectRatio( fun calculateAspectRatio(taskInfo: RunningTaskInfo): Float { val appLetterboxWidth = taskInfo.appCompatTaskInfo.topActivityLetterboxAppWidth val appLetterboxHeight = taskInfo.appCompatTaskInfo.topActivityLetterboxAppHeight - if (taskInfo.appCompatTaskInfo.topActivityBoundsLetterboxed) { + if (taskInfo.appCompatTaskInfo.isTopActivityLetterboxed) { return maxOf(appLetterboxWidth, appLetterboxHeight) / minOf(appLetterboxWidth, appLetterboxHeight).toFloat() } @@ -200,7 +216,7 @@ fun TaskInfo.hasPortraitTopActivity(): Boolean { } // Then check if the activity is portrait when letterboxed - appCompatTaskInfo.topActivityBoundsLetterboxed -> appCompatTaskInfo.isTopActivityPillarboxed + appCompatTaskInfo.isTopActivityLetterboxed -> appCompatTaskInfo.isTopActivityPillarboxed // Then check if the activity is portrait appBounds != null -> appBounds.height() > appBounds.width() @@ -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/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/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/OWNERS index b01b2b7ad520..afdda8ff865e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/OWNERS +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/OWNERS @@ -5,3 +5,6 @@ madym@google.com pbdr@google.com tkachenkoi@google.com vaniadesmonda@google.com +pragyabajoria@google.com +uysalorhan@google.com +gsennton@google.com 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/freeform/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/OWNERS index 8a0eea0a9bdd..93351c3f5f86 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/OWNERS +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/OWNERS @@ -5,3 +5,7 @@ madym@google.com nmusgrave@google.com pbdr@google.com tkachenkoi@google.com +vaniadesmonda@google.com +pragyabajoria@google.com +uysalorhan@google.com +gsennton@google.com diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java index 333c75f92ffc..abec3b9c0c3b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java @@ -170,7 +170,7 @@ public class KeyguardTransitionHandler @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull TransitionFinishCallback finishCallback) { - if (!handles(info) || mIsLaunchingActivityOverLockscreen) { + if (!handles(info)) { return false; } @@ -185,6 +185,9 @@ public class KeyguardTransitionHandler transition, info, startTransaction, finishTransaction, finishCallback); } + if (mIsLaunchingActivityOverLockscreen) { + return false; + } // Occlude/unocclude animations are only played if the keyguard is locked. if ((info.getFlags() & TRANSIT_FLAG_KEYGUARD_LOCKED) != 0) { 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/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/OWNERS b/libs/WindowManager/Shell/tests/OWNERS index a77fd51d5fcb..d1be12f6d1bb 100644 --- a/libs/WindowManager/Shell/tests/OWNERS +++ b/libs/WindowManager/Shell/tests/OWNERS @@ -1,4 +1,4 @@ -# Bug component: 1157642 +# Bug component: 928594 # includes OWNERS from parent directories natanieljr@google.com pablogamito@google.com @@ -15,3 +15,6 @@ tkachenkoi@google.com mpodolian@google.com jeremysim@google.com peanutbutter@google.com +pragyabajoria@google.com +uysalorhan@google.com +gsennton@google.com diff --git a/libs/WindowManager/Shell/tests/flicker/OWNERS b/libs/WindowManager/Shell/tests/flicker/OWNERS new file mode 100644 index 000000000000..4db0babdaf99 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/OWNERS @@ -0,0 +1,3 @@ +# Bug component: 1157642 +# includes OWNERS from parent directories +include platform/development:/tools/winscope/OWNERS 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/ShellTaskOrganizerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java index 716a148175df..413e49562435 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java @@ -365,7 +365,7 @@ public class ShellTaskOrganizerTests extends ShellTestCase { final RunningTaskInfo taskInfo1 = createTaskInfo(/* taskId= */ 12, WINDOWING_MODE_FULLSCREEN); taskInfo1.displayId = DEFAULT_DISPLAY; - taskInfo1.appCompatTaskInfo.topActivityInSizeCompat = false; + taskInfo1.appCompatTaskInfo.setTopActivityInSizeCompat(false); final TrackingTaskListener taskListener = new TrackingTaskListener(); mOrganizer.addListenerForType(taskListener, TASK_LISTENER_TYPE_FULLSCREEN); mOrganizer.onTaskAppeared(taskInfo1, /* leash= */ null); @@ -378,7 +378,7 @@ public class ShellTaskOrganizerTests extends ShellTestCase { final RunningTaskInfo taskInfo2 = createTaskInfo(taskInfo1.taskId, taskInfo1.getWindowingMode()); taskInfo2.displayId = taskInfo1.displayId; - taskInfo2.appCompatTaskInfo.topActivityInSizeCompat = true; + taskInfo2.appCompatTaskInfo.setTopActivityInSizeCompat(true); taskInfo2.isVisible = true; mOrganizer.onTaskInfoChanged(taskInfo2); verifyOnCompatInfoChangedInvokedWith(taskInfo2, taskListener); @@ -388,7 +388,7 @@ public class ShellTaskOrganizerTests extends ShellTestCase { final RunningTaskInfo taskInfo3 = createTaskInfo(taskInfo1.taskId, taskInfo1.getWindowingMode()); taskInfo3.displayId = taskInfo1.displayId; - taskInfo3.appCompatTaskInfo.topActivityInSizeCompat = true; + taskInfo3.appCompatTaskInfo.setTopActivityInSizeCompat(true); taskInfo3.isVisible = false; mOrganizer.onTaskInfoChanged(taskInfo3); verifyOnCompatInfoChangedInvokedWith(taskInfo3, null /* taskListener */); @@ -403,7 +403,7 @@ public class ShellTaskOrganizerTests extends ShellTestCase { final RunningTaskInfo taskInfo1 = createTaskInfo(/* taskId= */ 12, WINDOWING_MODE_FULLSCREEN); taskInfo1.displayId = DEFAULT_DISPLAY; - taskInfo1.appCompatTaskInfo.topActivityEligibleForLetterboxEducation = false; + taskInfo1.appCompatTaskInfo.setEligibleForLetterboxEducation(false); final TrackingTaskListener taskListener = new TrackingTaskListener(); mOrganizer.addListenerForType(taskListener, TASK_LISTENER_TYPE_FULLSCREEN); mOrganizer.onTaskAppeared(taskInfo1, /* leash= */ null); @@ -418,7 +418,7 @@ public class ShellTaskOrganizerTests extends ShellTestCase { final RunningTaskInfo taskInfo2 = createTaskInfo(taskInfo1.taskId, WINDOWING_MODE_FULLSCREEN); taskInfo2.displayId = taskInfo1.displayId; - taskInfo2.appCompatTaskInfo.topActivityEligibleForLetterboxEducation = true; + taskInfo2.appCompatTaskInfo.setEligibleForLetterboxEducation(true); taskInfo2.isVisible = true; mOrganizer.onTaskInfoChanged(taskInfo2); verifyOnCompatInfoChangedInvokedWith(taskInfo2, taskListener); @@ -428,7 +428,7 @@ public class ShellTaskOrganizerTests extends ShellTestCase { final RunningTaskInfo taskInfo3 = createTaskInfo(taskInfo1.taskId, WINDOWING_MODE_FULLSCREEN); taskInfo3.displayId = taskInfo1.displayId; - taskInfo3.appCompatTaskInfo.topActivityEligibleForLetterboxEducation = true; + taskInfo3.appCompatTaskInfo.setEligibleForLetterboxEducation(true); taskInfo3.isVisible = false; mOrganizer.onTaskInfoChanged(taskInfo3); verifyOnCompatInfoChangedInvokedWith(taskInfo3, null /* taskListener */); 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 77e22cd17f6f..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) { @@ -681,7 +687,7 @@ public class CompatUIControllerTest extends ShellTestCase { @Test public void testLetterboxEduLayout_notCreatedWhenLetterboxEducationIsDisabled() { TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true); - taskInfo.appCompatTaskInfo.isLetterboxEducationEnabled = false; + taskInfo.appCompatTaskInfo.setLetterboxEducationEnabled(false); mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener)); @@ -705,12 +711,12 @@ public class CompatUIControllerTest extends ShellTestCase { RunningTaskInfo taskInfo = new RunningTaskInfo(); taskInfo.taskId = taskId; taskInfo.displayId = displayId; - taskInfo.appCompatTaskInfo.topActivityInSizeCompat = hasSizeCompat; + taskInfo.appCompatTaskInfo.setTopActivityInSizeCompat(hasSizeCompat); taskInfo.isVisible = isVisible; taskInfo.isFocused = isFocused; taskInfo.isTopActivityTransparent = isTopActivityTransparent; - taskInfo.appCompatTaskInfo.isLetterboxEducationEnabled = true; - taskInfo.appCompatTaskInfo.topActivityBoundsLetterboxed = true; + taskInfo.appCompatTaskInfo.setLetterboxEducationEnabled(true); + taskInfo.appCompatTaskInfo.setTopActivityLetterboxed(true); return taskInfo; } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java index 3b93861d6cd2..e5d1919decf4 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java @@ -146,7 +146,7 @@ public class CompatUILayoutTest extends ShellTestCase { private static TaskInfo createTaskInfo(boolean hasSizeCompat) { ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo(); taskInfo.taskId = TASK_ID; - taskInfo.appCompatTaskInfo.topActivityInSizeCompat = hasSizeCompat; + taskInfo.appCompatTaskInfo.setTopActivityInSizeCompat(hasSizeCompat); taskInfo.appCompatTaskInfo.topActivityLetterboxHeight = 1000; taskInfo.appCompatTaskInfo.topActivityLetterboxWidth = 1000; taskInfo.configuration.windowConfiguration.setBounds(new Rect(0, 0, 2000, 2000)); 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/CompatUIWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java index c5033f3ae64b..1c0175603df9 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java @@ -446,7 +446,7 @@ public class CompatUIWindowManagerTest extends ShellTestCase { private static TaskInfo createTaskInfo(boolean hasSizeCompat) { ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo(); taskInfo.taskId = TASK_ID; - taskInfo.appCompatTaskInfo.topActivityInSizeCompat = hasSizeCompat; + taskInfo.appCompatTaskInfo.setTopActivityInSizeCompat(hasSizeCompat); taskInfo.configuration.uiMode &= ~Configuration.UI_MODE_TYPE_DESK; // Letterboxed activity that takes half the screen should show size compat restart button taskInfo.appCompatTaskInfo.topActivityLetterboxHeight = 1000; 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 b5664ac113fa..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(); @@ -499,7 +526,7 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo(); taskInfo.userId = userId; taskInfo.taskId = TASK_ID; - taskInfo.appCompatTaskInfo.topActivityEligibleForLetterboxEducation = eligible; + taskInfo.appCompatTaskInfo.setEligibleForLetterboxEducation(eligible); taskInfo.configuration.windowConfiguration.setBounds(bounds); return taskInfo; } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayoutTest.java index 7a641960a2c5..e8e68bdbd940 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayoutTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayoutTest.java @@ -155,7 +155,7 @@ public class UserAspectRatioSettingsLayoutTest extends ShellTestCase { private static TaskInfo createTaskInfo(boolean hasSizeCompat) { ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo(); taskInfo.taskId = TASK_ID; - taskInfo.appCompatTaskInfo.topActivityInSizeCompat = hasSizeCompat; + taskInfo.appCompatTaskInfo.setTopActivityInSizeCompat(hasSizeCompat); taskInfo.realActivity = new ComponentName("com.mypackage.test", "TestActivity"); return taskInfo; } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java index 9f288cc4bd93..9f86d49b52c4 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java @@ -317,7 +317,7 @@ public class UserAspectRatioSettingsWindowManagerTest extends ShellTestCase { // layout should be inflated taskInfo.appCompatTaskInfo.topActivityLetterboxHeight = stableBounds.height(); taskInfo.appCompatTaskInfo.topActivityLetterboxWidth = stableBounds.width(); - taskInfo.appCompatTaskInfo.isUserFullscreenOverrideEnabled = true; + taskInfo.appCompatTaskInfo.setUserFullscreenOverrideEnabled(true); mWindowManager.updateCompatInfo(taskInfo, mTaskListener, /* canShow= */ true); @@ -482,9 +482,9 @@ public class UserAspectRatioSettingsWindowManagerTest extends ShellTestCase { boolean topActivityBoundsLetterboxed, String action, String category) { ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo(); taskInfo.taskId = TASK_ID; - taskInfo.appCompatTaskInfo.topActivityEligibleForUserAspectRatioButton = - eligibleForUserAspectRatioButton; - taskInfo.appCompatTaskInfo.topActivityBoundsLetterboxed = topActivityBoundsLetterboxed; + taskInfo.appCompatTaskInfo.setEligibleForUserAspectRatioButton( + eligibleForUserAspectRatioButton); + taskInfo.appCompatTaskInfo.setTopActivityLetterboxed(topActivityBoundsLetterboxed); taskInfo.configuration.uiMode &= ~Configuration.UI_MODE_TYPE_DESK; taskInfo.realActivity = new ComponentName("com.mypackage.test", "TestActivity"); taskInfo.baseIntent = new Intent(action).addCategory(category); 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 76939f61832f..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,23 +2856,23 @@ class DesktopTasksControllerTest : ShellTestCase() { isResizeable = isResizable configuration.orientation = deviceOrientation configuration.windowConfiguration.windowingMode = windowingMode + appCompatTaskInfo.isUserFullscreenOverrideEnabled = enableUserFullscreenOverride + appCompatTaskInfo.isSystemFullscreenOverrideEnabled = enableSystemFullscreenOverride if (shouldLetterbox) { if (deviceOrientation == ORIENTATION_LANDSCAPE && screenOrientation == SCREEN_ORIENTATION_PORTRAIT) { // Letterbox to portrait size - appCompatTaskInfo.topActivityBoundsLetterboxed = true + appCompatTaskInfo.setTopActivityLetterboxed(true) appCompatTaskInfo.topActivityLetterboxAppWidth = 1200 appCompatTaskInfo.topActivityLetterboxAppHeight = 1600 } else if (deviceOrientation == ORIENTATION_PORTRAIT && screenOrientation == SCREEN_ORIENTATION_LANDSCAPE) { // Letterbox to landscape size - appCompatTaskInfo.topActivityBoundsLetterboxed = true + appCompatTaskInfo.setTopActivityLetterboxed(true) appCompatTaskInfo.topActivityLetterboxAppWidth = 1600 appCompatTaskInfo.topActivityLetterboxAppHeight = 1200 } - } else { - appCompatTaskInfo.topActivityBoundsLetterboxed = false } 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/DragResizeWindowGeometryTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java index 77337a03ef55..d8f395d76b39 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java @@ -56,7 +56,7 @@ import org.junit.runner.RunWith; public class DragResizeWindowGeometryTests extends ShellTestCase { private static final Size TASK_SIZE = new Size(500, 1000); private static final int TASK_CORNER_RADIUS = 10; - private static final int EDGE_RESIZE_THICKNESS = 15; + private static final int EDGE_RESIZE_THICKNESS = 12; private static final int FINE_CORNER_SIZE = EDGE_RESIZE_THICKNESS * 2 + 10; private static final int LARGE_CORNER_SIZE = FINE_CORNER_SIZE + 10; private static final DragResizeWindowGeometry GEOMETRY = new DragResizeWindowGeometry( 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/Idmap.cpp b/libs/androidfw/Idmap.cpp index 982419059ead..f066e4620675 100644 --- a/libs/androidfw/Idmap.cpp +++ b/libs/androidfw/Idmap.cpp @@ -121,7 +121,7 @@ OverlayDynamicRefTable::OverlayDynamicRefTable(const Idmap_data_header* data_hea uint8_t target_assigned_package_id) : data_header_(data_header), entries_(entries), - target_assigned_package_id_(target_assigned_package_id) { }; + target_assigned_package_id_(target_assigned_package_id) {} status_t OverlayDynamicRefTable::lookupResourceId(uint32_t* resId) const { const Idmap_overlay_entry* first_entry = entries_; diff --git a/libs/androidfw/include/androidfw/Idmap.h b/libs/androidfw/include/androidfw/Idmap.h index c32a38ee9ec2..64b1f0c6ed03 100644 --- a/libs/androidfw/include/androidfw/Idmap.h +++ b/libs/androidfw/include/androidfw/Idmap.h @@ -171,14 +171,14 @@ class LoadedIdmap { } // Returns a mapping from target resource ids to overlay values. - const IdmapResMap GetTargetResourcesMap(uint8_t target_assigned_package_id, - const OverlayDynamicRefTable* overlay_ref_table) const { + IdmapResMap GetTargetResourcesMap(uint8_t target_assigned_package_id, + const OverlayDynamicRefTable* overlay_ref_table) const { return IdmapResMap(data_header_, target_entries_, target_inline_entries_, inline_entry_values_, configurations_, target_assigned_package_id, overlay_ref_table); } // Returns a dynamic reference table for a loaded overlay package. - const OverlayDynamicRefTable GetOverlayDynamicRefTable(uint8_t target_assigned_package_id) const { + OverlayDynamicRefTable GetOverlayDynamicRefTable(uint8_t target_assigned_package_id) const { return OverlayDynamicRefTable(data_header_, overlay_entries_, target_assigned_package_id); } 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/media/jni/JetPlayer.h b/media/jni/JetPlayer.h index bb569bcad7be..4cc266dec445 100644 --- a/media/jni/JetPlayer.h +++ b/media/jni/JetPlayer.h @@ -40,7 +40,7 @@ public: static const int JET_NUMQUEUEDSEGMENT_UPDATE = 3; static const int JET_PAUSE_UPDATE = 4; - JetPlayer(void *javaJetPlayer, + explicit JetPlayer(void *javaJetPlayer, int maxTracks = 32, int trackBufferSize = 1200); ~JetPlayer(); @@ -69,7 +69,6 @@ private: void fireUpdateOnStatusChange(); void fireEventsFromJetQueue(); - JetPlayer() {} // no default constructor void dump(); void dumpJetStatus(S_JET_STATUS* pJetStatus); @@ -96,7 +95,7 @@ private: class JetPlayerThread : public Thread { public: - JetPlayerThread(JetPlayer *player) : mPlayer(player) { + explicit JetPlayerThread(JetPlayer *player) : mPlayer(player) { } protected: @@ -106,8 +105,7 @@ private: JetPlayer *mPlayer; bool threadLoop() { - int result; - result = mPlayer->render(); + mPlayer->render(); return false; } 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/NfcActivityManager.java b/nfc/java/android/nfc/NfcActivityManager.java index 0e40db612708..0eb846d6c72a 100644 --- a/nfc/java/android/nfc/NfcActivityManager.java +++ b/nfc/java/android/nfc/NfcActivityManager.java @@ -236,11 +236,7 @@ public final class NfcActivityManager extends IAppCallback.Stub public void setReaderMode(Binder token, int flags, Bundle extras) { if (DBG) Log.d(TAG, "Setting reader mode"); - try { - NfcAdapter.sService.setReaderMode(token, this, flags, extras); - } catch (RemoteException e) { - mAdapter.attemptDeadServiceRecovery(e); - } + NfcAdapter.callService(() -> NfcAdapter.sService.setReaderMode(token, this, flags, extras)); } /** @@ -248,19 +244,11 @@ public final class NfcActivityManager extends IAppCallback.Stub * Makes IPC call - do not hold lock. */ void requestNfcServiceCallback() { - try { - NfcAdapter.sService.setAppCallback(this); - } catch (RemoteException e) { - mAdapter.attemptDeadServiceRecovery(e); - } + NfcAdapter.callService(() -> NfcAdapter.sService.setAppCallback(this)); } void verifyNfcPermission() { - try { - NfcAdapter.sService.verifyNfcPermission(); - } catch (RemoteException e) { - mAdapter.attemptDeadServiceRecovery(e); - } + NfcAdapter.callService(() -> NfcAdapter.sService.verifyNfcPermission()); } @Override @@ -406,11 +394,8 @@ public final class NfcActivityManager extends IAppCallback.Stub } private void changeDiscoveryTech(Binder token, int pollTech, int listenTech) { - try { - NfcAdapter.sService.updateDiscoveryTechnology(token, pollTech, listenTech); - } catch (RemoteException e) { - mAdapter.attemptDeadServiceRecovery(e); - } + NfcAdapter.callService( + () -> NfcAdapter.sService.updateDiscoveryTechnology(token, pollTech, listenTech)); } } diff --git a/nfc/java/android/nfc/NfcAdapter.java b/nfc/java/android/nfc/NfcAdapter.java index 0ffab4ba3eaf..b36b705245cd 100644 --- a/nfc/java/android/nfc/NfcAdapter.java +++ b/nfc/java/android/nfc/NfcAdapter.java @@ -922,8 +922,8 @@ public final class NfcAdapter { * @hide */ @UnsupportedAppUsage - public INfcAdapter getService() { - isEnabled(); // NOP call to recover sService if it is stale + public static INfcAdapter getService() { + isEnabledStatic(); // NOP call to recover sService if it is stale return sService; } @@ -931,8 +931,8 @@ public final class NfcAdapter { * Returns the binder interface to the tag service. * @hide */ - public INfcTag getTagService() { - isEnabled(); // NOP call to recover sTagService if it is stale + public static INfcTag getTagService() { + isEnabledStatic(); // NOP call to recover sTagService if it is stale return sTagService; } @@ -940,8 +940,8 @@ public final class NfcAdapter { * Returns the binder interface to the card emulation service. * @hide */ - public INfcCardEmulation getCardEmulationService() { - isEnabled(); + public static INfcCardEmulation getCardEmulationService() { + isEnabledStatic(); return sCardEmulationService; } @@ -949,8 +949,8 @@ public final class NfcAdapter { * Returns the binder interface to the NFC-F card emulation service. * @hide */ - public INfcFCardEmulation getNfcFCardEmulationService() { - isEnabled(); + public static INfcFCardEmulation getNfcFCardEmulationService() { + isEnabledStatic(); return sNfcFCardEmulationService; } @@ -973,14 +973,14 @@ public final class NfcAdapter { * @hide */ @UnsupportedAppUsage - public void attemptDeadServiceRecovery(Exception e) { + public static void attemptDeadServiceRecovery(RemoteException e) { Log.e(TAG, "NFC service dead - attempting to recover", e); INfcAdapter service = getServiceInterface(); if (service == null) { Log.e(TAG, "could not retrieve NFC service during service recovery"); // nothing more can be done now, sService is still stale, we'll hit // this recovery path again later - return; + e.rethrowAsRuntimeException(); } // assigning to sService is not thread-safe, but this is best-effort code // and on a well-behaved system should never happen @@ -993,7 +993,7 @@ public final class NfcAdapter { Log.e(TAG, "could not retrieve NFC tag service during service recovery"); // nothing more can be done now, sService is still stale, we'll hit // this recovery path again later - return; + ee.rethrowAsRuntimeException(); } } @@ -1014,24 +1014,27 @@ public final class NfcAdapter { "could not retrieve NFC-F card emulation service during service recovery"); } } - - return; } - private boolean isCardEmulationEnabled() { + private static boolean isCardEmulationEnabled() { if (sHasCeFeature) { return (sCardEmulationService != null || sNfcFCardEmulationService != null); } return false; } - private boolean isTagReadingEnabled() { + private static boolean isTagReadingEnabled() { if (sHasNfcFeature) { return sTagService != null; } return false; } + private static boolean isEnabledStatic() { + boolean serviceState = callServiceReturn(() -> sService.getState() == STATE_ON, false); + return serviceState + && (isTagReadingEnabled() || isCardEmulationEnabled() || sHasNfcWlcFeature); + } /** * Return true if this NFC Adapter has any features enabled. @@ -1046,24 +1049,7 @@ public final class NfcAdapter { * @return true if this NFC Adapter has any features enabled */ public boolean isEnabled() { - boolean serviceState = false; - try { - serviceState = sService.getState() == STATE_ON; - } catch (RemoteException e) { - attemptDeadServiceRecovery(e); - // Try one more time - if (sService == null) { - Log.e(TAG, "Failed to recover NFC Service."); - return false; - } - try { - serviceState = sService.getState() == STATE_ON; - } catch (RemoteException ee) { - Log.e(TAG, "Failed to recover NFC Service."); - } - } - return serviceState - && (isTagReadingEnabled() || isCardEmulationEnabled() || sHasNfcWlcFeature); + return isEnabledStatic(); } /** @@ -1157,11 +1143,7 @@ public final class NfcAdapter { * @hide */ public void pausePolling(int timeoutInMs) { - try { - sService.pausePolling(timeoutInMs); - } catch (RemoteException e) { - attemptDeadServiceRecovery(e); - } + callService(() -> sService.pausePolling(timeoutInMs)); } @@ -1222,11 +1204,7 @@ public final class NfcAdapter { * @hide */ public void resumePolling() { - try { - sService.resumePolling(); - } catch (RemoteException e) { - attemptDeadServiceRecovery(e); - } + callService(() -> sService.resumePolling()); } /** @@ -1645,15 +1623,10 @@ public final class NfcAdapter { if (activity == null || intent == null) { throw new NullPointerException(); } - try { - TechListParcel parcel = null; - if (techLists != null && techLists.length > 0) { - parcel = new TechListParcel(techLists); - } - sService.setForegroundDispatch(intent, filters, parcel); - } catch (RemoteException e) { - attemptDeadServiceRecovery(e); - } + final TechListParcel parcel = (techLists != null && techLists.length > 0) + ? new TechListParcel(techLists) + : null; + callService(() -> sService.setForegroundDispatch(intent, filters, parcel)); } /** @@ -1677,11 +1650,7 @@ public final class NfcAdapter { throw new UnsupportedOperationException(); } } - try { - sService.setForegroundDispatch(null, null, null); - } catch (RemoteException e) { - attemptDeadServiceRecovery(e); - } + callService(() -> sService.setForegroundDispatch(null, null, null)); } /** @@ -1762,11 +1731,7 @@ public final class NfcAdapter { } Binder token = new Binder(); int flags = enable ? ENABLE_POLLING_FLAGS : DISABLE_POLLING_FLAGS; - try { - NfcAdapter.sService.setReaderMode(token, null, flags, null); - } catch (RemoteException e) { - attemptDeadServiceRecovery(e); - } + callService(() -> sService.setReaderMode(token, null, flags, null)); } /** @@ -1838,12 +1803,8 @@ public final class NfcAdapter { && ((pollTechnology & FLAG_SET_DEFAULT_TECH) == FLAG_SET_DEFAULT_TECH || (listenTechnology & FLAG_SET_DEFAULT_TECH) == FLAG_SET_DEFAULT_TECH)) { Binder token = new Binder(); - try { - NfcAdapter.sService.updateDiscoveryTechnology(token, - pollTechnology, listenTechnology); - } catch (RemoteException e) { - attemptDeadServiceRecovery(e); - } + callService( () -> + sService.updateDiscoveryTechnology(token, pollTechnology, listenTechnology)); } else { mNfcActivityManager.setDiscoveryTech(activity, pollTechnology, listenTechnology); } @@ -2227,11 +2188,7 @@ public final class NfcAdapter { if (tag == null) { throw new NullPointerException("tag cannot be null"); } - try { - sService.dispatch(tag); - } catch (RemoteException e) { - attemptDeadServiceRecovery(e); - } + callService(() -> sService.dispatch(tag)); } /** @@ -2267,8 +2224,10 @@ public final class NfcAdapter { synchronized (mLock) { if (mNfcUnlockHandlers.containsKey(unlockHandler)) { // update the tag technologies - sService.removeNfcUnlockHandler(mNfcUnlockHandlers.get(unlockHandler)); - mNfcUnlockHandlers.remove(unlockHandler); + callService(() -> { + sService.removeNfcUnlockHandler(mNfcUnlockHandlers.get(unlockHandler)); + mNfcUnlockHandlers.remove(unlockHandler); + }); } INfcUnlockHandler.Stub iHandler = new INfcUnlockHandler.Stub() { @@ -2277,20 +2236,18 @@ public final class NfcAdapter { return unlockHandler.onUnlockAttempted(tag); } }; - - sService.addNfcUnlockHandler(iHandler, - Tag.getTechCodesFromStrings(tagTechnologies)); - mNfcUnlockHandlers.put(unlockHandler, iHandler); + return callServiceReturn(() -> { + sService.addNfcUnlockHandler( + iHandler, Tag.getTechCodesFromStrings(tagTechnologies)); + mNfcUnlockHandlers.put(unlockHandler, iHandler); + return true; + }, false); } - } catch (RemoteException e) { - attemptDeadServiceRecovery(e); - return false; } catch (IllegalArgumentException e) { Log.e(TAG, "Unable to register LockscreenDispatch", e); return false; } - return true; } /** @@ -2307,17 +2264,14 @@ public final class NfcAdapter { throw new UnsupportedOperationException(); } } - try { - synchronized (mLock) { - if (mNfcUnlockHandlers.containsKey(unlockHandler)) { + synchronized (mLock) { + if (mNfcUnlockHandlers.containsKey(unlockHandler)) { + return callServiceReturn(() -> { sService.removeNfcUnlockHandler(mNfcUnlockHandlers.remove(unlockHandler)); - } - - return true; + return true; + }, false); } - } catch (RemoteException e) { - attemptDeadServiceRecovery(e); - return false; + return true; } } @@ -2525,26 +2479,8 @@ public final class NfcAdapter { Log.e(TAG, "TagIntentAppPreference is not supported"); throw new UnsupportedOperationException(); } - try { - Map<String, Boolean> result = (Map<String, Boolean>) sService - .getTagIntentAppPreferenceForUser(userId); - return result; - } catch (RemoteException e) { - attemptDeadServiceRecovery(e); - // Try one more time - if (sService == null) { - Log.e(TAG, "Failed to recover NFC Service."); - return Collections.emptyMap(); - } - try { - Map<String, Boolean> result = (Map<String, Boolean>) sService - .getTagIntentAppPreferenceForUser(userId); - return result; - } catch (RemoteException ee) { - Log.e(TAG, "Failed to recover NFC Service."); - } - return Collections.emptyMap(); - } + return callServiceReturn( () -> + sService.getTagIntentAppPreferenceForUser(userId), Collections.emptyMap()); } /** @@ -2590,50 +2526,44 @@ public final class NfcAdapter { callService(() -> sService.notifyTestHceData(technology, data)); } + /** @hide */ interface ServiceCall { void call() throws RemoteException; } - - void callService(ServiceCall call) { + /** @hide */ + static void callService(ServiceCall call) { try { if (sService == null) { - attemptDeadServiceRecovery(null); + attemptDeadServiceRecovery(new RemoteException("NFC Service is null")); } call.call(); } catch (RemoteException e) { attemptDeadServiceRecovery(e); - // Try one more time - if (sService == null) { - Log.e(TAG, "Failed to recover NFC Service."); - return; - } try { call.call(); } catch (RemoteException ee) { - Log.e(TAG, "Failed to recover NFC Service."); + ee.rethrowAsRuntimeException(); } } } + /** @hide */ interface ServiceCallReturn<T> { T call() throws RemoteException; } - <T> T callServiceReturn(ServiceCallReturn<T> call, T defaultReturn) { + /** @hide */ + static <T> T callServiceReturn(ServiceCallReturn<T> call, T defaultReturn) { try { if (sService == null) { - attemptDeadServiceRecovery(null); + attemptDeadServiceRecovery(new RemoteException("NFC Service is null")); } return call.call(); } catch (RemoteException e) { attemptDeadServiceRecovery(e); // Try one more time - if (sService == null) { - Log.e(TAG, "Failed to recover NFC Service."); - return defaultReturn; - } try { return call.call(); } catch (RemoteException ee) { - Log.e(TAG, "Failed to recover NFC Service."); + ee.rethrowAsRuntimeException(); } } return defaultReturn; diff --git a/nfc/java/android/nfc/NfcOemExtension.java b/nfc/java/android/nfc/NfcOemExtension.java index f6138a63fae4..2ec819cdc1a9 100644 --- a/nfc/java/android/nfc/NfcOemExtension.java +++ b/nfc/java/android/nfc/NfcOemExtension.java @@ -89,13 +89,11 @@ public final class NfcOemExtension { + "registering"); throw new IllegalArgumentException(); } - try { + NfcAdapter.callService(() -> { NfcAdapter.sService.registerOemExtensionCallback(mOemNfcExtensionCallback); mCallback = callback; mExecutor = executor; - } catch (RemoteException e) { - mAdapter.attemptDeadServiceRecovery(e); - } + }); } } @@ -117,13 +115,11 @@ public final class NfcOemExtension { Log.e(TAG, "Callback not registered"); throw new IllegalArgumentException(); } - try { + NfcAdapter.callService(() -> { NfcAdapter.sService.unregisterOemExtensionCallback(mOemNfcExtensionCallback); mCallback = null; mExecutor = null; - } catch (RemoteException e) { - mAdapter.attemptDeadServiceRecovery(e); - } + }); } } @@ -134,11 +130,7 @@ public final class NfcOemExtension { @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION) @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void clearPreference() { - try { - NfcAdapter.sService.clearPreference(); - } catch (RemoteException e) { - mAdapter.attemptDeadServiceRecovery(e); - } + NfcAdapter.callService(() -> NfcAdapter.sService.clearPreference()); } /** @@ -147,11 +139,7 @@ public final class NfcOemExtension { @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION) @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void synchronizeScreenState() { - try { - NfcAdapter.sService.setScreenState(); - } catch (RemoteException e) { - mAdapter.attemptDeadServiceRecovery(e); - } + NfcAdapter.callService(() -> NfcAdapter.sService.setScreenState()); } /** @@ -162,11 +150,7 @@ public final class NfcOemExtension { @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION) @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void maybeTriggerFirmwareUpdate() { - try { - NfcAdapter.sService.checkFirmware(); - } catch (RemoteException e) { - mAdapter.attemptDeadServiceRecovery(e); - } + NfcAdapter.callService(() -> NfcAdapter.sService.checkFirmware()); } private final class NfcOemExtensionCallback extends INfcOemExtensionCallback.Stub { diff --git a/nfc/java/android/nfc/cardemulation/CardEmulation.java b/nfc/java/android/nfc/cardemulation/CardEmulation.java index 2fe2ce39813b..e0438ce9258c 100644 --- a/nfc/java/android/nfc/cardemulation/CardEmulation.java +++ b/nfc/java/android/nfc/cardemulation/CardEmulation.java @@ -219,24 +219,9 @@ public final class CardEmulation { * <p class="note">Requires the {@link android.Manifest.permission#NFC} permission. */ public boolean isDefaultServiceForCategory(ComponentName service, String category) { - try { - return sService.isDefaultServiceForCategory(mContext.getUser().getIdentifier(), - service, category); - } catch (RemoteException e) { - // Try one more time - recoverService(); - if (sService == null) { - Log.e(TAG, "Failed to recover CardEmulationService."); - return false; - } - try { - return sService.isDefaultServiceForCategory(mContext.getUser().getIdentifier(), - service, category); - } catch (RemoteException ee) { - Log.e(TAG, "Failed to recover CardEmulationService."); - return false; - } - } + return callServiceReturn(() -> + sService.isDefaultServiceForCategory( + mContext.getUser().getIdentifier(), service, category), false); } /** @@ -251,24 +236,9 @@ public final class CardEmulation { * <p class="note">Requires the {@link android.Manifest.permission#NFC} permission. */ public boolean isDefaultServiceForAid(ComponentName service, String aid) { - try { - return sService.isDefaultServiceForAid(mContext.getUser().getIdentifier(), - service, aid); - } catch (RemoteException e) { - // Try one more time - recoverService(); - if (sService == null) { - Log.e(TAG, "Failed to recover CardEmulationService."); - return false; - } - try { - return sService.isDefaultServiceForAid(mContext.getUser().getIdentifier(), - service, aid); - } catch (RemoteException ee) { - Log.e(TAG, "Failed to reach CardEmulationService."); - return false; - } - } + return callServiceReturn(() -> + sService.isDefaultServiceForAid( + mContext.getUser().getIdentifier(), service, aid), false); } /** @@ -331,22 +301,8 @@ public final class CardEmulation { */ public int getSelectionModeForCategory(String category) { if (CATEGORY_PAYMENT.equals(category)) { - boolean paymentRegistered = false; - try { - paymentRegistered = sService.isDefaultPaymentRegistered(); - } catch (RemoteException e) { - recoverService(); - if (sService == null) { - Log.e(TAG, "Failed to recover CardEmulationService."); - return SELECTION_MODE_ALWAYS_ASK; - } - try { - paymentRegistered = sService.isDefaultPaymentRegistered(); - } catch (RemoteException ee) { - Log.e(TAG, "Failed to reach CardEmulationService."); - return SELECTION_MODE_ALWAYS_ASK; - } - } + boolean paymentRegistered = callServiceReturn(() -> + sService.isDefaultPaymentRegistered(), false); if (paymentRegistered) { return SELECTION_MODE_PREFER_DEFAULT; } else { @@ -369,13 +325,9 @@ public final class CardEmulation { @FlaggedApi(Flags.FLAG_NFC_OBSERVE_MODE) public boolean setShouldDefaultToObserveModeForService(@NonNull ComponentName service, boolean enable) { - try { - return sService.setShouldDefaultToObserveModeForService( - mContext.getUser().getIdentifier(), service, enable); - } catch (RemoteException e) { - Log.e(TAG, "Failed to reach CardEmulationService."); - } - return false; + return callServiceReturn(() -> + sService.setShouldDefaultToObserveModeForService( + mContext.getUser().getIdentifier(), service, enable), false); } /** @@ -396,27 +348,11 @@ public final class CardEmulation { @FlaggedApi(Flags.FLAG_NFC_READ_POLLING_LOOP) public boolean registerPollingLoopFilterForService(@NonNull ComponentName service, @NonNull String pollingLoopFilter, boolean autoTransact) { - pollingLoopFilter = validatePollingLoopFilter(pollingLoopFilter); - - try { - return sService.registerPollingLoopFilterForService(mContext.getUser().getIdentifier(), - service, pollingLoopFilter, autoTransact); - } catch (RemoteException e) { - // Try one more time - recoverService(); - if (sService == null) { - Log.e(TAG, "Failed to recover CardEmulationService."); - return false; - } - try { - return sService.registerPollingLoopFilterForService( - mContext.getUser().getIdentifier(), service, - pollingLoopFilter, autoTransact); - } catch (RemoteException ee) { - Log.e(TAG, "Failed to reach CardEmulationService."); - return false; - } - } + final String pollingLoopFilterV = validatePollingLoopFilter(pollingLoopFilter); + return callServiceReturn(() -> + sService.registerPollingLoopFilterForService( + mContext.getUser().getIdentifier(), service, pollingLoopFilterV, autoTransact), + false); } /** @@ -431,27 +367,10 @@ public final class CardEmulation { @FlaggedApi(Flags.FLAG_NFC_READ_POLLING_LOOP) public boolean removePollingLoopFilterForService(@NonNull ComponentName service, @NonNull String pollingLoopFilter) { - pollingLoopFilter = validatePollingLoopFilter(pollingLoopFilter); - - try { - return sService.removePollingLoopFilterForService(mContext.getUser().getIdentifier(), - service, pollingLoopFilter); - } catch (RemoteException e) { - // Try one more time - recoverService(); - if (sService == null) { - Log.e(TAG, "Failed to recover CardEmulationService."); - return false; - } - try { - return sService.removePollingLoopFilterForService( - mContext.getUser().getIdentifier(), service, - pollingLoopFilter); - } catch (RemoteException ee) { - Log.e(TAG, "Failed to reach CardEmulationService."); - return false; - } - } + final String pollingLoopFilterV = validatePollingLoopFilter(pollingLoopFilter); + return callServiceReturn(() -> + sService.removePollingLoopFilterForService( + mContext.getUser().getIdentifier(), service, pollingLoopFilterV), false); } @@ -477,28 +396,13 @@ public final class CardEmulation { @FlaggedApi(Flags.FLAG_NFC_READ_POLLING_LOOP) public boolean registerPollingLoopPatternFilterForService(@NonNull ComponentName service, @NonNull String pollingLoopPatternFilter, boolean autoTransact) { - pollingLoopPatternFilter = validatePollingLoopPatternFilter(pollingLoopPatternFilter); - - try { - return sService.registerPollingLoopPatternFilterForService( - mContext.getUser().getIdentifier(), - service, pollingLoopPatternFilter, autoTransact); - } catch (RemoteException e) { - // Try one more time - recoverService(); - if (sService == null) { - Log.e(TAG, "Failed to recover CardEmulationService."); - return false; - } - try { - return sService.registerPollingLoopPatternFilterForService( - mContext.getUser().getIdentifier(), service, - pollingLoopPatternFilter, autoTransact); - } catch (RemoteException ee) { - Log.e(TAG, "Failed to reach CardEmulationService."); - return false; - } - } + final String pollingLoopPatternFilterV = + validatePollingLoopPatternFilter(pollingLoopPatternFilter); + return callServiceReturn(() -> + sService.registerPollingLoopPatternFilterForService( + mContext.getUser().getIdentifier(), service, pollingLoopPatternFilterV, + autoTransact), + false); } /** @@ -518,27 +422,11 @@ public final class CardEmulation { @FlaggedApi(Flags.FLAG_NFC_READ_POLLING_LOOP) public boolean removePollingLoopPatternFilterForService(@NonNull ComponentName service, @NonNull String pollingLoopPatternFilter) { - pollingLoopPatternFilter = validatePollingLoopPatternFilter(pollingLoopPatternFilter); - - try { - return sService.removePollingLoopPatternFilterForService( - mContext.getUser().getIdentifier(), service, pollingLoopPatternFilter); - } catch (RemoteException e) { - // Try one more time - recoverService(); - if (sService == null) { - Log.e(TAG, "Failed to recover CardEmulationService."); - return false; - } - try { - return sService.removePollingLoopPatternFilterForService( - mContext.getUser().getIdentifier(), service, - pollingLoopPatternFilter); - } catch (RemoteException ee) { - Log.e(TAG, "Failed to reach CardEmulationService."); - return false; - } - } + final String pollingLoopPatternFilterV = + validatePollingLoopPatternFilter(pollingLoopPatternFilter); + return callServiceReturn(() -> + sService.removePollingLoopPatternFilterForService( + mContext.getUser().getIdentifier(), service, pollingLoopPatternFilterV), false); } /** @@ -563,25 +451,10 @@ public final class CardEmulation { */ public boolean registerAidsForService(ComponentName service, String category, List<String> aids) { - AidGroup aidGroup = new AidGroup(aids, category); - try { - return sService.registerAidGroupForService(mContext.getUser().getIdentifier(), - service, aidGroup); - } catch (RemoteException e) { - // Try one more time - recoverService(); - if (sService == null) { - Log.e(TAG, "Failed to recover CardEmulationService."); - return false; - } - try { - return sService.registerAidGroupForService(mContext.getUser().getIdentifier(), - service, aidGroup); - } catch (RemoteException ee) { - Log.e(TAG, "Failed to reach CardEmulationService."); - return false; - } - } + final AidGroup aidGroup = new AidGroup(aids, category); + return callServiceReturn(() -> + sService.registerAidGroupForService( + mContext.getUser().getIdentifier(), service, aidGroup), false); } /** @@ -603,27 +476,9 @@ public final class CardEmulation { @RequiresPermission(android.Manifest.permission.NFC) @NonNull public boolean unsetOffHostForService(@NonNull ComponentName service) { - NfcAdapter adapter = NfcAdapter.getDefaultAdapter(mContext); - if (adapter == null) { - return false; - } - - try { - return sService.unsetOffHostForService(mContext.getUser().getIdentifier(), service); - } catch (RemoteException e) { - // Try one more time - recoverService(); - if (sService == null) { - Log.e(TAG, "Failed to recover CardEmulationService."); - return false; - } - try { - return sService.unsetOffHostForService(mContext.getUser().getIdentifier(), service); - } catch (RemoteException ee) { - Log.e(TAG, "Failed to reach CardEmulationService."); - return false; - } - } + return callServiceReturn(() -> + sService.unsetOffHostForService( + mContext.getUser().getIdentifier(), service), false); } /** @@ -662,8 +517,6 @@ public final class CardEmulation { @NonNull public boolean setOffHostForService(@NonNull ComponentName service, @NonNull String offHostSecureElement) { - boolean validSecureElement = false; - NfcAdapter adapter = NfcAdapter.getDefaultAdapter(mContext); if (adapter == null || offHostSecureElement == null) { return false; @@ -684,25 +537,10 @@ public final class CardEmulation { } else if (offHostSecureElement.equals("SIM")) { offHostSecureElement = "SIM1"; } - - try { - return sService.setOffHostForService(mContext.getUser().getIdentifier(), service, - offHostSecureElement); - } catch (RemoteException e) { - // Try one more time - recoverService(); - if (sService == null) { - Log.e(TAG, "Failed to recover CardEmulationService."); - return false; - } - try { - return sService.setOffHostForService(mContext.getUser().getIdentifier(), service, - offHostSecureElement); - } catch (RemoteException ee) { - Log.e(TAG, "Failed to reach CardEmulationService."); - return false; - } - } + final String offHostSecureElementV = new String(offHostSecureElement); + return callServiceReturn(() -> + sService.setOffHostForService( + mContext.getUser().getIdentifier(), service, offHostSecureElementV), false); } /** @@ -720,25 +558,10 @@ public final class CardEmulation { * @return The list of AIDs registered for this category, or null if it couldn't be found. */ public List<String> getAidsForService(ComponentName service, String category) { - try { - AidGroup group = sService.getAidGroupForService(mContext.getUser().getIdentifier(), - service, category); - return (group != null ? group.getAids() : null); - } catch (RemoteException e) { - recoverService(); - if (sService == null) { - Log.e(TAG, "Failed to recover CardEmulationService."); - return null; - } - try { - AidGroup group = sService.getAidGroupForService(mContext.getUser().getIdentifier(), - service, category); - return (group != null ? group.getAids() : null); - } catch (RemoteException ee) { - Log.e(TAG, "Failed to recover CardEmulationService."); - return null; - } - } + AidGroup group = callServiceReturn(() -> + sService.getAidGroupForService( + mContext.getUser().getIdentifier(), service, category), null); + return (group != null ? group.getAids() : null); } /** @@ -757,24 +580,9 @@ public final class CardEmulation { * @return whether the group was successfully removed. */ public boolean removeAidsForService(ComponentName service, String category) { - try { - return sService.removeAidGroupForService(mContext.getUser().getIdentifier(), service, - category); - } catch (RemoteException e) { - // Try one more time - recoverService(); - if (sService == null) { - Log.e(TAG, "Failed to recover CardEmulationService."); - return false; - } - try { - return sService.removeAidGroupForService(mContext.getUser().getIdentifier(), - service, category); - } catch (RemoteException ee) { - Log.e(TAG, "Failed to reach CardEmulationService."); - return false; - } - } + return callServiceReturn(() -> + sService.removeAidGroupForService( + mContext.getUser().getIdentifier(), service, category), false); } /** @@ -811,22 +619,7 @@ public final class CardEmulation { if (activity == null || service == null) { throw new NullPointerException("activity or service or category is null"); } - try { - return sService.setPreferredService(service); - } catch (RemoteException e) { - // Try one more time - recoverService(); - if (sService == null) { - Log.e(TAG, "Failed to recover CardEmulationService."); - return false; - } - try { - return sService.setPreferredService(service); - } catch (RemoteException ee) { - Log.e(TAG, "Failed to reach CardEmulationService."); - return false; - } - } + return callServiceReturn(() -> sService.setPreferredService(service), false); } /** @@ -843,22 +636,7 @@ public final class CardEmulation { if (activity == null) { throw new NullPointerException("activity is null"); } - try { - return sService.unsetPreferredService(); - } catch (RemoteException e) { - // Try one more time - recoverService(); - if (sService == null) { - Log.e(TAG, "Failed to recover CardEmulationService."); - return false; - } - try { - return sService.unsetPreferredService(); - } catch (RemoteException ee) { - Log.e(TAG, "Failed to reach CardEmulationService."); - return false; - } - } + return callServiceReturn(() -> sService.unsetPreferredService(), false); } /** @@ -872,21 +650,7 @@ public final class CardEmulation { * @return whether AID prefix registering is supported on this device. */ public boolean supportsAidPrefixRegistration() { - try { - return sService.supportsAidPrefixRegistration(); - } catch (RemoteException e) { - recoverService(); - if (sService == null) { - Log.e(TAG, "Failed to recover CardEmulationService."); - return false; - } - try { - return sService.supportsAidPrefixRegistration(); - } catch (RemoteException ee) { - Log.e(TAG, "Failed to reach CardEmulationService."); - return false; - } - } + return callServiceReturn(() -> sService.supportsAidPrefixRegistration(), false); } /** @@ -897,25 +661,9 @@ public final class CardEmulation { @RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO) @Nullable public List<String> getAidsForPreferredPaymentService() { - try { - ApduServiceInfo serviceInfo = sService.getPreferredPaymentService( - mContext.getUser().getIdentifier()); - return (serviceInfo != null ? serviceInfo.getAids() : null); - } catch (RemoteException e) { - recoverService(); - if (sService == null) { - Log.e(TAG, "Failed to recover CardEmulationService."); - throw e.rethrowFromSystemServer(); - } - try { - ApduServiceInfo serviceInfo = - sService.getPreferredPaymentService(mContext.getUser().getIdentifier()); - return (serviceInfo != null ? serviceInfo.getAids() : null); - } catch (RemoteException ee) { - Log.e(TAG, "Failed to recover CardEmulationService."); - throw e.rethrowFromSystemServer(); - } - } + ApduServiceInfo serviceInfo = callServiceReturn(() -> + sService.getPreferredPaymentService(mContext.getUser().getIdentifier()), null); + return (serviceInfo != null ? serviceInfo.getAids() : null); } /** @@ -944,40 +692,16 @@ public final class CardEmulation { @RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO) @Nullable public String getRouteDestinationForPreferredPaymentService() { - try { - ApduServiceInfo serviceInfo = sService.getPreferredPaymentService( - mContext.getUser().getIdentifier()); - if (serviceInfo != null) { - if (!serviceInfo.isOnHost()) { - return serviceInfo.getOffHostSecureElement() == null ? - "OffHost" : serviceInfo.getOffHostSecureElement(); - } - return "Host"; - } - return null; - } catch (RemoteException e) { - recoverService(); - if (sService == null) { - Log.e(TAG, "Failed to recover CardEmulationService."); - throw e.rethrowFromSystemServer(); - } - try { - ApduServiceInfo serviceInfo = - sService.getPreferredPaymentService(mContext.getUser().getIdentifier()); - if (serviceInfo != null) { - if (!serviceInfo.isOnHost()) { - return serviceInfo.getOffHostSecureElement() == null ? - "Offhost" : serviceInfo.getOffHostSecureElement(); - } - return "Host"; - } - return null; - - } catch (RemoteException ee) { - Log.e(TAG, "Failed to recover CardEmulationService."); - throw e.rethrowFromSystemServer(); + ApduServiceInfo serviceInfo = callServiceReturn(() -> + sService.getPreferredPaymentService(mContext.getUser().getIdentifier()), null); + if (serviceInfo != null) { + if (!serviceInfo.isOnHost()) { + return serviceInfo.getOffHostSecureElement() == null ? + "OffHost" : serviceInfo.getOffHostSecureElement(); } + return "Host"; } + return null; } /** @@ -995,115 +719,44 @@ public final class CardEmulation { @RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO) @Nullable public CharSequence getDescriptionForPreferredPaymentService() { - try { - ApduServiceInfo serviceInfo = sService.getPreferredPaymentService( - mContext.getUser().getIdentifier()); - return (serviceInfo != null ? serviceInfo.getDescription() : null); - } catch (RemoteException e) { - recoverService(); - if (sService == null) { - Log.e(TAG, "Failed to recover CardEmulationService."); - throw e.rethrowFromSystemServer(); - } - try { - ApduServiceInfo serviceInfo = - sService.getPreferredPaymentService(mContext.getUser().getIdentifier()); - return (serviceInfo != null ? serviceInfo.getDescription() : null); - } catch (RemoteException ee) { - Log.e(TAG, "Failed to recover CardEmulationService."); - throw e.rethrowFromSystemServer(); - } - } + ApduServiceInfo serviceInfo = callServiceReturn(() -> + sService.getPreferredPaymentService(mContext.getUser().getIdentifier()), null); + return (serviceInfo != null ? serviceInfo.getDescription() : null); } /** * @hide */ public boolean setDefaultServiceForCategory(ComponentName service, String category) { - try { - return sService.setDefaultServiceForCategory(mContext.getUser().getIdentifier(), - service, category); - } catch (RemoteException e) { - // Try one more time - recoverService(); - if (sService == null) { - Log.e(TAG, "Failed to recover CardEmulationService."); - return false; - } - try { - return sService.setDefaultServiceForCategory(mContext.getUser().getIdentifier(), - service, category); - } catch (RemoteException ee) { - Log.e(TAG, "Failed to reach CardEmulationService."); - return false; - } - } + return callServiceReturn(() -> + sService.setDefaultServiceForCategory( + mContext.getUser().getIdentifier(), service, category), false); } /** * @hide */ public boolean setDefaultForNextTap(ComponentName service) { - try { - return sService.setDefaultForNextTap(mContext.getUser().getIdentifier(), service); - } catch (RemoteException e) { - // Try one more time - recoverService(); - if (sService == null) { - Log.e(TAG, "Failed to recover CardEmulationService."); - return false; - } - try { - return sService.setDefaultForNextTap(mContext.getUser().getIdentifier(), service); - } catch (RemoteException ee) { - Log.e(TAG, "Failed to reach CardEmulationService."); - return false; - } - } + return callServiceReturn(() -> + sService.setDefaultForNextTap( + mContext.getUser().getIdentifier(), service), false); } /** * @hide */ public boolean setDefaultForNextTap(int userId, ComponentName service) { - try { - return sService.setDefaultForNextTap(userId, service); - } catch (RemoteException e) { - // Try one more time - recoverService(); - if (sService == null) { - Log.e(TAG, "Failed to recover CardEmulationService."); - return false; - } - try { - return sService.setDefaultForNextTap(userId, service); - } catch (RemoteException ee) { - Log.e(TAG, "Failed to reach CardEmulationService."); - return false; - } - } + return callServiceReturn(() -> + sService.setDefaultForNextTap(userId, service), false); } /** * @hide */ public List<ApduServiceInfo> getServices(String category) { - try { - return sService.getServices(mContext.getUser().getIdentifier(), category); - } catch (RemoteException e) { - // Try one more time - recoverService(); - if (sService == null) { - Log.e(TAG, "Failed to recover CardEmulationService."); - return null; - } - try { - return sService.getServices(mContext.getUser().getIdentifier(), category); - } catch (RemoteException ee) { - Log.e(TAG, "Failed to reach CardEmulationService."); - return null; - } - } + return callServiceReturn(() -> + sService.getServices( + mContext.getUser().getIdentifier(), category), null); } /** @@ -1117,22 +770,8 @@ public final class CardEmulation { @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) @NonNull public List<ApduServiceInfo> getServices(@NonNull String category, @UserIdInt int userId) { - try { - return sService.getServices(userId, category); - } catch (RemoteException e) { - // Try one more time - recoverService(); - if (sService == null) { - Log.e(TAG, "Failed to recover CardEmulationService."); - return null; - } - try { - return sService.getServices(userId, category); - } catch (RemoteException ee) { - Log.e(TAG, "Failed to reach CardEmulationService."); - return null; - } - } + return callServiceReturn(() -> + sService.getServices(userId, category), null); } /** @@ -1222,22 +861,8 @@ public final class CardEmulation { if (service == null) { throw new NullPointerException("activity or service or category is null"); } - try { - return sService.setServiceEnabledForCategoryOther(userId, service, status); - } catch (RemoteException e) { - // Try one more time - recoverService(); - if (sService == null) { - Log.e(TAG, "Failed to recover CardEmulationService."); - return false; - } - try { - return sService.setServiceEnabledForCategoryOther(userId, service, status); - } catch (RemoteException ee) { - Log.e(TAG, "Failed to reach CardEmulationService."); - return false; - } - } + return callServiceReturn(() -> + sService.setServiceEnabledForCategoryOther(userId, service, status), false); } /** @@ -1269,22 +894,9 @@ public final class CardEmulation { if (!activity.isResumed()) { throw new IllegalArgumentException("Activity must be resumed."); } - try { - return sService.overrideRoutingTable(UserHandle.myUserId(), protocol, technology); - } catch (RemoteException e) { - // Try one more time - recoverService(); - if (sService == null) { - Log.e(TAG, "Failed to recover CardEmulationService."); - return false; - } - try { - return sService.overrideRoutingTable(UserHandle.myUserId(), protocol, technology); - } catch (RemoteException ee) { - Log.e(TAG, "Failed to reach CardEmulationService."); - return false; - } - } + return callServiceReturn(() -> + sService.overrideRoutingTable( + mContext.getUser().getIdentifier(), protocol, technology), false); } /** @@ -1303,27 +915,9 @@ public final class CardEmulation { if (!activity.isResumed()) { throw new IllegalArgumentException("Activity must be resumed."); } - try { - return sService.recoverRoutingTable(UserHandle.myUserId()); - } catch (RemoteException e) { - // Try one more time - recoverService(); - if (sService == null) { - Log.e(TAG, "Failed to recover CardEmulationService."); - return false; - } - try { - return sService.recoverRoutingTable(UserHandle.myUserId()); - } catch (RemoteException ee) { - Log.e(TAG, "Failed to reach CardEmulationService."); - return false; - } - } - } - - void recoverService() { - NfcAdapter adapter = NfcAdapter.getDefaultAdapter(mContext); - sService = adapter.getCardEmulationService(); + return callServiceReturn(() -> + sService.recoverRoutingTable( + mContext.getUser().getIdentifier()), false); } /** @@ -1351,4 +945,53 @@ public final class CardEmulation { return ComponentName.unflattenFromString(defaultPaymentComponent); } + + /** @hide */ + interface ServiceCall { + void call() throws RemoteException; + } + /** @hide */ + public static void callService(ServiceCall call) { + try { + if (sService == null) { + NfcAdapter.attemptDeadServiceRecovery( + new RemoteException("NFC CardEmulation Service is null")); + sService = NfcAdapter.getCardEmulationService(); + } + call.call(); + } catch (RemoteException e) { + NfcAdapter.attemptDeadServiceRecovery(e); + sService = NfcAdapter.getCardEmulationService(); + try { + call.call(); + } catch (RemoteException ee) { + ee.rethrowAsRuntimeException(); + } + } + } + /** @hide */ + interface ServiceCallReturn<T> { + T call() throws RemoteException; + } + /** @hide */ + public static <T> T callServiceReturn(ServiceCallReturn<T> call, T defaultReturn) { + try { + if (sService == null) { + NfcAdapter.attemptDeadServiceRecovery( + new RemoteException("NFC CardEmulation Service is null")); + sService = NfcAdapter.getCardEmulationService(); + } + return call.call(); + } catch (RemoteException e) { + NfcAdapter.attemptDeadServiceRecovery(e); + sService = NfcAdapter.getCardEmulationService(); + // Try one more time + try { + return call.call(); + } catch (RemoteException ee) { + ee.rethrowAsRuntimeException(); + } + } + return defaultReturn; + } } 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/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/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPageTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPageTest.kt index d7147b5f7fe7..0d73cb3e63c9 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPageTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPageTest.kt @@ -22,8 +22,9 @@ import android.content.pm.PackageInfo import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsNotDisplayed import androidx.compose.ui.test.assertIsNotEnabled -import androidx.compose.ui.test.assertIsOff -import androidx.compose.ui.test.assertIsOn +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.isOff +import androidx.compose.ui.test.isOn import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onRoot @@ -31,6 +32,7 @@ import androidx.compose.ui.test.performClick import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.settingslib.spa.testutils.FakeNavControllerWrapper +import com.android.settingslib.spa.testutils.waitUntilExists import com.android.settingslib.spaprivileged.R import com.android.settingslib.spaprivileged.model.app.IPackageManagers import com.android.settingslib.spaprivileged.model.enterprise.NoRestricted @@ -99,8 +101,8 @@ class TogglePermissionAppInfoPageTest { setEntryItem(listModel) - composeTestRule.onNodeWithText(context.getString(R.string.app_permission_summary_allowed)) - .assertIsDisplayed() + composeTestRule.waitUntilExists( + hasText(context.getString(R.string.app_permission_summary_allowed))) } @Test @@ -141,8 +143,8 @@ class TogglePermissionAppInfoPageTest { setTogglePermissionAppInfoPage(listModel) - composeTestRule.onNodeWithText(context.getString(listModel.switchTitleResId)) - .assertIsOn() + composeTestRule.waitUntilExists( + hasText(context.getString(listModel.switchTitleResId)) and isOn()) } @Test @@ -151,8 +153,8 @@ class TogglePermissionAppInfoPageTest { setTogglePermissionAppInfoPage(listModel) - composeTestRule.onNodeWithText(context.getString(listModel.switchTitleResId)) - .assertIsOff() + composeTestRule.waitUntilExists( + hasText(context.getString(listModel.switchTitleResId)) and isOff()) } @Test @@ -160,11 +162,10 @@ class TogglePermissionAppInfoPageTest { val listModel = TestTogglePermissionAppListModel(isAllowed = false, isChangeable = true) setTogglePermissionAppInfoPage(listModel) - composeTestRule.onNodeWithText(context.getString(listModel.switchTitleResId)) - .performClick() + composeTestRule.onNodeWithText(context.getString(listModel.switchTitleResId)).performClick() - composeTestRule.onNodeWithText(context.getString(listModel.switchTitleResId)) - .assertIsOn() + composeTestRule.waitUntilExists( + hasText(context.getString(listModel.switchTitleResId)) and isOn()) } @Test @@ -172,11 +173,10 @@ class TogglePermissionAppInfoPageTest { val listModel = TestTogglePermissionAppListModel(isAllowed = false, isChangeable = false) setTogglePermissionAppInfoPage(listModel) - composeTestRule.onNodeWithText(context.getString(listModel.switchTitleResId)) - .performClick() + composeTestRule.onNodeWithText(context.getString(listModel.switchTitleResId)).performClick() - composeTestRule.onNodeWithText(context.getString(listModel.switchTitleResId)) - .assertIsOff() + composeTestRule.waitUntilExists( + hasText(context.getString(listModel.switchTitleResId)) and isOff()) } @Test diff --git a/packages/SettingsLib/aconfig/settingslib.aconfig b/packages/SettingsLib/aconfig/settingslib.aconfig index 754d9423773f..0d124e80dfcf 100644 --- a/packages/SettingsLib/aconfig/settingslib.aconfig +++ b/packages/SettingsLib/aconfig/settingslib.aconfig @@ -108,4 +108,14 @@ flag { namespace: "android_settings" description: "Settings catalyst project migration" bug: "323791114" -}
\ No newline at end of file +} + +flag { + name: "asha_profile_access_profile_enabled_true" + namespace: "accessibility" + description: "Changes the return value of HearingAidProfile.accessProfileEnabled() to true" + bug: "356530795" + metadata { + purpose: PURPOSE_BUGFIX + } +} 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/HearingAidProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidProfile.java index f2450de60878..fb9b4dfc5382 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidProfile.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidProfile.java @@ -34,6 +34,7 @@ import androidx.annotation.NonNull; import com.android.settingslib.R; import com.android.settingslib.Utils; +import com.android.settingslib.flags.Flags; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -141,7 +142,7 @@ public class HearingAidProfile implements LocalBluetoothProfile { } public boolean accessProfileEnabled() { - return false; + return Flags.ashaProfileAccessProfileEnabledTrue(); } public boolean isAutoConnectable() { 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/LocalBluetoothProfileManagerCallbackExt.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManagerCallbackExt.kt new file mode 100644 index 000000000000..b7338fca54ff --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManagerCallbackExt.kt @@ -0,0 +1,43 @@ +/* + * 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 + +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.buffer +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.launch + +/** [Flow] for [LocalBluetoothProfileManager.ServiceListener] service state changes */ +val LocalBluetoothProfileManager.onServiceStateChanged: Flow<Unit> + get() = + callbackFlow { + val listener = + object : LocalBluetoothProfileManager.ServiceListener { + override fun onServiceConnected() { + launch { trySend(Unit) } + } + + override fun onServiceDisconnected() { + launch { trySend(Unit) } + } + } + addServiceListener(listener) + awaitClose { removeServiceListener(listener) } + } + .buffer(capacity = Channel.CONFLATED)
\ No newline at end of file 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 a06f0849c0bc..f7492cfc9a72 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java +++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java @@ -16,8 +16,8 @@ package com.android.settingslib.notification.modes; -import static android.service.notification.ZenModeConfig.UPDATE_ORIGIN_UNKNOWN; -import static android.service.notification.ZenModeConfig.UPDATE_ORIGIN_USER; +import static android.service.notification.ZenModeConfig.ORIGIN_UNKNOWN; +import static android.service.notification.ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI; import android.app.AutomaticZenRule; import android.app.NotificationManager; @@ -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 @@ -154,7 +158,7 @@ public class TestModeBuilder { mRule.setEnabled(enabled); mConfigZenRule.enabled = enabled; if (!enabled) { - mConfigZenRule.disabledOrigin = byUser ? UPDATE_ORIGIN_USER : UPDATE_ORIGIN_UNKNOWN; + mConfigZenRule.disabledOrigin = byUser ? ORIGIN_USER_IN_SYSTEMUI : ORIGIN_UNKNOWN; } return this; } 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 88af7ee3a54f..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; @@ -66,7 +67,7 @@ public class ZenMode implements Parcelable { private static final String TAG = "ZenMode"; - static final String MANUAL_DND_MODE_ID = "manual_dnd"; + static final String MANUAL_DND_MODE_ID = ZenModeConfig.MANUAL_RULE_ID; static final String TEMP_NEW_MODE_ID = "temp_new_mode"; // Must match com.android.server.notification.ZenModeHelper#applyCustomPolicy. @@ -119,7 +120,7 @@ public class ZenMode implements Parcelable { return Status.ENABLED; } } else { - if (zenRuleExtraData.disabledOrigin == ZenModeConfig.UPDATE_ORIGIN_USER) { + if (zenRuleExtraData.disabledOrigin == ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI) { return Status.DISABLED_BY_USER; } else { return Status.DISABLED_BY_OTHER; // by APP, SYSTEM, UNKNOWN. @@ -188,11 +189,37 @@ public class ZenMode implements Parcelable { return mRule.getType(); } + /** Returns the trigger description of the mode. */ @Nullable public String getTriggerDescription() { return mRule.getTriggerDescription(); } + /** + * Returns a "dynamic" trigger description. For some modes (such as manual Do Not Disturb) + * when activated, we know when (and if) the mode is expected to end on its own; this dynamic + * description reflects that. In other cases, returns {@link #getTriggerDescription}. + */ + @Nullable + public String getDynamicDescription(Context context) { + if (isManualDnd() && isActive()) { + long countdownEndTime = tryParseCountdownConditionId(mRule.getConditionId()); + if (countdownEndTime > 0) { + CharSequence formattedTime = ZenModeConfig.getFormattedTime(context, + countdownEndTime, ZenModeConfig.isToday(countdownEndTime), + context.getUserId()); + return context.getString(com.android.internal.R.string.zen_mode_until, + formattedTime); + } + } + // TODO: b/333527800 - For TYPE_SCHEDULE_TIME rules we could do the same; however + // according to the snoozing discussions the mode may or may not end at the scheduled + // time if manually activated. When we resolve that point, we could calculate end time + // for these modes as well. + + return getTriggerDescription(); + } + @NonNull public ListenableFuture<Drawable> getIcon(@NonNull Context context, @NonNull ZenIconLoader iconLoader) { diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenModesBackend.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenModesBackend.java index f533c951d7f8..492828d701b9 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenModesBackend.java +++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenModesBackend.java @@ -116,7 +116,6 @@ public class ZenModesBackend { private ZenMode getManualDndMode(ZenModeConfig config) { ZenModeConfig.ZenRule manualRule = config.manualRule; - // TODO: b/333682392 - Replace with final strings for name & trigger description AutomaticZenRule manualDndRule = new AutomaticZenRule.Builder( mContext.getString(R.string.zen_mode_settings_title), manualRule.conditionId) .setType(manualRule.type) @@ -127,7 +126,7 @@ public class ZenModesBackend { .setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_PRIORITY) .build(); - return ZenMode.manualDndMode(manualDndRule, config != null && config.isManualActive()); + return ZenMode.manualDndMode(manualDndRule, config.isManualActive()); } public void updateMode(ZenMode mode) { diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSharingRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSharingRepository.kt index 99d5891818b6..7a66335ef22f 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 @@ -30,6 +30,7 @@ import com.android.settingslib.bluetooth.BluetoothUtils import com.android.settingslib.bluetooth.LocalBluetoothManager import com.android.settingslib.bluetooth.onBroadcastStartedOrStopped import com.android.settingslib.bluetooth.onProfileConnectionStateChanged +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 @@ -90,13 +91,24 @@ class AudioSharingRepositoryImpl( private val coroutineScope: CoroutineScope, private val backgroundCoroutineContext: CoroutineContext, ) : AudioSharingRepository { + private val isAudioSharingProfilesReady: StateFlow<Boolean> = + btManager.profileManager.onServiceStateChanged + .map { isAudioSharingProfilesReady() } + .onStart { emit(isAudioSharingProfilesReady()) } + .flowOn(backgroundCoroutineContext) + .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), false) + override val inAudioSharing: Flow<Boolean> = - btManager.profileManager.leAudioBroadcastProfile?.let { broadcast -> - broadcast.onBroadcastStartedOrStopped - .map { isBroadcasting() } - .onStart { emit(isBroadcasting()) } - .flowOn(backgroundCoroutineContext) - } ?: flowOf(false) + isAudioSharingProfilesReady.flatMapLatest { ready -> + if (ready) { + btManager.profileManager.leAudioBroadcastProfile.onBroadcastStartedOrStopped + .map { isBroadcasting() } + .onStart { emit(isBroadcasting()) } + .flowOn(backgroundCoroutineContext) + } else { + flowOf(false) + } + } private val primaryChange: Flow<Unit> = callbackFlow { val callback = @@ -108,7 +120,8 @@ class AudioSharingRepositoryImpl( contentResolver.registerContentObserver( Settings.Secure.getUriFor(BluetoothUtils.getPrimaryGroupIdUriForBroadcast()), false, - callback) + callback + ) awaitClose { contentResolver.unregisterContentObserver(callback) } } @@ -120,64 +133,80 @@ class AudioSharingRepositoryImpl( .stateIn( coroutineScope, SharingStarted.WhileSubscribed(), - BluetoothUtils.getPrimaryGroupIdForBroadcast(contentResolver)) + BluetoothCsipSetCoordinator.GROUP_ID_INVALID + ) override val secondaryGroupId: StateFlow<Int> = merge( - btManager.profileManager.leAudioBroadcastAssistantProfile - ?.onSourceConnectedOrRemoved - ?.map { getSecondaryGroupId() } ?: emptyFlow(), - btManager.eventManager.onProfileConnectionStateChanged - .filter { profileConnection -> - profileConnection.state == BluetoothAdapter.STATE_DISCONNECTED && + isAudioSharingProfilesReady.flatMapLatest { ready -> + if (ready) { + btManager.profileManager.leAudioBroadcastAssistantProfile + .onSourceConnectedOrRemoved + .map { getSecondaryGroupId() } + } else { + emptyFlow() + } + }, + btManager.eventManager.onProfileConnectionStateChanged + .filter { profileConnection -> + profileConnection.state == BluetoothAdapter.STATE_DISCONNECTED && profileConnection.bluetoothProfile == - BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT - } - .map { getSecondaryGroupId() }, - primaryGroupId.map { getSecondaryGroupId() }) + BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT + } + .map { getSecondaryGroupId() }, + primaryGroupId.map { getSecondaryGroupId() }) .onStart { emit(getSecondaryGroupId()) } .flowOn(backgroundCoroutineContext) - .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), getSecondaryGroupId()) + .stateIn( + coroutineScope, + SharingStarted.WhileSubscribed(), + BluetoothCsipSetCoordinator.GROUP_ID_INVALID + ) override val volumeMap: StateFlow<GroupIdToVolumes> = - (btManager.profileManager.volumeControlProfile?.let { volumeControl -> - inAudioSharing.flatMapLatest { isSharing -> - if (isSharing) { - callbackFlow { - val callback = - object : BluetoothVolumeControl.Callback { - override fun onDeviceVolumeChanged( - device: BluetoothDevice, - @IntRange( - from = AUDIO_SHARING_VOLUME_MIN.toLong(), - to = AUDIO_SHARING_VOLUME_MAX.toLong()) - volume: Int - ) { - launch { send(Pair(device, volume)) } - } - } - // Once registered, we will receive the initial volume of all - // connected BT devices on VolumeControlProfile via callbacks - volumeControl.registerCallback( - ConcurrentUtils.DIRECT_EXECUTOR, callback) - awaitClose { volumeControl.unregisterCallback(callback) } + inAudioSharing.flatMapLatest { isSharing -> + if (isSharing) { + callbackFlow { + val callback = + object : BluetoothVolumeControl.Callback { + override fun onDeviceVolumeChanged( + device: BluetoothDevice, + @IntRange( + from = AUDIO_SHARING_VOLUME_MIN.toLong(), + to = AUDIO_SHARING_VOLUME_MAX.toLong() + ) + volume: Int + ) { + launch { send(Pair(device, volume)) } } - .runningFold(emptyMap<Int, Int>()) { acc, value -> - val groupId = - BluetoothUtils.getGroupId( - btManager.cachedDeviceManager.findDevice(value.first)) - if (groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) { - acc + Pair(groupId, value.second) - } else { - acc - } - } - .flowOn(backgroundCoroutineContext) - } else { - emptyFlow() + } + // Once registered, we will receive the initial volume of all + // connected BT devices on VolumeControlProfile via callbacks + btManager.profileManager.volumeControlProfile.registerCallback( + ConcurrentUtils.DIRECT_EXECUTOR, callback + ) + awaitClose { + btManager.profileManager.volumeControlProfile.unregisterCallback( + callback + ) } } - } ?: emptyFlow()) + .runningFold(emptyMap<Int, Int>()) { acc, value -> + val groupId = + BluetoothUtils.getGroupId( + btManager.cachedDeviceManager.findDevice(value.first) + ) + if (groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) { + acc + Pair(groupId, value.second) + } else { + acc + } + } + .flowOn(backgroundCoroutineContext) + } else { + emptyFlow() + } + } .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyMap()) override suspend fun setSecondaryVolume( @@ -196,12 +225,25 @@ class AudioSharingRepositoryImpl( } } + private fun isBroadcastProfileReady(): Boolean = + btManager.profileManager.leAudioBroadcastProfile?.isProfileReady ?: false + + private fun isAssistantProfileReady(): Boolean = + btManager.profileManager.leAudioBroadcastAssistantProfile?.isProfileReady ?: false + + private fun isVolumeControlProfileReady(): Boolean = + btManager.profileManager.volumeControlProfile?.isProfileReady ?: false + + private fun isAudioSharingProfilesReady(): Boolean = + isBroadcastProfileReady() && isAssistantProfileReady() && isVolumeControlProfileReady() + private fun isBroadcasting(): Boolean = btManager.profileManager.leAudioBroadcastProfile?.isEnabled(null) ?: false private fun getSecondaryGroupId(): Int = BluetoothUtils.getGroupId( - BluetoothUtils.getSecondaryDeviceForBroadcast(contentResolver, btManager)) + BluetoothUtils.getSecondaryDeviceForBroadcast(contentResolver, btManager) + ) } class AudioSharingRepositoryEmptyImpl : AudioSharingRepository { @@ -215,5 +257,6 @@ class AudioSharingRepositoryEmptyImpl : AudioSharingRepository { override suspend fun setSecondaryVolume( @IntRange(from = AUDIO_SHARING_VOLUME_MIN.toLong(), to = AUDIO_SHARING_VOLUME_MAX.toLong()) volume: Int - ) {} + ) { + } } 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 c54a2e4d479b..078f0c8adba5 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 @@ -57,6 +57,7 @@ import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.eq import org.mockito.Captor import org.mockito.Mock +import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.mockito.Spy @@ -145,8 +146,11 @@ class AudioSharingRepositoryTest { } @Test - fun audioSharingStateChange_emitValues() { + fun audioSharingStateChange_profileReady_emitValues() { testScope.runTest { + `when`(broadcast.isProfileReady).thenReturn(true) + `when`(assistant.isProfileReady).thenReturn(true) + `when`(volumeControl.isProfileReady).thenReturn(true) val states = mutableListOf<Boolean?>() underTest.inAudioSharing.onEach { states.add(it) }.launchIn(backgroundScope) runCurrent() @@ -155,7 +159,19 @@ class AudioSharingRepositoryTest { triggerAudioSharingStateChange(TriggerType.BROADCAST_START, broadcastStarted) runCurrent() - Truth.assertThat(states).containsExactly(true, false, true) + Truth.assertThat(states).containsExactly(false, true, false, true) + } + } + + @Test + fun audioSharingStateChange_profileNotReady_broadcastCallbackNotRegistered() { + testScope.runTest { + val states = mutableListOf<Boolean?>() + underTest.inAudioSharing.onEach { states.add(it) }.launchIn(backgroundScope) + runCurrent() + verify(broadcast, never()).registerServiceCallBack(any(), any()) + + Truth.assertThat(states).containsExactly(false) } } @@ -176,8 +192,21 @@ class AudioSharingRepositoryTest { } @Test - fun secondaryGroupIdChange_emitValues() { + fun secondaryGroupIdChange_profileNotReady_assistantCallbackNotRegistered() { + testScope.runTest { + val groupIds = mutableListOf<Int?>() + underTest.secondaryGroupId.onEach { groupIds.add(it) }.launchIn(backgroundScope) + runCurrent() + verify(assistant, never()).registerServiceCallBack(any(), any()) + } + } + + @Test + fun secondaryGroupIdChange_profileReady_emitValues() { testScope.runTest { + `when`(broadcast.isProfileReady).thenReturn(true) + `when`(assistant.isProfileReady).thenReturn(true) + `when`(volumeControl.isProfileReady).thenReturn(true) val groupIds = mutableListOf<Int?>() underTest.secondaryGroupId.onEach { groupIds.add(it) }.launchIn(backgroundScope) runCurrent() @@ -211,8 +240,11 @@ class AudioSharingRepositoryTest { } @Test - fun volumeMapChange_emitValues() { + fun volumeMapChange_profileReady_emitValues() { testScope.runTest { + `when`(broadcast.isProfileReady).thenReturn(true) + `when`(assistant.isProfileReady).thenReturn(true) + `when`(volumeControl.isProfileReady).thenReturn(true) val volumeMaps = mutableListOf<GroupIdToVolumes?>() underTest.volumeMap.onEach { volumeMaps.add(it) }.launchIn(backgroundScope) runCurrent() @@ -234,6 +266,16 @@ class AudioSharingRepositoryTest { } @Test + fun volumeMapChange_profileNotReady_volumeControlCallbackNotRegistered() { + testScope.runTest { + val volumeMaps = mutableListOf<GroupIdToVolumes?>() + underTest.volumeMap.onEach { volumeMaps.add(it) }.launchIn(backgroundScope) + runCurrent() + verify(volumeControl, never()).registerCallback(any(), any()) + } + } + + @Test fun setSecondaryVolume_setValue() { testScope.runTest { Settings.Secure.putInt( @@ -258,6 +300,7 @@ class AudioSharingRepositoryTest { `when`(broadcast.isEnabled(null)).thenReturn(true) broadcastCallbackCaptor.value.broadcastAction() } + TriggerType.BROADCAST_STOP -> { `when`(broadcast.isEnabled(null)).thenReturn(false) broadcastCallbackCaptor.value.broadcastAction() 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/ZenModeTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java index 651e57c184d7..d9fdcc38b576 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java @@ -93,7 +93,7 @@ public class ZenModeTest { public void constructor_disabledRuleByUser_statusDisabledByUser() { AutomaticZenRule azr = new AutomaticZenRule.Builder(ZEN_RULE).setEnabled(false).build(); ZenModeConfig.ZenRule configZenRule = zenConfigRuleFor(azr, false); - configZenRule.disabledOrigin = ZenModeConfig.UPDATE_ORIGIN_USER; + configZenRule.disabledOrigin = ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI; ZenMode mode = new ZenMode("id", azr, configZenRule); assertThat(mode.getStatus()).isEqualTo(ZenMode.Status.DISABLED_BY_USER); @@ -103,7 +103,7 @@ public class ZenModeTest { public void constructor_disabledRuleByOther_statusDisabledByOther() { AutomaticZenRule azr = new AutomaticZenRule.Builder(ZEN_RULE).setEnabled(false).build(); ZenModeConfig.ZenRule configZenRule = zenConfigRuleFor(azr, false); - configZenRule.disabledOrigin = ZenModeConfig.UPDATE_ORIGIN_APP; + configZenRule.disabledOrigin = ZenModeConfig.ORIGIN_APP; ZenMode mode = new ZenMode("id", azr, configZenRule); assertThat(mode.getStatus()).isEqualTo(ZenMode.Status.DISABLED_BY_OTHER); 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/Android.bp b/packages/SystemUI/Android.bp index c2e8c374bf5f..6d78705d1fbc 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -645,6 +645,15 @@ filegroup { } filegroup { + name: "SystemUI-robotest-utils", + srcs: [ + "tests/robolectric/src/com/android/systemui/testutils/**/*.kt", + "tests/robolectric/src/com/android/systemui/testutils/**/*.java", + ], + path: "tests/robolectric/src/com/android/systemui/testutils", +} + +filegroup { name: "SystemUI-tests-multivalent", srcs: [ "multivalentTests/src/**/*.kt", @@ -944,35 +953,36 @@ android_robolectric_test { strict_mode: false, } -android_ravenwood_test { - name: "SystemUiRavenTests", - srcs: [ - ":SystemUI-tests-utils", - ":SystemUI-tests-multivalent", - // TODO(b/294256649): pivot to using {.aapt.jar} and re-enable - // use_resource_processor: true when better supported by soong - ":SystemUIRobo-stub{.aapt.srcjar}", - ], - static_libs: [ - "SystemUI-core", - "SystemUI-res", - "SystemUI-tests-base", - "androidx.test.uiautomator_uiautomator", - "androidx.core_core-animation-testing", - "androidx.test.ext.junit", - "kosmos", - "mockito-kotlin-nodeps", - ], - libs: [ - "android.test.runner", - "android.test.base", - "android.test.mock", - ], - auto_gen_config: true, - plugins: [ - "dagger2-compiler", - ], -} +// Disable for now. TODO(b/356666754) Re-enable it +// android_ravenwood_test { +// name: "SystemUiRavenTests", +// srcs: [ +// ":SystemUI-tests-utils", +// ":SystemUI-tests-multivalent", +// // TODO(b/294256649): pivot to using {.aapt.jar} and re-enable +// // use_resource_processor: true when better supported by soong +// ":SystemUIRobo-stub{.aapt.srcjar}", +// ], +// static_libs: [ +// "SystemUI-core", +// "SystemUI-res", +// "SystemUI-tests-base", +// "androidx.test.uiautomator_uiautomator", +// "androidx.core_core-animation-testing", +// "androidx.test.ext.junit", +// "kosmos", +// "mockito-kotlin-nodeps", +// ], +// libs: [ +// "android.test.runner", +// "android.test.base", +// "android.test.mock", +// ], +// auto_gen_config: true, +// plugins: [ +// "dagger2-compiler", +// ], +// } // Opt-out config for optimizing the SystemUI target using R8. // Disabled via `export SYSTEMUI_OPTIMIZE_JAVA=false`, or explicitly in Make via diff --git a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/model/A11yMenuShortcut.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/model/A11yMenuShortcut.java index 66a2fae0c4c3..c698d18bfde8 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/model/A11yMenuShortcut.java +++ b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/model/A11yMenuShortcut.java @@ -19,7 +19,6 @@ import android.util.Log; import com.android.systemui.accessibility.accessibilitymenu.R; -import java.util.HashMap; import java.util.Map; /** @@ -52,80 +51,80 @@ public class A11yMenuShortcut { private static final int LABEL_TEXT_INDEX = 3; /** Map stores all shortcut resource IDs that is in matching order of defined shortcut. */ - private static final Map<ShortcutId, int[]> sShortcutResource = new HashMap<>() {{ - put(ShortcutId.ID_ASSISTANT_VALUE, new int[] { + private static final Map<ShortcutId, int[]> sShortcutResource = Map.ofEntries( + Map.entry(ShortcutId.ID_ASSISTANT_VALUE, new int[] { R.drawable.ic_logo_a11y_assistant_24dp, R.color.assistant_color, R.string.assistant_utterance, R.string.assistant_label, - }); - put(ShortcutId.ID_A11YSETTING_VALUE, new int[] { + }), + Map.entry(ShortcutId.ID_A11YSETTING_VALUE, new int[] { R.drawable.ic_logo_a11y_settings_24dp, R.color.a11y_settings_color, R.string.a11y_settings_label, R.string.a11y_settings_label, - }); - put(ShortcutId.ID_POWER_VALUE, new int[] { + }), + Map.entry(ShortcutId.ID_POWER_VALUE, new int[] { R.drawable.ic_logo_a11y_power_24dp, R.color.power_color, R.string.power_utterance, R.string.power_label, - }); - put(ShortcutId.ID_RECENT_VALUE, new int[] { + }), + Map.entry(ShortcutId.ID_RECENT_VALUE, new int[] { R.drawable.ic_logo_a11y_recent_apps_24dp, R.color.recent_apps_color, R.string.recent_apps_label, R.string.recent_apps_label, - }); - put(ShortcutId.ID_LOCKSCREEN_VALUE, new int[] { + }), + Map.entry(ShortcutId.ID_LOCKSCREEN_VALUE, new int[] { R.drawable.ic_logo_a11y_lock_24dp, R.color.lockscreen_color, R.string.lockscreen_label, R.string.lockscreen_label, - }); - put(ShortcutId.ID_QUICKSETTING_VALUE, new int[] { + }), + Map.entry(ShortcutId.ID_QUICKSETTING_VALUE, new int[] { R.drawable.ic_logo_a11y_quick_settings_24dp, R.color.quick_settings_color, R.string.quick_settings_label, R.string.quick_settings_label, - }); - put(ShortcutId.ID_NOTIFICATION_VALUE, new int[] { + }), + Map.entry(ShortcutId.ID_NOTIFICATION_VALUE, new int[] { R.drawable.ic_logo_a11y_notifications_24dp, R.color.notifications_color, R.string.notifications_label, R.string.notifications_label, - }); - put(ShortcutId.ID_SCREENSHOT_VALUE, new int[] { + }), + Map.entry(ShortcutId.ID_SCREENSHOT_VALUE, new int[] { R.drawable.ic_logo_a11y_screenshot_24dp, R.color.screenshot_color, R.string.screenshot_utterance, R.string.screenshot_label, - }); - put(ShortcutId.ID_BRIGHTNESS_UP_VALUE, new int[] { + }), + Map.entry(ShortcutId.ID_BRIGHTNESS_UP_VALUE, new int[] { R.drawable.ic_logo_a11y_brightness_up_24dp, R.color.brightness_color, R.string.brightness_up_label, R.string.brightness_up_label, - }); - put(ShortcutId.ID_BRIGHTNESS_DOWN_VALUE, new int[] { + }), + Map.entry(ShortcutId.ID_BRIGHTNESS_DOWN_VALUE, new int[] { R.drawable.ic_logo_a11y_brightness_down_24dp, R.color.brightness_color, R.string.brightness_down_label, R.string.brightness_down_label, - }); - put(ShortcutId.ID_VOLUME_UP_VALUE, new int[] { + }), + Map.entry(ShortcutId.ID_VOLUME_UP_VALUE, new int[] { R.drawable.ic_logo_a11y_volume_up_24dp, R.color.volume_color, R.string.volume_up_label, R.string.volume_up_label, - }); - put(ShortcutId.ID_VOLUME_DOWN_VALUE, new int[] { + }), + Map.entry(ShortcutId.ID_VOLUME_DOWN_VALUE, new int[] { R.drawable.ic_logo_a11y_volume_down_24dp, R.color.volume_color, R.string.volume_down_label, R.string.volume_down_label, - }); - }}; + }) + ); /** Shortcut id used to identify. */ private int mShortcutId = ShortcutId.UNSPECIFIED_ID_VALUE.ordinal(); diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 197dc6a67249..9046d4e328f4 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" @@ -1048,16 +1025,6 @@ flag { } flag { - namespace: "systemui" - name: "privacy_dot_unfold_wrong_corner_fix" - description: "Fixes an issue where the privacy dot is at the wrong corner after unfolding/folding." - bug: "339335643" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { name: "validate_keyboard_shortcut_helper_icon_uri" namespace: "systemui" description: "Adds a check that the caller can access the content URI of an icon in the shortcut helper." @@ -1219,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." @@ -1282,4 +1260,14 @@ flag { namespace: "systemui" description: "Adding haptic component infrastructure to sliders in Compose." bug: "341968766" +} + +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/facade/enabled/src/com/android/systemui/scene/LockscreenSceneModule.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/LockscreenSceneModule.kt index bd5b795a76c1..72965fb24d89 100644 --- a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/LockscreenSceneModule.kt +++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/LockscreenSceneModule.kt @@ -66,11 +66,11 @@ interface LockscreenSceneModule { @Provides fun providesLockscreenContent( - viewModel: LockscreenContentViewModel, + viewModelFactory: LockscreenContentViewModel.Factory, blueprints: Set<@JvmSuppressWildcards ComposableLockscreenSceneBlueprint>, clockInteractor: KeyguardClockInteractor, ): LockscreenContent { - return LockscreenContent(viewModel, blueprints, clockInteractor) + return LockscreenContent(viewModelFactory, blueprints, clockInteractor) } } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt index d4bad23a1ee9..872bef256f3a 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt @@ -27,7 +27,7 @@ import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.semantics.clearAndSetSemantics -import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.disabled import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -243,7 +243,7 @@ private fun SceneScope.CommunalScene( if (isFocusable) { Modifier.focusable() } else { - Modifier.semantics { contentDescription = "" }.clearAndSetSemantics {} + Modifier.semantics { disabled() }.clearAndSetSemantics {} } ) ) { @@ -259,7 +259,7 @@ private fun SceneScope.CommunalScene( modifier = modifier.focusable(isFocusable).semantics { if (!isFocusable) { - contentDescription = "" + disabled() } } ) 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/LockscreenContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt index 25e91bed5808..672b8a703e2b 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt @@ -29,6 +29,7 @@ import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.ui.composable.blueprint.ComposableLockscreenSceneBlueprint import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel +import com.android.systemui.lifecycle.rememberViewModel /** * Renders the content of the lockscreen. @@ -37,7 +38,7 @@ import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel * outside the scene container framework. */ class LockscreenContent( - private val viewModel: LockscreenContentViewModel, + private val viewModelFactory: LockscreenContentViewModel.Factory, private val blueprints: Set<@JvmSuppressWildcards ComposableLockscreenSceneBlueprint>, private val clockInteractor: KeyguardClockInteractor, ) { @@ -49,6 +50,7 @@ class LockscreenContent( fun SceneScope.Content( modifier: Modifier = Modifier, ) { + val viewModel = rememberViewModel { viewModelFactory.create() } val isContentVisible: Boolean by viewModel.isContentVisible.collectAsStateWithLifecycle() if (!isContentVisible) { // If the content isn't supposed to be visible, show a large empty box as it's needed @@ -69,6 +71,6 @@ class LockscreenContent( } val blueprint = blueprintByBlueprintId[blueprintId] ?: return - with(blueprint) { Content(modifier.sysuiResTag("keyguard_root_view")) } + with(blueprint) { Content(viewModel, modifier.sysuiResTag("keyguard_root_view")) } } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt index b077e183e8ed..7fe1b3e619c1 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt @@ -24,7 +24,7 @@ import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.compose.animation.scene.animateSceneFloatAsState import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.keyguard.ui.viewmodel.LockscreenSceneViewModel +import com.android.systemui.keyguard.ui.viewmodel.LockscreenSceneActionsViewModel import com.android.systemui.qs.ui.composable.QuickSettings import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.ui.composable.ComposableScene @@ -37,13 +37,21 @@ import kotlinx.coroutines.flow.Flow class LockscreenScene @Inject constructor( - viewModel: LockscreenSceneViewModel, + actionsViewModelFactory: LockscreenSceneActionsViewModel.Factory, private val lockscreenContent: Lazy<LockscreenContent>, ) : ComposableScene { override val key = Scenes.Lockscreen + private val actionsViewModel: LockscreenSceneActionsViewModel by lazy { + actionsViewModelFactory.create() + } + override val destinationScenes: Flow<Map<UserAction, UserActionResult>> = - viewModel.destinationScenes + actionsViewModel.actions + + override suspend fun activate() { + actionsViewModel.activate() + } @Composable override fun SceneScope.Content( 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/CommunalBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/CommunalBlueprint.kt index 210ca69e6fd3..adad4468b751 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/CommunalBlueprint.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/CommunalBlueprint.kt @@ -32,16 +32,15 @@ import dagger.multibindings.IntoSet import javax.inject.Inject /** Renders the lockscreen scene when showing the communal glanceable hub. */ -class CommunalBlueprint -@Inject -constructor( - private val viewModel: LockscreenContentViewModel, -) : ComposableLockscreenSceneBlueprint { +class CommunalBlueprint @Inject constructor() : ComposableLockscreenSceneBlueprint { override val id: String = "communal" @Composable - override fun SceneScope.Content(modifier: Modifier) { + override fun SceneScope.Content( + viewModel: LockscreenContentViewModel, + modifier: Modifier, + ) { LockscreenLongPress( viewModel = viewModel.touchHandling, modifier = modifier, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ComposableLockscreenSceneBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ComposableLockscreenSceneBlueprint.kt index cb739830a24b..df36d0774f11 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ComposableLockscreenSceneBlueprint.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ComposableLockscreenSceneBlueprint.kt @@ -20,9 +20,14 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.android.compose.animation.scene.SceneScope import com.android.systemui.keyguard.shared.model.LockscreenSceneBlueprint +import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel /** Defines interface for classes that can render the content for a specific blueprint/layout. */ interface ComposableLockscreenSceneBlueprint : LockscreenSceneBlueprint { /** Renders the content of this blueprint. */ - @Composable fun SceneScope.Content(modifier: Modifier) + @Composable + fun SceneScope.Content( + viewModel: LockscreenContentViewModel, + modifier: Modifier, + ) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt index a9e63c61711f..a3e07016ced1 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt @@ -53,7 +53,6 @@ import kotlin.math.roundToInt class DefaultBlueprint @Inject constructor( - private val viewModel: LockscreenContentViewModel, private val statusBarSection: StatusBarSection, private val lockSection: LockSection, private val ambientIndicationSectionOptional: Optional<AmbientIndicationSection>, @@ -66,10 +65,17 @@ constructor( override val id: String = "default" @Composable - override fun SceneScope.Content(modifier: Modifier) { + 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, @@ -94,6 +100,7 @@ constructor( Box { with(topAreaSection) { DefaultClockLayout( + smartSpacePaddingTop = viewModel::getSmartSpacePaddingTop, modifier = Modifier.thenIf(isShadeLayoutWide) { Modifier.fillMaxWidth(0.5f) @@ -106,6 +113,8 @@ constructor( if (isShadeLayoutWide) { with(notificationSection) { Notifications( + areNotificationsVisible = areNotificationsVisible, + isShadeLayoutWide = isShadeLayoutWide, burnInParams = null, modifier = Modifier.fillMaxWidth(0.5f) @@ -118,6 +127,8 @@ constructor( if (!isShadeLayoutWide) { with(notificationSection) { Notifications( + areNotificationsVisible = areNotificationsVisible, + isShadeLayoutWide = isShadeLayoutWide, burnInParams = null, modifier = Modifier.weight(weight = 1f) ) 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 72cf83269760..000000000000 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt +++ /dev/null @@ -1,248 +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 viewModel: LockscreenContentViewModel, - 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(modifier: Modifier) { - val isUdfpsVisible = viewModel.isUdfpsVisible - val isShadeLayoutWide by viewModel.isShadeLayoutWide.collectAsStateWithLifecycle() - val unfoldTranslations by viewModel.unfoldTranslations.collectAsStateWithLifecycle() - - 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( - modifier = - Modifier.graphicsLayer { - translationX = unfoldTranslations.start - }, - ) - } - if (isShadeLayoutWide) { - with(notificationSection) { - Notifications( - burnInParams = null, - modifier = - Modifier.fillMaxWidth(0.5f) - .fillMaxHeight() - .align(alignment = Alignment.TopEnd) - ) - } - } - } - if (!isShadeLayoutWide) { - with(notificationSection) { - Notifications( - 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 df068c4eb4ef..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,21 +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 androidx.lifecycle.compose.collectAsStateWithLifecycle 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.keyguard.ui.viewmodel.LockscreenContentViewModel 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 @@ -59,7 +54,6 @@ constructor( sharedNotificationContainerViewModel: SharedNotificationContainerViewModel, stackScrollLayout: NotificationStackScrollLayout, sharedNotificationContainerBinder: SharedNotificationContainerBinder, - private val lockscreenContentViewModel: LockscreenContentViewModel, ) { init { @@ -89,23 +83,18 @@ constructor( * adjustment */ @Composable - fun SceneScope.Notifications(burnInParams: BurnInParameters?, modifier: Modifier = Modifier) { - val areNotificationsVisible by - lockscreenContentViewModel - .areNotificationsVisible(contentKey) - .collectAsStateWithLifecycle(initialValue = false) + fun SceneScope.Notifications( + areNotificationsVisible: Boolean, + isShadeLayoutWide: Boolean, + burnInParams: BurnInParameters?, + modifier: Modifier = Modifier + ) { if (!areNotificationsVisible) { return } - val isShadeLayoutWide by - lockscreenContentViewModel.isShadeLayoutWide.collectAsStateWithLifecycle() 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/keyguard/ui/composable/section/SmartSpaceSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/SmartSpaceSection.kt index 33ed14b2e7cc..da78eed2612b 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/SmartSpaceSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/SmartSpaceSection.kt @@ -16,6 +16,7 @@ package com.android.systemui.keyguard.ui.composable.section +import android.content.res.Resources import android.widget.FrameLayout import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.IntrinsicSize @@ -43,7 +44,6 @@ import com.android.systemui.keyguard.ui.composable.modifier.onTopPlacementChange import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel -import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel import com.android.systemui.res.R import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController import javax.inject.Inject @@ -55,12 +55,12 @@ constructor( private val keyguardUnlockAnimationController: KeyguardUnlockAnimationController, private val keyguardSmartspaceViewModel: KeyguardSmartspaceViewModel, private val aodBurnInViewModel: AodBurnInViewModel, - private val lockscreenContentViewModel: LockscreenContentViewModel, ) { @Composable fun SceneScope.SmartSpace( burnInParams: BurnInParameters, onTopChanged: (top: Float?) -> Unit, + smartSpacePaddingTop: (Resources) -> Int, modifier: Modifier = Modifier, ) { val resources = LocalContext.current.resources @@ -72,9 +72,7 @@ constructor( modifier .onTopPlacementChanged(onTopChanged) .padding( - top = { - lockscreenContentViewModel.getSmartSpacePaddingTop(resources) - }, + top = { smartSpacePaddingTop(resources) }, bottom = { resources.getDimensionPixelSize( R.dimen.keyguard_status_view_bottom_margin diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt index c0832d9c4b59..0eeb79b959ce 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt @@ -17,6 +17,7 @@ package com.android.systemui.keyguard.ui.composable.section import android.content.Context +import android.content.res.Resources import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -61,6 +62,7 @@ constructor( ) { @Composable fun SceneScope.DefaultClockLayout( + smartSpacePaddingTop: (Resources) -> Int, modifier: Modifier = Modifier, ) { val currentClockLayout by clockViewModel.currentClockLayout.collectAsStateWithLifecycle() @@ -99,22 +101,41 @@ constructor( SceneTransitionLayout(state) { scene(splitShadeLargeClockScene) { LargeClockWithSmartSpace( + smartSpacePaddingTop = smartSpacePaddingTop, shouldOffSetClockToOneHalf = !hasCustomPositionUpdatedAnimation ) } scene(splitShadeSmallClockScene) { - SmallClockWithSmartSpace(modifier = Modifier.fillMaxWidth(0.5f)) + SmallClockWithSmartSpace( + smartSpacePaddingTop = smartSpacePaddingTop, + modifier = Modifier.fillMaxWidth(0.5f), + ) } - scene(smallClockScene) { SmallClockWithSmartSpace() } + scene(smallClockScene) { + SmallClockWithSmartSpace( + smartSpacePaddingTop = smartSpacePaddingTop, + ) + } - scene(largeClockScene) { LargeClockWithSmartSpace() } + scene(largeClockScene) { + LargeClockWithSmartSpace( + smartSpacePaddingTop = smartSpacePaddingTop, + ) + } - scene(WeatherClockScenes.largeClockScene) { WeatherLargeClockWithSmartSpace() } + scene(WeatherClockScenes.largeClockScene) { + WeatherLargeClockWithSmartSpace( + smartSpacePaddingTop = smartSpacePaddingTop, + ) + } scene(WeatherClockScenes.splitShadeLargeClockScene) { - WeatherLargeClockWithSmartSpace(modifier = Modifier.fillMaxWidth(0.5f)) + WeatherLargeClockWithSmartSpace( + smartSpacePaddingTop = smartSpacePaddingTop, + modifier = Modifier.fillMaxWidth(0.5f), + ) } } with(mediaCarouselSection) { KeyguardMediaCarousel() } @@ -122,7 +143,10 @@ constructor( } @Composable - private fun SceneScope.SmallClockWithSmartSpace(modifier: Modifier = Modifier) { + private fun SceneScope.SmallClockWithSmartSpace( + smartSpacePaddingTop: (Resources) -> Int, + modifier: Modifier = Modifier, + ) { val burnIn = rememberBurnIn(clockInteractor) Column(modifier = modifier) { @@ -137,13 +161,17 @@ constructor( SmartSpace( burnInParams = burnIn.parameters, onTopChanged = burnIn.onSmartspaceTopChanged, + smartSpacePaddingTop = smartSpacePaddingTop, ) } } } @Composable - private fun SceneScope.LargeClockWithSmartSpace(shouldOffSetClockToOneHalf: Boolean = false) { + private fun SceneScope.LargeClockWithSmartSpace( + smartSpacePaddingTop: (Resources) -> Int, + shouldOffSetClockToOneHalf: Boolean = false, + ) { val burnIn = rememberBurnIn(clockInteractor) val isLargeClockVisible by clockViewModel.isLargeClockVisible.collectAsStateWithLifecycle() @@ -158,6 +186,7 @@ constructor( SmartSpace( burnInParams = burnIn.parameters, onTopChanged = burnIn.onSmartspaceTopChanged, + smartSpacePaddingTop = smartSpacePaddingTop, ) } with(clockSection) { @@ -180,7 +209,10 @@ constructor( } @Composable - private fun SceneScope.WeatherLargeClockWithSmartSpace(modifier: Modifier = Modifier) { + private fun SceneScope.WeatherLargeClockWithSmartSpace( + smartSpacePaddingTop: (Resources) -> Int, + modifier: Modifier = Modifier, + ) { val burnIn = rememberBurnIn(clockInteractor) val isLargeClockVisible by clockViewModel.isLargeClockVisible.collectAsStateWithLifecycle() val currentClockState = clockViewModel.currentClock.collectAsStateWithLifecycle() @@ -206,6 +238,7 @@ constructor( SmartSpace( burnInParams = burnIn.parameters, onTopChanged = burnIn.onSmartspaceTopChanged, + smartSpacePaddingTop = smartSpacePaddingTop, modifier = Modifier.heightIn( min = getDimen(context, "enhanced_smartspace_height", density) 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/scene/ui/composable/GoneScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt index a9ddf846c5c3..b0c3fb31256f 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt @@ -31,7 +31,7 @@ import com.android.systemui.qs.ui.composable.QuickSettings import com.android.systemui.qs.ui.composable.QuickSettings.SharedValues.MediaLandscapeTopOffset import com.android.systemui.qs.ui.composable.QuickSettings.SharedValues.MediaOffset.Default import com.android.systemui.scene.shared.model.Scenes -import com.android.systemui.scene.ui.viewmodel.GoneSceneViewModel +import com.android.systemui.scene.ui.viewmodel.GoneSceneActionsViewModel import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel import dagger.Lazy @@ -48,12 +48,18 @@ class GoneScene constructor( private val notificationStackScrolLView: Lazy<NotificationScrollView>, private val notificationsPlaceholderViewModel: NotificationsPlaceholderViewModel, - private val viewModel: GoneSceneViewModel, + private val viewModelFactory: GoneSceneActionsViewModel.Factory, ) : ComposableScene { override val key = Scenes.Gone + private val actionsViewModel: GoneSceneActionsViewModel by lazy { viewModelFactory.create() } + override val destinationScenes: Flow<Map<UserAction, UserActionResult>> = - viewModel.destinationScenes + actionsViewModel.actions + + override suspend fun activate() { + actionsViewModel.activate() + } @Composable override fun SceneScope.Content( diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt index facbcaffcb5a..445ffcb0c60c 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt @@ -53,6 +53,7 @@ import com.android.compose.animation.scene.LowestZIndexContentPicker import com.android.compose.animation.scene.SceneScope import com.android.compose.windowsizeclass.LocalWindowSizeClass import com.android.systemui.keyguard.ui.composable.LockscreenContent +import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.shared.model.ShadeAlignment import com.android.systemui.shade.ui.viewmodel.OverlayShadeViewModel @@ -63,11 +64,12 @@ import java.util.Optional /** The overlay shade renders a lightweight shade UI container on top of a background scene. */ @Composable fun SceneScope.OverlayShade( - viewModel: OverlayShadeViewModel, + viewModelFactory: OverlayShadeViewModel.Factory, lockscreenContent: Lazy<Optional<LockscreenContent>>, modifier: Modifier = Modifier, content: @Composable () -> Unit, ) { + val viewModel = rememberViewModel { viewModelFactory.create() } val backgroundScene by viewModel.backgroundScene.collectAsStateWithLifecycle() Box(modifier) { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt index 1cd48bf2e628..8c53740ebfd0 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt @@ -73,6 +73,7 @@ import com.android.systemui.common.ui.compose.windowinsets.CutoutLocation import com.android.systemui.common.ui.compose.windowinsets.LocalDisplayCutout import com.android.systemui.common.ui.compose.windowinsets.LocalScreenCornerRadius import com.android.systemui.compose.modifiers.sysuiResTag +import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.privacy.OngoingPrivacyChip import com.android.systemui.res.R import com.android.systemui.scene.shared.model.Scenes @@ -122,12 +123,13 @@ object ShadeHeader { @Composable fun SceneScope.CollapsedShadeHeader( - viewModel: ShadeHeaderViewModel, + viewModelFactory: ShadeHeaderViewModel.Factory, createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager, createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController, statusBarIconController: StatusBarIconController, modifier: Modifier = Modifier, ) { + val viewModel = rememberViewModel { viewModelFactory.create() } val isDisabled by viewModel.isDisabled.collectAsStateWithLifecycle() if (isDisabled) { return @@ -279,12 +281,13 @@ fun SceneScope.CollapsedShadeHeader( @Composable fun SceneScope.ExpandedShadeHeader( - viewModel: ShadeHeaderViewModel, + viewModelFactory: ShadeHeaderViewModel.Factory, createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager, createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController, statusBarIconController: StatusBarIconController, modifier: Modifier = Modifier, ) { + val viewModel = rememberViewModel { viewModelFactory.create() } val isDisabled by viewModel.isDisabled.collectAsStateWithLifecycle() if (isDisabled) { return diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt index 77b48d3d307e..0e3fcf4598af 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt @@ -80,6 +80,7 @@ import com.android.systemui.common.ui.compose.windowinsets.LocalDisplayCutout import com.android.systemui.common.ui.compose.windowinsets.LocalScreenCornerRadius import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.media.controls.ui.composable.MediaCarousel import com.android.systemui.media.controls.ui.composable.MediaContentPicker import com.android.systemui.media.controls.ui.composable.shouldElevateMedia @@ -102,7 +103,8 @@ import com.android.systemui.scene.session.ui.composable.SaveableSession import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.ui.composable.ComposableScene import com.android.systemui.shade.shared.model.ShadeMode -import com.android.systemui.shade.ui.viewmodel.ShadeSceneViewModel +import com.android.systemui.shade.ui.viewmodel.ShadeSceneActionsViewModel +import com.android.systemui.shade.ui.viewmodel.ShadeSceneContentViewModel import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel import com.android.systemui.statusbar.phone.StatusBarLocation @@ -145,7 +147,8 @@ class ShadeScene constructor( private val shadeSession: SaveableSession, private val notificationStackScrollView: Lazy<NotificationScrollView>, - private val viewModel: ShadeSceneViewModel, + private val actionsViewModelFactory: ShadeSceneActionsViewModel.Factory, + private val contentViewModelFactory: ShadeSceneContentViewModel.Factory, private val notificationsPlaceholderViewModel: NotificationsPlaceholderViewModel, private val tintedIconManagerFactory: TintedIconManager.Factory, private val batteryMeterViewControllerFactory: BatteryMeterViewController.Factory, @@ -157,12 +160,16 @@ constructor( override val key = Scenes.Shade + private val actionsViewModel: ShadeSceneActionsViewModel by lazy { + actionsViewModelFactory.create() + } + override suspend fun activate() { - viewModel.activate() + actionsViewModel.activate() } override val destinationScenes: Flow<Map<UserAction, UserActionResult>> = - viewModel.destinationScenes + actionsViewModel.actions @Composable override fun SceneScope.Content( @@ -170,7 +177,7 @@ constructor( ) = ShadeScene( notificationStackScrollView.get(), - viewModel = viewModel, + viewModel = rememberViewModel { contentViewModelFactory.create() }, notificationsPlaceholderViewModel = notificationsPlaceholderViewModel, createTintedIconManager = tintedIconManagerFactory::create, createBatteryMeterViewController = batteryMeterViewControllerFactory::create, @@ -196,7 +203,7 @@ constructor( @Composable private fun SceneScope.ShadeScene( notificationStackScrollView: NotificationScrollView, - viewModel: ShadeSceneViewModel, + viewModel: ShadeSceneContentViewModel, notificationsPlaceholderViewModel: NotificationsPlaceholderViewModel, createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager, createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController, @@ -242,7 +249,7 @@ private fun SceneScope.ShadeScene( @Composable private fun SceneScope.SingleShade( notificationStackScrollView: NotificationScrollView, - viewModel: ShadeSceneViewModel, + viewModel: ShadeSceneContentViewModel, notificationsPlaceholderViewModel: NotificationsPlaceholderViewModel, createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager, createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController, @@ -261,7 +268,7 @@ private fun SceneScope.SingleShade( key = QuickSettings.SharedValues.TilesSquishiness, canOverflow = false ) - val isClickable by viewModel.isClickable.collectAsStateWithLifecycle() + val isEmptySpaceClickable by viewModel.isEmptySpaceClickable.collectAsStateWithLifecycle() val isMediaVisible by viewModel.isMediaVisible.collectAsStateWithLifecycle() val shouldPunchHoleBehindScrim = @@ -299,9 +306,9 @@ private fun SceneScope.SingleShade( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth() - .thenIf(isClickable) { + .thenIf(isEmptySpaceClickable) { Modifier.clickable( - onClick = { viewModel.onContentClicked() } + onClick = { viewModel.onEmptySpaceClicked() } ) } .thenIf(cutoutLocation != CutoutLocation.CENTER) { @@ -309,7 +316,7 @@ private fun SceneScope.SingleShade( }, ) { CollapsedShadeHeader( - viewModel = viewModel.shadeHeaderViewModel, + viewModelFactory = viewModel.shadeHeaderViewModelFactory, createTintedIconManager = createTintedIconManager, createBatteryMeterViewController = createBatteryMeterViewController, statusBarIconController = statusBarIconController, @@ -361,6 +368,8 @@ private fun SceneScope.SingleShade( maxScrimTop = { maxNotifScrimTop.value }, shadeMode = ShadeMode.Single, shouldPunchHoleBehindScrim = shouldPunchHoleBehindScrim, + onEmptySpaceClick = + viewModel::onEmptySpaceClicked.takeIf { isEmptySpaceClickable }, ) }, ) @@ -407,7 +416,7 @@ private fun SceneScope.SingleShade( @Composable private fun SceneScope.SplitShade( notificationStackScrollView: NotificationScrollView, - viewModel: ShadeSceneViewModel, + viewModel: ShadeSceneContentViewModel, notificationsPlaceholderViewModel: NotificationsPlaceholderViewModel, createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager, createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController, @@ -468,8 +477,10 @@ private fun SceneScope.SplitShade( } } - val brightnessMirrorShowing by - viewModel.brightnessMirrorViewModel.isShowing.collectAsStateWithLifecycle() + val brightnessMirrorViewModel = rememberViewModel { + viewModel.brightnessMirrorViewModelFactory.create() + } + val brightnessMirrorShowing by brightnessMirrorViewModel.isShowing.collectAsStateWithLifecycle() val contentAlpha by animateFloatAsState( targetValue = if (brightnessMirrorShowing) 0f else 1f, @@ -481,6 +492,7 @@ private fun SceneScope.SplitShade( onDispose { notificationsPlaceholderViewModel.setAlphaForBrightnessMirror(1f) } } + val isEmptySpaceClickable by viewModel.isEmptySpaceClickable.collectAsStateWithLifecycle() val isMediaVisible by viewModel.isMediaVisible.collectAsStateWithLifecycle() val brightnessMirrorShowingModifier = Modifier.graphicsLayer { alpha = contentAlpha } @@ -503,7 +515,7 @@ private fun SceneScope.SplitShade( modifier = Modifier.fillMaxSize(), ) { CollapsedShadeHeader( - viewModel = viewModel.shadeHeaderViewModel, + viewModelFactory = viewModel.shadeHeaderViewModelFactory, createTintedIconManager = createTintedIconManager, createBatteryMeterViewController = createBatteryMeterViewController, statusBarIconController = statusBarIconController, @@ -522,7 +534,7 @@ private fun SceneScope.SplitShade( .graphicsLayer { translationX = unfoldTranslationXForStartSide }, ) { BrightnessMirror( - viewModel = viewModel.brightnessMirrorViewModel, + viewModel = brightnessMirrorViewModel, qsSceneAdapter = viewModel.qsSceneAdapter, // Need to use the offset measured from the container as the header // has to be accounted for @@ -591,6 +603,8 @@ private fun SceneScope.SplitShade( shouldPunchHoleBehindScrim = false, shouldReserveSpaceForNavBar = false, shadeMode = ShadeMode.Split, + onEmptySpaceClick = + viewModel::onEmptySpaceClicked.takeIf { isEmptySpaceClickable }, modifier = Modifier.weight(1f) .fillMaxHeight() diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/bottombar/ui/BottomBarComponent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/bottombar/ui/BottomBarComponent.kt index c08eb94f25c0..981a0ffb5742 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/bottombar/ui/BottomBarComponent.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/bottombar/ui/BottomBarComponent.kt @@ -20,16 +20,14 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import com.android.compose.PlatformButton +import com.android.compose.PlatformOutlinedButton import com.android.systemui.res.R import com.android.systemui.volume.panel.component.bottombar.ui.viewmodel.BottomBarViewModel import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope @@ -51,16 +49,10 @@ constructor( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { - OutlinedButton( - onClick = viewModel::onSettingsClicked, - colors = - ButtonDefaults.outlinedButtonColors( - contentColor = MaterialTheme.colorScheme.onSurface, - ), - ) { + PlatformOutlinedButton(onClick = viewModel::onSettingsClicked) { Text(text = stringResource(R.string.volume_panel_dialog_settings_button)) } - Button(onClick = viewModel::onDoneClicked) { + PlatformButton(onClick = viewModel::onDoneClicked) { Text(stringResource(R.string.inline_done_button)) } } 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/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/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/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt index 3075c54fb069..8236eece9069 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt @@ -34,6 +34,7 @@ import com.android.systemui.keyguard.data.repository.keyguardOcclusionRepository import com.android.systemui.keyguard.shared.model.ClockSize import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope +import com.android.systemui.lifecycle.activateIn import com.android.systemui.res.R import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Scenes @@ -79,6 +80,7 @@ class LockscreenContentViewModelTest(flags: FlagsParameterization) : SysuiTestCa fakeFeatureFlagsClassic.set(Flags.LOCK_SCREEN_LONG_PRESS_ENABLED, true) shadeRepository.setShadeLayoutWide(false) underTest = lockscreenContentViewModel + underTest.activateIn(testScope) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneActionsViewModelTest.kt index bca83f0f1ff7..b3ea03ec2fb5 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneActionsViewModelTest.kt @@ -37,6 +37,7 @@ import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepositor import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.kosmos.testScope +import com.android.systemui.lifecycle.activateIn import com.android.systemui.power.data.repository.fakePowerRepository import com.android.systemui.power.shared.model.WakefulnessState import com.android.systemui.scene.domain.interactor.sceneInteractor @@ -44,9 +45,7 @@ import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.shared.model.TransitionKeys import com.android.systemui.shade.data.repository.shadeRepository import com.android.systemui.shade.domain.interactor.shadeInteractor -import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModel import com.android.systemui.testKosmos -import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat import kotlin.math.pow import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -63,7 +62,7 @@ import platform.test.runner.parameterized.Parameters @RunWith(ParameterizedAndroidJunit4::class) @RunWithLooper @EnableSceneContainer -class LockscreenSceneViewModelTest : SysuiTestCase() { +class LockscreenSceneActionsViewModelTest : SysuiTestCase() { companion object { private const val parameterCount = 6 @@ -172,6 +171,7 @@ class LockscreenSceneViewModelTest : SysuiTestCase() { @EnableFlags(Flags.FLAG_COMMUNAL_HUB) fun destinationScenes() = testScope.runTest { + underTest.activateIn(this) kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true) kosmos.fakeAuthenticationRepository.setAuthenticationMethod( if (canSwipeToEnter) { @@ -192,7 +192,7 @@ class LockscreenSceneViewModelTest : SysuiTestCase() { }, ) - val destinationScenes by collectLastValue(underTest.destinationScenes) + val destinationScenes by collectLastValue(underTest.actions) val downDestination = destinationScenes?.get( Swipe( @@ -255,15 +255,10 @@ class LockscreenSceneViewModelTest : SysuiTestCase() { ) } - private fun createLockscreenSceneViewModel(): LockscreenSceneViewModel { - return LockscreenSceneViewModel( + private fun createLockscreenSceneViewModel(): LockscreenSceneActionsViewModel { + return LockscreenSceneActionsViewModel( deviceEntryInteractor = kosmos.deviceEntryInteractor, communalInteractor = kosmos.communalInteractor, - touchHandling = - KeyguardTouchHandlingViewModel( - interactor = mock(), - ), - notifications = kosmos.notificationsPlaceholderViewModel, shadeInteractor = kosmos.shadeInteractor, ) } 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/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 228d61accce4..66e45ab8ccbe 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt @@ -48,9 +48,9 @@ import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.flags.Flags import com.android.systemui.flags.fakeFeatureFlagsClassic -import com.android.systemui.keyguard.ui.viewmodel.KeyguardTouchHandlingViewModel -import com.android.systemui.keyguard.ui.viewmodel.LockscreenSceneViewModel +import com.android.systemui.keyguard.ui.viewmodel.LockscreenSceneActionsViewModel import com.android.systemui.kosmos.testScope +import com.android.systemui.lifecycle.activateIn 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 @@ -63,15 +63,15 @@ 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.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModel +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 import com.android.systemui.testKosmos import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.android.telecom.telecomManager import com.google.common.truth.Truth.assertThat @@ -141,20 +141,16 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { private lateinit var bouncerActionButtonInteractor: BouncerActionButtonInteractor private lateinit var bouncerViewModel: BouncerViewModel - private val lockscreenSceneViewModel by lazy { - LockscreenSceneViewModel( + private val lockscreenSceneActionsViewModel by lazy { + LockscreenSceneActionsViewModel( deviceEntryInteractor = deviceEntryInteractor, communalInteractor = communalInteractor, - touchHandling = - KeyguardTouchHandlingViewModel( - interactor = mock(), - ), - notifications = kosmos.notificationsPlaceholderViewModel, shadeInteractor = kosmos.shadeInteractor, ) } - private lateinit var shadeSceneViewModel: ShadeSceneViewModel + private lateinit var shadeSceneContentViewModel: ShadeSceneContentViewModel + private lateinit var shadeSceneActionsViewModel: ShadeSceneActionsViewModel private val powerInteractor by lazy { kosmos.powerInteractor } @@ -193,11 +189,16 @@ 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) .isEqualTo(sceneContainerConfig.initialSceneKey) @@ -225,8 +226,8 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { @Test fun swipeUpOnLockscreen_enterCorrectPin_unlocksDevice() = testScope.runTest { - val destinationScenes by collectLastValue(lockscreenSceneViewModel.destinationScenes) - 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, @@ -245,8 +246,8 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { testScope.runTest { setAuthMethod(AuthenticationMethodModel.None, enableLockscreen = true) - val destinationScenes by collectLastValue(lockscreenSceneViewModel.destinationScenes) - 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, @@ -256,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) @@ -265,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( @@ -276,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) @@ -293,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( @@ -351,8 +352,8 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { fun swipeUpOnLockscreenWhileUnlocked_dismissesLockscreen() = testScope.runTest { unlockDevice() - val destinationScenes by collectLastValue(lockscreenSceneViewModel.destinationScenes) - 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) } @@ -373,8 +374,8 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { fun dismissingIme_whileOnPasswordBouncer_navigatesToLockscreen() = testScope.runTest { setAuthMethod(AuthenticationMethodModel.Password) - val destinationScenes by collectLastValue(lockscreenSceneViewModel.destinationScenes) - 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, @@ -391,8 +392,8 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { fun bouncerActionButtonClick_opensEmergencyServicesDialer() = testScope.runTest { setAuthMethod(AuthenticationMethodModel.Password) - val destinationScenes by collectLastValue(lockscreenSceneViewModel.destinationScenes) - 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) @@ -411,8 +412,8 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { testScope.runTest { setAuthMethod(AuthenticationMethodModel.Password) startPhoneCall() - val destinationScenes by collectLastValue(lockscreenSceneViewModel.destinationScenes) - 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/scene/ui/viewmodel/GoneSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/GoneSceneActionsViewModelTest.kt index a4992e2e61d4..b52627570246 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/GoneSceneActionsViewModelTest.kt @@ -25,6 +25,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.kosmos.testScope +import com.android.systemui.lifecycle.activateIn import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade import com.android.systemui.shade.data.repository.shadeRepository import com.android.systemui.shade.domain.interactor.shadeInteractor @@ -42,25 +43,26 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) @TestableLooper.RunWithLooper @EnableSceneContainer -class GoneSceneViewModelTest : SysuiTestCase() { +class GoneSceneActionsViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope private val shadeRepository by lazy { kosmos.shadeRepository } - private lateinit var underTest: GoneSceneViewModel + private lateinit var underTest: GoneSceneActionsViewModel @Before fun setUp() { underTest = - GoneSceneViewModel( + GoneSceneActionsViewModel( shadeInteractor = kosmos.shadeInteractor, ) + underTest.activateIn(testScope) } @Test fun downTransitionKey_splitShadeEnabled_isGoneToSplitShade() = testScope.runTest { - val destinationScenes by collectLastValue(underTest.destinationScenes) + val destinationScenes by collectLastValue(underTest.actions) shadeRepository.setShadeLayoutWide(true) runCurrent() @@ -71,7 +73,7 @@ class GoneSceneViewModelTest : SysuiTestCase() { @Test fun downTransitionKey_splitShadeDisabled_isNull() = testScope.runTest { - val destinationScenes by collectLastValue(underTest.destinationScenes) + val destinationScenes by collectLastValue(underTest.actions) shadeRepository.setShadeLayoutWide(false) runCurrent() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneActionsViewModelTest.kt new file mode 100644 index 000000000000..206d3ac67778 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneActionsViewModelTest.kt @@ -0,0 +1,133 @@ +/* + * 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.scene.ui.viewmodel + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.compose.animation.scene.Back +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.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testScope +import com.android.systemui.lifecycle.activateIn +import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class SceneActionsViewModelTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + + private val underTest = FakeSceneActionsViewModel() + + @Test + fun actions_emptyBeforeActivation() = + testScope.runTest { + val actions by collectLastValue(underTest.actions) + + assertThat(underTest.isActive).isFalse() + assertThat(actions).isEmpty() + } + + @Test + fun actions_emptyBeforeFirstValue() = + testScope.runTest { + val actions by collectLastValue(underTest.actions) + underTest.activateIn(testScope) + runCurrent() + + assertThat(underTest.isActive).isTrue() + assertThat(actions).isEmpty() + } + + @Test + fun actions() = + testScope.runTest { + val actions by collectLastValue(underTest.actions) + underTest.activateIn(testScope) + runCurrent() + assertThat(underTest.isActive).isTrue() + + val expected1 = + mapOf( + Back to UserActionResult(toScene = Scenes.Gone), + Swipe(SwipeDirection.Up) to UserActionResult(toScene = Scenes.Shade) + ) + underTest.upstream.value = expected1 + runCurrent() + assertThat(actions).isEqualTo(expected1) + + val expected2 = + mapOf( + Back to UserActionResult(toScene = Scenes.Lockscreen), + Swipe(SwipeDirection.Down) to UserActionResult(toScene = Scenes.Shade) + ) + underTest.upstream.value = expected2 + runCurrent() + assertThat(actions).isEqualTo(expected2) + } + + @Test + fun actions_emptyAfterCancellation() = + testScope.runTest { + val actions by collectLastValue(underTest.actions) + val job = Job() + underTest.activateIn(testScope, job) + runCurrent() + + val expected = + mapOf( + Back to UserActionResult(toScene = Scenes.Lockscreen), + Swipe(SwipeDirection.Down) to UserActionResult(toScene = Scenes.Shade) + ) + underTest.upstream.value = expected + runCurrent() + assertThat(actions).isEqualTo(expected) + + job.cancel() + runCurrent() + assertThat(underTest.isActive).isFalse() + assertThat(actions).isEmpty() + } + + private class FakeSceneActionsViewModel : SceneActionsViewModel() { + + val upstream = MutableStateFlow<Map<UserAction, UserActionResult>>(emptyMap()) + + override suspend fun hydrateActions( + setActions: (Map<UserAction, UserActionResult>) -> Unit, + ) { + upstream.collectLatest { setActions(it) } + } + } +} 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/NotificationShadeWindowModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/NotificationShadeWindowModelTest.kt index add33dacf669..6a886643cebb 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/NotificationShadeWindowModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/NotificationShadeWindowModelTest.kt @@ -43,7 +43,7 @@ class NotificationShadeWindowModelTest : SysuiTestCase() { } @Test - fun transitionToOccluded() = + fun transitionToOccludedByOCCLUDEDTransition() = testScope.runTest { val isKeyguardOccluded by collectLastValue(underTest.isKeyguardOccluded) assertThat(isKeyguardOccluded).isFalse() @@ -62,4 +62,25 @@ class NotificationShadeWindowModelTest : SysuiTestCase() { ) assertThat(isKeyguardOccluded).isFalse() } + + @Test + fun transitionToOccludedByDREAMINGTransition() = + testScope.runTest { + val isKeyguardOccluded by collectLastValue(underTest.isKeyguardOccluded) + assertThat(isKeyguardOccluded).isFalse() + + keyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.DREAMING, + testScope, + ) + assertThat(isKeyguardOccluded).isTrue() + + keyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.DREAMING, + to = KeyguardState.AOD, + testScope, + ) + assertThat(isKeyguardOccluded).isFalse() + } } 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/events/PrivacyDotViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/events/PrivacyDotViewControllerTest.kt index f12643280c26..663c3418b144 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/events/PrivacyDotViewControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/events/PrivacyDotViewControllerTest.kt @@ -18,8 +18,6 @@ package com.android.systemui.statusbar.events import android.graphics.Point import android.graphics.Rect -import android.platform.test.annotations.DisableFlags -import android.platform.test.annotations.EnableFlags import android.testing.TestableLooper.RunWithLooper import android.view.Display import android.view.DisplayAdjustments @@ -28,7 +26,6 @@ import android.widget.FrameLayout import android.widget.FrameLayout.LayoutParams.UNSPECIFIED_GRAVITY 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.res.R import com.android.systemui.statusbar.FakeStatusBarStateController @@ -297,8 +294,7 @@ class PrivacyDotViewControllerTest : SysuiTestCase() { } @Test - @EnableFlags(Flags.FLAG_PRIVACY_DOT_UNFOLD_WRONG_CORNER_FIX) - fun initialize_newViews_fixFlagEnabled_gravityIsUpdated() { + fun initialize_newViews_gravityIsUpdated() { val newTopLeftView = initDotView() val newTopRightView = initDotView() val newBottomLeftView = initDotView() @@ -318,28 +314,6 @@ class PrivacyDotViewControllerTest : SysuiTestCase() { .isNotEqualTo(UNSPECIFIED_GRAVITY) } - @Test - @DisableFlags(Flags.FLAG_PRIVACY_DOT_UNFOLD_WRONG_CORNER_FIX) - fun initialize_newViews_fixFlagDisabled_gravityIsNotUpdated() { - val newTopLeftView = initDotView() - val newTopRightView = initDotView() - val newBottomLeftView = initDotView() - val newBottomRightView = initDotView() - setRotation(ROTATION_LANDSCAPE) // Bottom right used in landscape - - val controller = createAndInitializeController() - // Re-init with different views, but same rotation - controller.initialize( - newTopLeftView, - newTopRightView, - newBottomLeftView, - newBottomRightView - ) - - assertThat((newBottomRightView.layoutParams as FrameLayout.LayoutParams).gravity) - .isEqualTo(UNSPECIFIED_GRAVITY) - } - private fun setRotation(rotation: Int) { whenever(mockDisplay.rotation).thenReturn(rotation) } @@ -347,7 +321,7 @@ class PrivacyDotViewControllerTest : SysuiTestCase() { private fun initDotView(): View { val privacyDot = View(context).also { it.id = R.id.privacy_dot } return FrameLayout(context).also { - it.layoutParams = FrameLayout.LayoutParams(/* width = */ 0, /* height = */ 0) + it.layoutParams = FrameLayout.LayoutParams(/* width= */ 0, /* height= */ 0) it.addView(privacyDot) } } 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/phone/DozeServiceHostTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java index 2114489d59c4..64414050e0f4 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java @@ -88,7 +88,6 @@ public class DozeServiceHostTest extends SysuiTestCase { @Mock private PowerManager mPowerManager; @Mock private WakefulnessLifecycle mWakefullnessLifecycle; @Mock private CentralSurfaces mCentralSurfaces; - @Mock private NotificationIconAreaController mNotificationIconAreaController; @Mock private NotificationShadeWindowViewController mNotificationShadeWindowViewController; @Mock private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; @Mock private ShadeLockscreenInteractor mShadeLockscreenInteractor; @@ -106,7 +105,7 @@ public class DozeServiceHostTest extends SysuiTestCase { mHeadsUpManager, mBatteryController, mScrimController, () -> mBiometricUnlockController, () -> mAssistManager, mDozeScrimController, mKeyguardUpdateMonitor, mPulseExpansionHandler, mNotificationShadeWindowController, - mNotificationWakeUpCoordinator, mAuthController, mNotificationIconAreaController, + mNotificationWakeUpCoordinator, mAuthController, mShadeLockscreenInteractor, mDozeInteractor); mDozeServiceHost.initialize( 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/tests/src/com/android/systemui/util/sensors/ProximityCheckTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/sensors/ProximityCheckTest.java index 5dd008ac10f7..0eea120e4de0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/util/sensors/ProximityCheckTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/sensors/ProximityCheckTest.java @@ -74,6 +74,26 @@ public class ProximityCheckTest extends SysuiTestCase { } @Test + public void testRecursiveCheck() { + mProximityCheck.check(100, event-> mProximityCheck.check(100, mTestableCallback)); + + assertThat(mTestableCallback.mNumCalls).isEqualTo(0); + assertThat(mTestableCallback.mLastResult).isNull(); + + mFakeExecutor.advanceClockToNext(); + mFakeExecutor.runAllReady(); + + assertThat(mTestableCallback.mNumCalls).isEqualTo(0); + assertThat(mTestableCallback.mLastResult).isNull(); + + mFakeProximitySensor.setLastEvent(new ThresholdSensorEvent(true, 0)); + mFakeProximitySensor.alertListeners(); + + assertThat(mTestableCallback.mNumCalls).isEqualTo(1); + assertThat(mTestableCallback.mLastResult).isTrue(); + } + + @Test public void testTimeout() { mProximityCheck.check(100, mTestableCallback); 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/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/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 159fb2e50c43..a5fd5b95315e 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -971,8 +971,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> @@ -1367,13 +1367,18 @@ <!-- Media projection that launched from 1P/3P apps --> <!-- 1P/3P app media projection permission dialog title. [CHAR LIMIT=NONE] --> - <string name="media_projection_entry_app_permission_dialog_title">Start recording or casting with <xliff:g id="app_seeking_permission" example="Meet">%s</xliff:g>?</string> + <string name="media_projection_entry_app_permission_dialog_title">Share your screen with <xliff:g id="app_seeking_permission" example="Meet">%s</xliff:g>?</string> + + <!-- 1P/3P app media projection permission option for capturing just a single app [CHAR LIMIT=50] --> + <string name="media_projection_entry_app_permission_dialog_option_text_single_app">Share one app</string> + <!-- 1P/3P app media projection permission option for capturing the whole screen [CHAR LIMIT=50] --> + <string name="media_projection_entry_app_permission_dialog_option_text_entire_screen">Share entire screen</string> <!-- 1P/3P app media projection permission warning for capturing the whole screen. [CHAR LIMIT=350] --> - <string name="media_projection_entry_app_permission_dialog_warning_entire_screen">When you’re sharing, recording, or casting, <xliff:g id="app_seeking_permission" example="Meet">%s</xliff:g> has access to anything visible on your screen or played on your device. So be careful with things like passwords, payment details, messages, photos, and audio and video.</string> + <string name="media_projection_entry_app_permission_dialog_warning_entire_screen">When you’re sharing your entire screen, anything on your screen is visible to <xliff:g id="app_seeking_permission" example="Meet">%s</xliff:g>. So be careful with things like passwords, payment details, messages, photos, and audio and video.</string> <!-- 1P/3P app media projection permission warning for capturing an app. [CHAR LIMIT=350] --> - <string name="media_projection_entry_app_permission_dialog_warning_single_app">When you’re sharing, recording, or casting an app, <xliff:g id="app_seeking_permission" example="Meet">%s</xliff:g> has access to anything shown or played on that app. So be careful with things like passwords, payment details, messages, photos, and audio and video.</string> + <string name="media_projection_entry_app_permission_dialog_warning_single_app">When you’re sharing an app, anything shown or played in that app is visible to <xliff:g id="app_seeking_permission" example="Meet">%s</xliff:g>. So be careful with things like passwords, payment details, messages, photos, and audio and video.</string> <!-- 1P/3P apps media projection permission button to continue with app selection or recording [CHAR LIMIT=60] --> - <string name="media_projection_entry_app_permission_dialog_continue">Start</string> + <string name="media_projection_entry_app_permission_dialog_continue_entire_screen">Share screen</string> <!-- 1P/3P apps disabled the single app projection option. [CHAR LIMIT=NONE] --> <string name="media_projection_entry_app_permission_dialog_single_app_disabled"><xliff:g id="app_name" example="Meet">%1$s</xliff:g> has disabled this option</string> @@ -1389,7 +1394,7 @@ <!-- System casting media projection permission warning for capturing a single app when SysUI casting requests it. [CHAR LIMIT=350] --> <string name="media_projection_entry_cast_permission_dialog_warning_single_app">When you’re casting an app, anything shown or played in that app is visible. So be careful with things like passwords, payment details, messages, photos, and audio and video.</string> <!-- System casting media projection permission button to continue for SysUI casting. [CHAR LIMIT=60] --> - <string name="media_projection_entry_cast_permission_dialog_continue">Start casting</string> + <string name="media_projection_entry_cast_permission_dialog_continue_entire_screen">Cast screen</string> <!-- Other sharing (not recording nor casting) that launched by SysUI (currently not in use) --> <!-- System sharing media projection permission dialog title. [CHAR LIMIT=100] --> @@ -1400,6 +1405,8 @@ <string name="media_projection_entry_generic_permission_dialog_warning_single_app">When you’re sharing, recording, or casting 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> <!-- System sharing media projection permission button to continue. [CHAR LIMIT=60] --> <string name="media_projection_entry_generic_permission_dialog_continue">Start</string> + <!-- System sharing media projection permission button to continue to the next step. [CHAR LIMIT=60] --> + <string name="media_projection_entry_generic_permission_dialog_continue_single_app">Next</string> <!-- Task switcher notification --> <!-- Task switcher notification text. [CHAR LIMIT=100] --> @@ -3672,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] --> @@ -3680,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/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/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java index f688d4f17841..d468f2f0b0aa 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java @@ -60,9 +60,7 @@ import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController; import com.android.systemui.statusbar.notification.AnimatableProperty; import com.android.systemui.statusbar.notification.PropertyAnimator; import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerAlwaysOnDisplayViewBinder; -import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor; import com.android.systemui.statusbar.notification.stack.AnimationProperties; -import com.android.systemui.statusbar.phone.NotificationIconAreaController; import com.android.systemui.statusbar.phone.NotificationIconContainer; import com.android.systemui.util.ViewController; import com.android.systemui.util.concurrency.DelayableExecutor; @@ -87,7 +85,6 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS private final StatusBarStateController mStatusBarStateController; private final ClockRegistry mClockRegistry; private final KeyguardSliceViewController mKeyguardSliceViewController; - private final NotificationIconAreaController mNotificationIconAreaController; private final LockscreenSmartspaceController mSmartspaceController; private final SecureSettings mSecureSettings; private final DumpManager mDumpManager; @@ -165,7 +162,6 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS StatusBarStateController statusBarStateController, ClockRegistry clockRegistry, KeyguardSliceViewController keyguardSliceViewController, - NotificationIconAreaController notificationIconAreaController, LockscreenSmartspaceController smartspaceController, NotificationIconContainerAlwaysOnDisplayViewBinder nicViewBinder, KeyguardUnlockAnimationController keyguardUnlockAnimationController, @@ -183,7 +179,6 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS mStatusBarStateController = statusBarStateController; mClockRegistry = clockRegistry; mKeyguardSliceViewController = keyguardSliceViewController; - mNotificationIconAreaController = notificationIconAreaController; mSmartspaceController = smartspaceController; mNicViewBinder = nicViewBinder; mSecureSettings = secureSettings; @@ -351,10 +346,8 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS int getNotificationIconAreaHeight() { if (MigrateClocksToBlueprint.isEnabled()) { return 0; - } else if (NotificationIconContainerRefactor.isEnabled()) { - return mAodIconContainer != null ? mAodIconContainer.getHeight() : 0; } else { - return mNotificationIconAreaController.getHeight(); + return mAodIconContainer != null ? mAodIconContainer.getHeight() : 0; } } @@ -603,16 +596,11 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS NotificationIconContainer nic = (NotificationIconContainer) mView.findViewById( com.android.systemui.res.R.id.left_aligned_notification_icon_container); - if (NotificationIconContainerRefactor.isEnabled()) { - if (mAodIconsBindHandle != null) { - mAodIconsBindHandle.dispose(); - } - if (nic != null) { - mAodIconsBindHandle = mNicViewBinder.bindWhileAttached(nic); - mAodIconContainer = nic; - } - } else { - mNotificationIconAreaController.setupAodIcons(nic); + if (mAodIconsBindHandle != null) { + mAodIconsBindHandle.dispose(); + } + if (nic != null) { + mAodIconsBindHandle = mNicViewBinder.bindWhileAttached(nic); mAodIconContainer = nic; } } 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/ui/binder/BiometricViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt index 2d525aa6fcba..43ba097684d6 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 @@ -699,7 +699,12 @@ class Spaghetti( } fun startTransitionToCredentialUI(isError: Boolean) { - applicationScope.launch { + if (!constraintBp()) { + applicationScope.launch { + viewModel.onSwitchToCredential() + legacyCallback?.onUseDeviceCredential() + } + } else { viewModel.onSwitchToCredential() legacyCallback?.onUseDeviceCredential() } 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 fbc64708c3af..214420d45560 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 @@ -833,7 +833,7 @@ constructor( messageJob?.cancel() messageJob = null - if (helpMessage.isNotBlank()) { + if (helpMessage.isNotBlank() && needsUserConfirmation) { showHelp(helpMessage) } } 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/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt index 42866465a0cc..b0f2c18db565 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt @@ -30,7 +30,7 @@ import com.android.systemui.dreams.AssistantAttentionMonitor import com.android.systemui.dreams.DreamMonitor import com.android.systemui.dreams.homecontrols.HomeControlsDreamStartable import com.android.systemui.globalactions.GlobalActionsComponent -import com.android.systemui.inputdevice.oobe.KeyboardTouchpadOobeTutorialCoreStartable +import com.android.systemui.inputdevice.tutorial.KeyboardTouchpadTutorialCoreStartable import com.android.systemui.keyboard.KeyboardUI import com.android.systemui.keyboard.PhysicalKeyboardCoreStartable import com.android.systemui.keyguard.KeyguardViewConfigurator @@ -258,9 +258,9 @@ abstract class SystemUICoreStartableModule { @Binds @IntoMap - @ClassKey(KeyboardTouchpadOobeTutorialCoreStartable::class) - abstract fun bindOobeSchedulerCoreStartable( - listener: KeyboardTouchpadOobeTutorialCoreStartable + @ClassKey(KeyboardTouchpadTutorialCoreStartable::class) + abstract fun bindKeyboardTouchpadTutorialCoreStartable( + listener: KeyboardTouchpadTutorialCoreStartable ): CoreStartable @Binds diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index 609aa39a025f..25b6b14049a6 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -132,7 +132,6 @@ import com.android.systemui.statusbar.notification.row.dagger.NotificationRowCom import com.android.systemui.statusbar.phone.CentralSurfaces; import com.android.systemui.statusbar.phone.ConfigurationControllerModule; import com.android.systemui.statusbar.phone.LetterboxModule; -import com.android.systemui.statusbar.phone.NotificationIconAreaControllerModule; import com.android.systemui.statusbar.pipeline.dagger.StatusBarPipelineModule; import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.statusbar.policy.KeyguardStateController; @@ -230,7 +229,6 @@ import javax.inject.Named; MediaProjectionTaskSwitcherModule.class, MediaRouterModule.class, MotionToolModule.class, - NotificationIconAreaControllerModule.class, PeopleHubModule.class, PeopleModule.class, PluginModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractor.kt index 195aa5f4d31f..28db3b861278 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractor.kt @@ -20,6 +20,7 @@ import android.content.Context import android.content.Intent import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor +import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.deviceentry.shared.model.BiometricMessage @@ -33,6 +34,7 @@ import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus import com.android.systemui.plugins.ActivityStarter import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.util.kotlin.combine import com.android.systemui.util.kotlin.sample import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -64,6 +66,7 @@ constructor( activityStarter: ActivityStarter, powerInteractor: PowerInteractor, keyguardTransitionInteractor: KeyguardTransitionInteractor, + communalSceneInteractor: CommunalSceneInteractor, ) { private val keyguardOccludedByApp: Flow<Boolean> = if (KeyguardWmStateRefactor.isEnabled) { @@ -75,12 +78,20 @@ constructor( primaryBouncerInteractor.isShowing, alternateBouncerInteractor.isVisible, keyguardInteractor.isDozing, - ) { occluded, showing, primaryBouncerShowing, alternateBouncerVisible, dozing -> + communalSceneInteractor.isIdleOnCommunal, + ) { + occluded, + showing, + primaryBouncerShowing, + alternateBouncerVisible, + dozing, + isIdleOnCommunal -> occluded && showing && !primaryBouncerShowing && !alternateBouncerVisible && - !dozing + !dozing && + !isIdleOnCommunal } .distinctUntilChanged() } 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 59de2032c4f1..cd0b3f9b6693 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt @@ -34,19 +34,14 @@ 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 -import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor import com.android.systemui.statusbar.notification.interruption.VisualInterruptionRefactor import com.android.systemui.statusbar.notification.shared.NotificationAvalancheSuppression -import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor -import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor import com.android.systemui.statusbar.notification.shared.PriorityPeopleSection import javax.inject.Inject @@ -61,11 +56,9 @@ class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, ha modesUi dependsOn modesApi // Internal notification frontend dependencies - NotificationsLiveDataStoreRefactor.token dependsOn NotificationIconContainerRefactor.token - FooterViewRefactor.token dependsOn NotificationIconContainerRefactor.token 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 @@ -81,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/KeyboardTouchpadOobeTutorialCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/KeyboardTouchpadTutorialCoreStartable.kt index 701d3da1ee66..e8e1dd4c85d0 100644 --- a/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/KeyboardTouchpadOobeTutorialCoreStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/KeyboardTouchpadTutorialCoreStartable.kt @@ -14,23 +14,24 @@ * limitations under the License. */ -package com.android.systemui.inputdevice.oobe +package com.android.systemui.inputdevice.tutorial import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.inputdevice.oobe.domain.interactor.OobeSchedulerInteractor +import com.android.systemui.inputdevice.tutorial.domain.interactor.TutorialSchedulerInteractor import com.android.systemui.shared.Flags.newTouchpadGesturesTutorial import dagger.Lazy import javax.inject.Inject -/** A [CoreStartable] to launch a scheduler for keyboard and touchpad OOBE education */ +/** A [CoreStartable] to launch a scheduler for keyboard and touchpad education */ @SysUISingleton -class KeyboardTouchpadOobeTutorialCoreStartable +class KeyboardTouchpadTutorialCoreStartable @Inject -constructor(private val oobeSchedulerInteractor: Lazy<OobeSchedulerInteractor>) : CoreStartable { +constructor(private val tutorialSchedulerInteractor: Lazy<TutorialSchedulerInteractor>) : + CoreStartable { override fun start() { if (newTouchpadGesturesTutorial()) { - oobeSchedulerInteractor.get().start() + tutorialSchedulerInteractor.get().start() } } } diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/data/model/OobeSchedulerInfo.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/data/model/TutorialSchedulerInfo.kt index e5aedc031ebe..cfe64e269c95 100644 --- a/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/data/model/OobeSchedulerInfo.kt +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/data/model/TutorialSchedulerInfo.kt @@ -14,14 +14,14 @@ * limitations under the License. */ -package com.android.systemui.inputdevice.oobe.data.model +package com.android.systemui.inputdevice.tutorial.data.model -data class OobeSchedulerInfo( +data class TutorialSchedulerInfo( val keyboard: DeviceSchedulerInfo = DeviceSchedulerInfo(), val touchpad: DeviceSchedulerInfo = DeviceSchedulerInfo() ) -data class DeviceSchedulerInfo(var isLaunched: Boolean = false, var connectionTime: Long? = null) { +data class DeviceSchedulerInfo(var isLaunched: Boolean = false, var connectTime: Long? = null) { val wasEverConnected: Boolean - get() = connectionTime != null + 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 new file mode 100644 index 000000000000..31ff01836428 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/data/repository/TutorialSchedulerRepository.kt @@ -0,0 +1,83 @@ +/* + * 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.data.repository + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +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.inputdevice.tutorial.data.model.DeviceSchedulerInfo +import com.android.systemui.inputdevice.tutorial.data.model.TutorialSchedulerInfo +import javax.inject.Inject +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map + +@SysUISingleton +class TutorialSchedulerRepository +@Inject +constructor(@Application private val applicationContext: Context) { + + private val Context.dataStore: DataStore<Preferences> by + preferencesDataStore(name = DATASTORE_NAME) + + suspend fun loadData(): TutorialSchedulerInfo { + return applicationContext.dataStore.data.map { pref -> getSchedulerInfo(pref) }.first() + } + + suspend fun updateConnectTime(device: DeviceType, time: Long) { + applicationContext.dataStore.edit { pref -> pref[getConnectKey(device)] = time } + } + + suspend fun updateLaunch(device: DeviceType) { + 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 getDeviceSchedulerInfo(pref: Preferences, device: DeviceType): DeviceSchedulerInfo { + val isLaunched = pref[getLaunchedKey(device)] ?: false + val connectionTime = pref[getConnectKey(device)] ?: null + return DeviceSchedulerInfo(isLaunched, connectionTime) + } + + private fun getLaunchedKey(device: DeviceType) = + booleanPreferencesKey(device.name + IS_LAUNCHED_SUFFIX) + + private fun getConnectKey(device: DeviceType) = + longPreferencesKey(device.name + CONNECT_TIME_SUFFIX) + + companion object { + const val DATASTORE_NAME = "TutorialScheduler" + const val IS_LAUNCHED_SUFFIX = "_IS_LAUNCHED" + const val CONNECT_TIME_SUFFIX = "_CONNECTED_TIME" + } +} + +enum class DeviceType { + KEYBOARD, + TOUCHPAD +} diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/domain/interactor/OobeSchedulerInteractor.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialSchedulerInteractor.kt index b014c08d4564..05e104468f67 100644 --- a/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/domain/interactor/OobeSchedulerInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialSchedulerInteractor.kt @@ -14,14 +14,15 @@ * limitations under the License. */ -package com.android.systemui.inputdevice.oobe.domain.interactor +package com.android.systemui.inputdevice.tutorial.domain.interactor import android.content.Context import android.content.Intent import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.inputdevice.oobe.data.model.DeviceSchedulerInfo -import com.android.systemui.inputdevice.oobe.data.model.OobeSchedulerInfo +import com.android.systemui.inputdevice.tutorial.data.model.DeviceSchedulerInfo +import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType +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 @@ -35,49 +36,65 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch /** - * When the first time a keyboard or touchpad id connected, wait for [LAUNCH_DELAY], then launch the + * When the first time a keyboard or touchpad is connected, wait for [LAUNCH_DELAY], then launch the * tutorial as soon as there's a connected device */ @SysUISingleton -class OobeSchedulerInteractor +class TutorialSchedulerInteractor @Inject constructor( @Application private val context: Context, @Application private val applicationScope: CoroutineScope, private val keyboardRepository: KeyboardRepository, - private val touchpadRepository: TouchpadRepository + private val touchpadRepository: TouchpadRepository, + private val tutorialSchedulerRepository: TutorialSchedulerRepository ) { - private val info = OobeSchedulerInfo() - fun start() { - if (!info.keyboard.isLaunched) { - applicationScope.launch { - schedule(keyboardRepository.isAnyKeyboardConnected, info.keyboard) + 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) + if (!info.touchpad.isLaunched) { + applicationScope.launch { + schedule( + touchpadRepository.isAnyTouchpadConnected, + info.touchpad, + DeviceType.TOUCHPAD + ) + } } } } - private suspend fun schedule(isAnyDeviceConnected: Flow<Boolean>, info: DeviceSchedulerInfo) { + private suspend fun schedule( + isAnyDeviceConnected: Flow<Boolean>, + info: DeviceSchedulerInfo, + deviceType: DeviceType + ) { if (!info.wasEverConnected) { waitForDeviceConnection(isAnyDeviceConnected) - info.connectionTime = Instant.now().toEpochMilli() + info.connectTime = Instant.now().toEpochMilli() + tutorialSchedulerRepository.updateConnectTime(deviceType, info.connectTime!!) } - delay(remainingTimeMillis(info.connectionTime!!)) + delay(remainingTimeMillis(info.connectTime!!)) waitForDeviceConnection(isAnyDeviceConnected) info.isLaunched = true - launchOobe() + tutorialSchedulerRepository.updateLaunch(deviceType) + launchTutorial() } private suspend fun waitForDeviceConnection(isAnyDeviceConnected: Flow<Boolean>): Boolean { return isAnyDeviceConnected.filter { it }.first() } - private fun launchOobe() { + private fun launchTutorial() { val intent = Intent(TUTORIAL_ACTION) intent.addCategory(Intent.CATEGORY_DEFAULT) intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) @@ -90,7 +107,6 @@ constructor( } companion object { - const val TAG = "OobeSchedulerInteractor" const val TUTORIAL_ACTION = "com.android.systemui.action.TOUCHPAD_TUTORIAL" private val LAUNCH_DELAY = Duration.ofHours(72).toMillis() } 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/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt index 80cf4c50866c..ba533ce3b1bc 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt @@ -106,7 +106,7 @@ constructor( private val falsingManager: FalsingManager, private val keyguardClockViewModel: KeyguardClockViewModel, private val smartspaceViewModel: KeyguardSmartspaceViewModel, - private val lockscreenContentViewModel: LockscreenContentViewModel, + private val lockscreenContentViewModelFactory: LockscreenContentViewModel.Factory, private val lockscreenSceneBlueprintsLazy: Lazy<Set<LockscreenSceneBlueprint>>, private val clockInteractor: KeyguardClockInteractor, private val keyguardViewMediator: KeyguardViewMediator, @@ -143,7 +143,7 @@ constructor( val composeView = createLockscreen( context = context, - viewModel = lockscreenContentViewModel, + viewModelFactory = lockscreenContentViewModelFactory, blueprints = lockscreenSceneBlueprintsLazy.get(), ) composeView.id = View.generateViewId() @@ -224,7 +224,7 @@ constructor( private fun createLockscreen( context: Context, - viewModel: LockscreenContentViewModel, + viewModelFactory: LockscreenContentViewModel.Factory, blueprints: Set<@JvmSuppressWildcards LockscreenSceneBlueprint>, ): View { val sceneBlueprints = @@ -239,7 +239,7 @@ constructor( scene(currentScene) { with( LockscreenContent( - viewModel = viewModel, + viewModelFactory = viewModelFactory, blueprints = sceneBlueprints, clockInteractor = clockInteractor ) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index 0b3d0f7160f0..3f9c98d6a5d4 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -2399,6 +2399,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); } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt index edf17c1e9e80..81b0064f0f03 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt @@ -232,6 +232,9 @@ interface KeyguardRepository { /** Receive an event for doze time tick */ val dozeTimeTick: Flow<Long> + /** Receive an event lockscreen being shown in a dismissible state */ + val showDismissibleKeyguard: MutableStateFlow<Long> + /** Observable for DismissAction */ val dismissAction: StateFlow<DismissAction> @@ -305,6 +308,8 @@ interface KeyguardRepository { fun dozeTimeTick() + fun showDismissibleKeyguard() + fun setDismissAction(dismissAction: DismissAction) suspend fun setKeyguardDone(keyguardDoneType: KeyguardDone) @@ -439,6 +444,12 @@ constructor( _dozeTimeTick.value = systemClock.uptimeMillis() } + override val showDismissibleKeyguard = MutableStateFlow<Long>(0L) + + override fun showDismissibleKeyguard() { + showDismissibleKeyguard.value = systemClock.uptimeMillis() + } + private val _lastDozeTapToWakePosition = MutableStateFlow<Point?>(null) override val lastDozeTapToWakePosition = _lastDozeTapToWakePosition.asStateFlow() diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt index 3775d191949e..17c1e823a1ca 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt @@ -133,7 +133,12 @@ constructor( transitionInteractor.startedKeyguardState.replayCache.last() == KeyguardState.DREAMING ) { - startTransitionTo(KeyguardState.LOCKSCREEN) + if (powerInteractor.detailedWakefulness.value.isAwake()) { + startTransitionTo( + KeyguardState.LOCKSCREEN, + ownerReason = "Dream has ended and device is awake" + ) + } } } } @@ -144,7 +149,7 @@ constructor( scope.launch { combine( keyguardInteractor.isKeyguardOccluded, - keyguardInteractor.isDreaming + keyguardInteractor.isAbleToDream // Debounce the dreaming signal since there is a race condition between // the occluded and dreaming signals. We therefore add a small delay // to give enough time for occluded to flip to false when the dream @@ -172,7 +177,7 @@ constructor( } scope.launch { - keyguardInteractor.isDreaming + keyguardInteractor.isAbleToDream .filter { !it } .sample(deviceEntryInteractor.isUnlocked, ::Pair) .collect { (_, dismissable) -> diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt index 8f4110c7cc57..db5a63bbf446 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt @@ -74,6 +74,7 @@ constructor( listenForGoneToAodOrDozing() listenForGoneToDreaming() listenForGoneToLockscreenOrHub() + listenForGoneToOccluded() listenForGoneToDreamingLockscreenHosted() } @@ -81,6 +82,27 @@ constructor( scope.launch("$TAG#showKeyguard") { startTransitionTo(KeyguardState.LOCKSCREEN) } } + /** + * A special case supported on foldables, where folding the device may put the device on an + * unlocked lockscreen, but if an occluding app is already showing (like a active phone call), + * then go directly to OCCLUDED. + */ + private fun listenForGoneToOccluded() { + scope.launch("$TAG#listenForGoneToOccluded") { + keyguardInteractor.showDismissibleKeyguard + .filterRelevantKeyguardState() + .sample(keyguardInteractor.isKeyguardOccluded, ::Pair) + .collect { (_, isKeyguardOccluded) -> + if (isKeyguardOccluded) { + startTransitionTo( + KeyguardState.OCCLUDED, + ownerReason = "Dismissible keyguard with occlusion" + ) + } + } + } + } + // Primarily for when the user chooses to lock down the device private fun listenForGoneToLockscreenOrHub() { if (KeyguardWmStateRefactor.isEnabled) { @@ -166,11 +188,12 @@ constructor( interpolator = Interpolators.LINEAR duration = when (toState) { - KeyguardState.DREAMING -> TO_DREAMING_DURATION KeyguardState.AOD -> TO_AOD_DURATION KeyguardState.DOZING -> TO_DOZING_DURATION + KeyguardState.DREAMING -> TO_DREAMING_DURATION KeyguardState.LOCKSCREEN -> TO_LOCKSCREEN_DURATION KeyguardState.GLANCEABLE_HUB -> TO_GLANCEABLE_HUB_DURATION + KeyguardState.OCCLUDED -> TO_OCCLUDED_DURATION else -> DEFAULT_DURATION }.inWholeMilliseconds } @@ -179,10 +202,11 @@ constructor( companion object { private const val TAG = "FromGoneTransitionInteractor" private val DEFAULT_DURATION = 500.milliseconds - val TO_DREAMING_DURATION = 933.milliseconds val TO_AOD_DURATION = 1300.milliseconds val TO_DOZING_DURATION = 933.milliseconds + val TO_DREAMING_DURATION = 933.milliseconds val TO_LOCKSCREEN_DURATION = DEFAULT_DURATION val TO_GLANCEABLE_HUB_DURATION = DEFAULT_DURATION + val TO_OCCLUDED_DURATION = 100.milliseconds } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/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/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/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt index 91b66c3c0a9b..aab5b9b29680 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt @@ -70,7 +70,6 @@ import com.android.systemui.res.R import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.CrossFadeHelper import com.android.systemui.statusbar.VibratorHelper -import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor import com.android.systemui.statusbar.phone.ScreenOffAnimationController import com.android.systemui.temporarydisplay.ViewPriority import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator @@ -292,20 +291,18 @@ object KeyguardRootViewBinder { } } - if (NotificationIconContainerRefactor.isEnabled) { - launch { - val iconsAppearTranslationPx = - configuration - .getDimensionPixelSize(R.dimen.shelf_appear_translation) - .stateIn(this) - viewModel.isNotifIconContainerVisible.collect { isVisible -> - childViews[aodNotificationIconContainerId] - ?.setAodNotifIconContainerIsVisible( - isVisible, - iconsAppearTranslationPx.value, - screenOffAnimationController, - ) - } + launch { + val iconsAppearTranslationPx = + configuration + .getDimensionPixelSize(R.dimen.shelf_appear_translation) + .stateIn(this) + viewModel.isNotifIconContainerVisible.collect { isVisible -> + childViews[aodNotificationIconContainerId] + ?.setAodNotifIconContainerIsVisible( + isVisible, + iconsAppearTranslationPx.value, + screenOffAnimationController, + ) } } @@ -519,7 +516,6 @@ object KeyguardRootViewBinder { if (MigrateClocksToBlueprint.isEnabled) { throw IllegalStateException("should only be called in legacy code paths") } - if (NotificationIconContainerRefactor.isUnexpectedlyInLegacyMode()) return coroutineScope { val iconAppearTranslationPx = configuration.getDimensionPixelSize(R.dimen.shelf_appear_translation).stateIn(this) 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/AodNotificationIconsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt index d77b54825664..36ef78e08c4f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt @@ -37,8 +37,6 @@ import com.android.systemui.statusbar.notification.icon.ui.viewbinder.AlwaysOnDi import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder import com.android.systemui.statusbar.notification.icon.ui.viewbinder.StatusBarIconViewBindingFailureTracker import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerAlwaysOnDisplayViewModel -import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor -import com.android.systemui.statusbar.phone.NotificationIconAreaController import com.android.systemui.statusbar.phone.NotificationIconContainer import com.android.systemui.statusbar.ui.SystemBarUtilsState import com.android.systemui.util.ui.value @@ -53,7 +51,6 @@ constructor( private val iconBindingFailureTracker: StatusBarIconViewBindingFailureTracker, private val nicAodViewModel: NotificationIconContainerAlwaysOnDisplayViewModel, private val nicAodIconViewStore: AlwaysOnDisplayNotificationIconViewStore, - private val notificationIconAreaController: NotificationIconAreaController, private val systemBarUtilsState: SystemBarUtilsState, private val rootViewModel: KeyguardRootViewModel, ) : KeyguardSection() { @@ -86,20 +83,16 @@ constructor( return } - if (NotificationIconContainerRefactor.isEnabled) { - nicBindingDisposable?.dispose() - nicBindingDisposable = - NotificationIconContainerViewBinder.bindWhileAttached( - nic, - nicAodViewModel, - configurationState, - systemBarUtilsState, - iconBindingFailureTracker, - nicAodIconViewStore, - ) - } else { - notificationIconAreaController.setupAodIcons(nic) - } + nicBindingDisposable?.dispose() + nicBindingDisposable = + NotificationIconContainerViewBinder.bindWhileAttached( + nic, + nicAodViewModel, + configurationState, + systemBarUtilsState, + iconBindingFailureTracker, + nicAodIconViewStore, + ) } override fun applyConstraints(constraintSet: ConstraintSet) { 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/AlternateBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModel.kt index 5a559fc3aa45..470f17b74032 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModel.kt @@ -42,9 +42,6 @@ constructor( val transitionToAlternateBouncerProgress = keyguardTransitionInteractor.transitionValue(ALTERNATE_BOUNCER) - val forcePluginOpen: Flow<Boolean> = - transitionToAlternateBouncerProgress.map { it > 0f }.distinctUntilChanged() - /** An observable for the scrim alpha. */ val scrimAlpha = transitionToAlternateBouncerProgress.map { it * alternateBouncerScrimAlpha } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModel.kt index 680f966708be..2426f9745885 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModel.kt @@ -19,6 +19,7 @@ package com.android.systemui.keyguard.ui.viewmodel import androidx.annotation.VisibleForTesting import com.android.app.tracing.FlowTracing.traceEmissionCount +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor @@ -29,19 +30,23 @@ import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAfforda import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.stateIn @OptIn(ExperimentalCoroutinesApi::class) class KeyguardQuickAffordancesCombinedViewModel @Inject constructor( + @Application private val applicationScope: CoroutineScope, private val quickAffordanceInteractor: KeyguardQuickAffordanceInteractor, private val keyguardInteractor: KeyguardInteractor, shadeInteractor: ShadeInteractor, @@ -133,9 +138,14 @@ constructor( /** The source of truth of alpha for all of the quick affordances on lockscreen */ val transitionAlpha: Flow<Float> = merge( - fadeInAlpha, - fadeOutAlpha, - ) + fadeInAlpha, + fadeOutAlpha, + ) + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = 0f, + ) /** * Whether quick affordances are "opaque enough" to be considered visible to and interactive by @@ -199,38 +209,42 @@ constructor( private fun button( position: KeyguardQuickAffordancePosition ): Flow<KeyguardQuickAffordanceViewModel> { - return previewMode.flatMapLatest { previewMode -> - combine( - if (previewMode.isInPreviewMode) { - quickAffordanceInteractor.quickAffordanceAlwaysVisible(position = position) - } else { - quickAffordanceInteractor.quickAffordance(position = position) - }, - keyguardInteractor.animateDozingTransitions.distinctUntilChanged(), - areQuickAffordancesFullyOpaque, - selectedPreviewSlotId, - quickAffordanceInteractor.useLongPress(), - ) { model, animateReveal, isFullyOpaque, selectedPreviewSlotId, useLongPress -> - val slotId = position.toSlotId() - val isSelected = selectedPreviewSlotId == slotId - model.toViewModel( - animateReveal = !previewMode.isInPreviewMode && animateReveal, - isClickable = isFullyOpaque && !previewMode.isInPreviewMode, - isSelected = - previewMode.isInPreviewMode && - previewMode.shouldHighlightSelectedAffordance && - isSelected, - isDimmed = - previewMode.isInPreviewMode && - previewMode.shouldHighlightSelectedAffordance && - !isSelected, - forceInactive = previewMode.isInPreviewMode, - slotId = slotId, - useLongPress = useLongPress, - ) - } - .distinctUntilChanged() - }.traceEmissionCount({"QuickAfforcances#button${position.toSlotId()}"}) + return previewMode + .flatMapLatest { previewMode -> + combine( + if (previewMode.isInPreviewMode) { + quickAffordanceInteractor.quickAffordanceAlwaysVisible( + position = position + ) + } else { + quickAffordanceInteractor.quickAffordance(position = position) + }, + keyguardInteractor.animateDozingTransitions.distinctUntilChanged(), + areQuickAffordancesFullyOpaque, + selectedPreviewSlotId, + quickAffordanceInteractor.useLongPress(), + ) { model, animateReveal, isFullyOpaque, selectedPreviewSlotId, useLongPress -> + val slotId = position.toSlotId() + val isSelected = selectedPreviewSlotId == slotId + model.toViewModel( + animateReveal = !previewMode.isInPreviewMode && animateReveal, + isClickable = isFullyOpaque && !previewMode.isInPreviewMode, + isSelected = + previewMode.isInPreviewMode && + previewMode.shouldHighlightSelectedAffordance && + isSelected, + isDimmed = + previewMode.isInPreviewMode && + previewMode.shouldHighlightSelectedAffordance && + !isSelected, + forceInactive = previewMode.isInPreviewMode, + slotId = slotId, + useLongPress = useLongPress, + ) + } + .distinctUntilChanged() + } + .traceEmissionCount({ "QuickAfforcances#button${position.toSlotId()}" }) } private fun KeyguardQuickAffordanceModel.toViewModel( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt index 38a2b1bad3bf..050ef6f94f0a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt @@ -83,6 +83,7 @@ constructor( private val communalInteractor: CommunalInteractor, private val keyguardTransitionInteractor: KeyguardTransitionInteractor, private val notificationsKeyguardInteractor: NotificationsKeyguardInteractor, + private val alternateBouncerToAodTransitionViewModel: AlternateBouncerToAodTransitionViewModel, private val alternateBouncerToGoneTransitionViewModel: AlternateBouncerToGoneTransitionViewModel, private val alternateBouncerToLockscreenTransitionViewModel: @@ -239,6 +240,7 @@ constructor( merge( alphaOnShadeExpansion, keyguardInteractor.dismissAlpha, + alternateBouncerToAodTransitionViewModel.lockscreenAlpha(viewState), alternateBouncerToGoneTransitionViewModel.lockscreenAlpha(viewState), alternateBouncerToLockscreenTransitionViewModel.lockscreenAlpha(viewState), aodToGoneTransitionViewModel.lockscreenAlpha(viewState), diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt index 3fffeffec254..59cb6e5cef91 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt @@ -20,40 +20,43 @@ import android.content.res.Resources import com.android.compose.animation.scene.ContentKey import com.android.internal.annotations.VisibleForTesting import com.android.systemui.biometrics.AuthController -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.shared.model.ClockSize +import com.android.systemui.lifecycle.SysUiViewModel import com.android.systemui.res.R import com.android.systemui.scene.domain.interactor.SceneContainerOcclusionInteractor import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor -import javax.inject.Inject +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow +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.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch -@SysUISingleton class LockscreenContentViewModel -@Inject +@AssistedInject constructor( clockInteractor: KeyguardClockInteractor, private val interactor: KeyguardBlueprintInteractor, private val authController: AuthController, val touchHandling: KeyguardTouchHandlingViewModel, - val shadeInteractor: ShadeInteractor, - @Application private val applicationScope: CoroutineScope, - unfoldTransitionInteractor: UnfoldTransitionInteractor, - occlusionInteractor: SceneContainerOcclusionInteractor, -) { + private val shadeInteractor: ShadeInteractor, + private val unfoldTransitionInteractor: UnfoldTransitionInteractor, + private val occlusionInteractor: SceneContainerOcclusionInteractor, +) : SysUiViewModel() { @VisibleForTesting val clockSize = clockInteractor.clockSize val isUdfpsVisible: Boolean @@ -61,32 +64,36 @@ constructor( val isShadeLayoutWide: StateFlow<Boolean> = shadeInteractor.isShadeLayoutWide + private val _unfoldTranslations = MutableStateFlow(UnfoldTranslations()) /** Amount of horizontal translation that should be applied to elements in the scene. */ - val unfoldTranslations: StateFlow<UnfoldTranslations> = - combine( - unfoldTransitionInteractor.unfoldTranslationX(isOnStartSide = true), - unfoldTransitionInteractor.unfoldTranslationX(isOnStartSide = false), - ) { start, end -> - UnfoldTranslations( - start = start, - end = end, - ) - } - .stateIn( - scope = applicationScope, - started = SharingStarted.WhileSubscribed(), - initialValue = UnfoldTranslations(), - ) + val unfoldTranslations: StateFlow<UnfoldTranslations> = _unfoldTranslations.asStateFlow() + private val _isContentVisible = MutableStateFlow(true) /** Whether the content of the scene UI should be shown. */ - val isContentVisible: StateFlow<Boolean> = - occlusionInteractor.isOccludingActivityShown - .map { !it } - .stateIn( - scope = applicationScope, - started = SharingStarted.WhileSubscribed(), - initialValue = true, - ) + val isContentVisible: StateFlow<Boolean> = _isContentVisible.asStateFlow() + + override suspend fun onActivated() { + coroutineScope { + launch { + combine( + unfoldTransitionInteractor.unfoldTranslationX(isOnStartSide = true), + unfoldTransitionInteractor.unfoldTranslationX(isOnStartSide = false), + ) { start, end -> + UnfoldTranslations( + start = start, + end = end, + ) + } + .collectLatest { _unfoldTranslations.value = it } + } + + launch { + occlusionInteractor.isOccludingActivityShown + .map { !it } + .collectLatest { _isContentVisible.value = it } + } + } + } /** * Returns a flow that indicates whether lockscreen notifications should be rendered in the @@ -142,4 +149,9 @@ constructor( */ val end: Float = 0f, ) + + @AssistedFactory + interface Factory { + fun create(): LockscreenContentViewModel + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneActionsViewModel.kt new file mode 100644 index 000000000000..7383f57bc4e5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneActionsViewModel.kt @@ -0,0 +1,125 @@ +/* + * 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.keyguard.ui.viewmodel + +import com.android.compose.animation.scene.Edge +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.communal.domain.interactor.CommunalInteractor +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor +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 com.android.systemui.util.kotlin.filterValuesNotNull +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf + +/** Models UI state and handles user input for the lockscreen scene. */ +class LockscreenSceneActionsViewModel +@AssistedInject +constructor( + private val deviceEntryInteractor: DeviceEntryInteractor, + private val communalInteractor: CommunalInteractor, + private val shadeInteractor: ShadeInteractor, +) : SceneActionsViewModel() { + + override suspend fun hydrateActions(setActions: (Map<UserAction, UserActionResult>) -> Unit) { + shadeInteractor.isShadeTouchable + .flatMapLatest { isShadeTouchable -> + if (!isShadeTouchable) { + flowOf(emptyMap()) + } else { + combine( + deviceEntryInteractor.isUnlocked, + communalInteractor.isCommunalAvailable, + shadeInteractor.shadeMode, + ) { isDeviceUnlocked, isCommunalAvailable, shadeMode -> + val notifShadeSceneKey = + UserActionResult( + toScene = SceneFamilies.NotifShade, + transitionKey = + ToSplitShade.takeIf { shadeMode is ShadeMode.Split }, + ) + + mapOf( + Swipe.Left to + UserActionResult(Scenes.Communal).takeIf { + isCommunalAvailable + }, + Swipe.Up to if (isDeviceUnlocked) Scenes.Gone else Scenes.Bouncer, + + // Swiping down from the top edge goes to QS (or shade if in split + // shade mode). + swipeDownFromTop(pointerCount = 1) to + if (shadeMode is ShadeMode.Single) { + UserActionResult(Scenes.QuickSettings) + } else { + notifShadeSceneKey + }, + + // TODO(b/338577208): Remove once we add Dual Shade invocation zones + swipeDownFromTop(pointerCount = 2) to + UserActionResult( + toScene = SceneFamilies.QuickSettings, + transitionKey = + ToSplitShade.takeIf { shadeMode is ShadeMode.Split } + ), + + // Swiping down, not from the edge, always navigates to the notif + // shade scene. + swipeDown(pointerCount = 1) to notifShadeSceneKey, + swipeDown(pointerCount = 2) to notifShadeSceneKey, + ) + .filterValuesNotNull() + } + } + } + .collectLatest { setActions(it) } + } + + private fun swipeDownFromTop(pointerCount: Int): Swipe { + return Swipe( + SwipeDirection.Down, + fromSource = Edge.Top, + pointerCount = pointerCount, + ) + } + + private fun swipeDown(pointerCount: Int): Swipe { + return Swipe( + SwipeDirection.Down, + pointerCount = pointerCount, + ) + } + + @AssistedFactory + interface Factory { + fun create(): LockscreenSceneActionsViewModel + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt deleted file mode 100644 index 15892e9e1c85..000000000000 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt +++ /dev/null @@ -1,116 +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. - */ - -@file:OptIn(ExperimentalCoroutinesApi::class) - -package com.android.systemui.keyguard.ui.viewmodel - -import com.android.compose.animation.scene.Edge -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.communal.domain.interactor.CommunalInteractor -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor -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.shade.domain.interactor.ShadeInteractor -import com.android.systemui.shade.shared.model.ShadeMode -import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel -import com.android.systemui.util.kotlin.filterValuesNotNull -import javax.inject.Inject -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOf - -/** Models UI state and handles user input for the lockscreen scene. */ -@SysUISingleton -class LockscreenSceneViewModel -@Inject -constructor( - private val deviceEntryInteractor: DeviceEntryInteractor, - private val communalInteractor: CommunalInteractor, - private val shadeInteractor: ShadeInteractor, - val touchHandling: KeyguardTouchHandlingViewModel, - val notifications: NotificationsPlaceholderViewModel, -) { - val destinationScenes: Flow<Map<UserAction, UserActionResult>> = - shadeInteractor.isShadeTouchable.flatMapLatest { isShadeTouchable -> - if (!isShadeTouchable) { - flowOf(emptyMap()) - } else { - combine( - deviceEntryInteractor.isUnlocked, - communalInteractor.isCommunalAvailable, - shadeInteractor.shadeMode, - ) { isDeviceUnlocked, isCommunalAvailable, shadeMode -> - val notifShadeSceneKey = - UserActionResult( - toScene = SceneFamilies.NotifShade, - transitionKey = ToSplitShade.takeIf { shadeMode is ShadeMode.Split }, - ) - - mapOf( - Swipe.Left to - UserActionResult(Scenes.Communal).takeIf { isCommunalAvailable }, - Swipe.Up to if (isDeviceUnlocked) Scenes.Gone else Scenes.Bouncer, - - // Swiping down from the top edge goes to QS (or shade if in split shade - // mode). - swipeDownFromTop(pointerCount = 1) to - if (shadeMode is ShadeMode.Single) { - UserActionResult(Scenes.QuickSettings) - } else { - notifShadeSceneKey - }, - - // TODO(b/338577208): Remove once we add Dual Shade invocation zones. - swipeDownFromTop(pointerCount = 2) to - UserActionResult( - toScene = SceneFamilies.QuickSettings, - transitionKey = - ToSplitShade.takeIf { shadeMode is ShadeMode.Split } - ), - - // Swiping down, not from the edge, always navigates to the notif shade - // scene. - swipeDown(pointerCount = 1) to notifShadeSceneKey, - swipeDown(pointerCount = 2) to notifShadeSceneKey, - ) - .filterValuesNotNull() - } - } - } - - private fun swipeDownFromTop(pointerCount: Int): Swipe { - return Swipe( - SwipeDirection.Down, - fromSource = Edge.Top, - pointerCount = pointerCount, - ) - } - - private fun swipeDown(pointerCount: Int): Swipe { - return Swipe( - SwipeDirection.Down, - pointerCount = pointerCount, - ) - } -} 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/BaseMediaProjectionPermissionDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/BaseMediaProjectionPermissionDialogDelegate.kt index 83f694bb1980..cdf8f06b5a23 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/BaseMediaProjectionPermissionDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/BaseMediaProjectionPermissionDialogDelegate.kt @@ -95,13 +95,16 @@ abstract class BaseMediaProjectionPermissionDialogDelegate<T : AlertDialog>( private fun initScreenShareOptions() { selectedScreenShareOption = screenShareOptions.first { it.mode == defaultSelectedMode } - warning.text = warningText + setOptionSpecificFields() initScreenShareSpinner() } private val warningText: String get() = dialog.context.getString(selectedScreenShareOption.warningText, appName) + private val startButtonText: String + get() = dialog.context.getString(selectedScreenShareOption.startButtonText) + private fun initScreenShareSpinner() { val adapter = OptionsAdapter(dialog.context.applicationContext, screenShareOptions) screenShareModeSpinner = dialog.requireViewById(R.id.screen_share_mode_options) @@ -126,7 +129,13 @@ abstract class BaseMediaProjectionPermissionDialogDelegate<T : AlertDialog>( override fun onItemSelected(adapterView: AdapterView<*>?, view: View, pos: Int, id: Long) { selectedScreenShareOption = screenShareOptions[pos] + setOptionSpecificFields() + } + + /** Sets fields on the dialog that change based on which option is selected. */ + private fun setOptionSpecificFields() { warning.text = warningText + startButton.text = startButtonText } override fun onNothingSelected(parent: AdapterView<*>?) {} @@ -137,10 +146,6 @@ abstract class BaseMediaProjectionPermissionDialogDelegate<T : AlertDialog>( dialogTitle.text = title } - protected fun setStartButtonText(@StringRes stringId: Int) { - startButton.setText(stringId) - } - protected fun setStartButtonOnClickListener(listener: View.OnClickListener?) { startButton.setOnClickListener { view -> shouldLogCancel = false diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/ScreenShareOption.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/ScreenShareOption.kt index 9bd57832c4df..ab921732ebf9 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/ScreenShareOption.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/ScreenShareOption.kt @@ -26,9 +26,10 @@ annotation class ScreenShareMode const val SINGLE_APP = 0 const val ENTIRE_SCREEN = 1 -class ScreenShareOption( +data class ScreenShareOption( @ScreenShareMode val mode: Int, @StringRes val spinnerText: Int, @StringRes val warningText: Int, + @StringRes val startButtonText: Int, val spinnerDisabledText: String? = null, ) 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 5a2d88cf1466..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,12 +48,12 @@ class ShareToAppPermissionDialogDelegate( appName, hostUid, mediaProjectionMetricsLogger, + dialogIconDrawable = R.drawable.ic_present_to_all, ) { override fun onCreate(dialog: AlertDialog, savedInstanceState: Bundle?) { super.onCreate(dialog, savedInstanceState) // TODO(b/270018943): Handle the case of System sharing (not recording nor casting) setDialogTitle(R.string.media_projection_entry_app_permission_dialog_title) - setStartButtonText(R.string.media_projection_entry_app_permission_dialog_continue) setStartButtonOnClickListener { // Note that it is important to run this callback before dismissing, so that the // callback can disable the dialog exit animation if it wants to. @@ -84,18 +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_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_entire_screen, ) ) return if (singleAppDisabledText != null) { diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/SystemCastPermissionDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/SystemCastPermissionDialogDelegate.kt index 1ac3ccd19cf4..a19fb9630280 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/SystemCastPermissionDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/SystemCastPermissionDialogDelegate.kt @@ -52,7 +52,6 @@ class SystemCastPermissionDialogDelegate( super.onCreate(dialog, savedInstanceState) // TODO(b/270018943): Handle the case of System sharing (not recording nor casting) setDialogTitle(R.string.media_projection_entry_cast_permission_dialog_title) - setStartButtonText(R.string.media_projection_entry_cast_permission_dialog_continue) setStartButtonOnClickListener { // Note that it is important to run this callback before dismissing, so that the // callback can disable the dialog exit animation if it wants to. @@ -89,6 +88,9 @@ class SystemCastPermissionDialogDelegate( warningText = R.string .media_projection_entry_cast_permission_dialog_warning_single_app, + startButtonText = + R.string + .media_projection_entry_generic_permission_dialog_continue_single_app, spinnerDisabledText = singleAppDisabledText, ), ScreenShareOption( @@ -99,6 +101,9 @@ class SystemCastPermissionDialogDelegate( warningText = R.string .media_projection_entry_cast_permission_dialog_warning_entire_screen, + startButtonText = + R.string + .media_projection_entry_cast_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/navigationbar/views/buttons/KeyButtonView.java b/packages/SystemUI/src/com/android/systemui/navigationbar/views/buttons/KeyButtonView.java index 133d14ddb9f4..111a2d43f881 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/views/buttons/KeyButtonView.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/views/buttons/KeyButtonView.java @@ -21,6 +21,7 @@ import static android.view.KeyEvent.KEYCODE_UNKNOWN; import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK; import static android.view.accessibility.AccessibilityNodeInfo.ACTION_LONG_CLICK; +import android.annotation.Nullable; import android.app.ActivityManager; import android.content.Context; import android.content.res.Configuration; @@ -49,6 +50,7 @@ import android.view.View; import android.view.ViewConfiguration; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; +import android.view.inputmethod.Flags; import android.widget.ImageView; import com.android.internal.annotations.VisibleForTesting; @@ -231,15 +233,28 @@ public class KeyButtonView extends ImageView implements ButtonInterface { @Override public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); - if (mCode != KEYCODE_UNKNOWN) { + if (isClickable()) { info.addAction(new AccessibilityNodeInfo.AccessibilityAction(ACTION_CLICK, null)); if (isLongClickable()) { info.addAction( - new AccessibilityNodeInfo.AccessibilityAction(ACTION_LONG_CLICK, null)); + new AccessibilityNodeInfo.AccessibilityAction(ACTION_LONG_CLICK, + getAccessibilityLongClickActionLabel())); } } } + /** + * Gets the accessibility long click action label for the button, or {@code null} for no label. + */ + @Nullable + private CharSequence getAccessibilityLongClickActionLabel() { + if (Flags.imeSwitcherRevamp() && getId() == R.id.ime_switcher) { + return getContext().getText( + com.android.internal.R.string.input_method_ime_switch_long_click_action_desc); + } + return null; + } + @Override protected void onWindowVisibilityChanged(int visibility) { super.onWindowVisibilityChanged(visibility); 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 db0676e26639..9939075b77d2 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 9c88eb95c274..5a3f1c0b7426 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/tileimpl/QSTileViewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt index abc0453259ce..6a8cc1715aca 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt @@ -1082,7 +1082,12 @@ constructor( inner class StateChangeRunnable(private val state: QSTile.State) : Runnable { override fun run() { - traceSection("QSTileViewImpl#handleStateChanged") { handleStateChanged(state) } + var traceTag = "QSTileViewImpl#handleStateChanged" + if (!state.spec.isNullOrEmpty()) { + traceTag += ":" + traceTag += state.spec + } + traceSection(traceTag.take(Trace.MAX_SECTION_NAME_LEN)) { handleStateChanged(state) } } // We want all instances of this runnable to be equal to each other, so they can be used to diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt index ddd0c7607cdd..9bcf9272c5d6 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt @@ -34,7 +34,6 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import java.io.PrintWriter import java.util.concurrent.CopyOnWriteArraySet -import java.util.function.Supplier import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.flow.collectIndexed @@ -158,6 +157,8 @@ constructor( override fun isTileReady(): Boolean = qsTileViewModel.currentState != null + private var cachedState = QSTile.AdapterState() + override fun setListening(client: Any?, listening: Boolean) { client ?: return if (listening) { @@ -168,7 +169,10 @@ constructor( .filterNotNull() .map { mapState(context, it, qsTileViewModel.config) } .onEach { legacyState -> - callbacks.forEach { it.onStateChanged(legacyState) } + val changed = legacyState.copyTo(cachedState) + if (changed) { + callbacks.forEach { it.onStateChanged(legacyState) } + } } .launchIn(applicationScope) } @@ -235,7 +239,7 @@ constructor( handlesLongClick = viewModelState.supportedActions.contains(QSTileState.UserAction.LONG_CLICK) - iconSupplier = Supplier { + icon = when (val stateIcon = viewModelState.icon()) { is Icon.Loaded -> if (viewModelState.iconRes == null) DrawableIcon(stateIcon.drawable) @@ -243,7 +247,7 @@ constructor( is Icon.Resource -> ResourceIcon.get(stateIcon.res) null -> null } - } + state = viewModelState.activationState.legacyState contentDescription = viewModelState.contentDescription 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/recents/ScreenPinningRequest.java b/packages/SystemUI/src/com/android/systemui/recents/ScreenPinningRequest.java index 8d0a386bc3b0..432a35a1a3dd 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/ScreenPinningRequest.java +++ b/packages/SystemUI/src/com/android/systemui/recents/ScreenPinningRequest.java @@ -16,7 +16,9 @@ package com.android.systemui.recents; +import static com.android.systemui.Flags.enableViewCaptureTracing; import static com.android.systemui.shared.recents.utilities.Utilities.isLargeScreen; +import static com.android.systemui.util.ConvenienceExtensionsKt.toKotlinLazy; import static com.android.systemui.util.leak.RotationUtils.ROTATION_LANDSCAPE; import static com.android.systemui.util.leak.RotationUtils.ROTATION_NONE; import static com.android.systemui.util.leak.RotationUtils.ROTATION_SEASCAPE; @@ -53,6 +55,8 @@ import android.widget.TextView; import androidx.annotation.NonNull; +import com.android.app.viewcapture.ViewCapture; +import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.systemui.CoreStartable; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.SysUISingleton; @@ -83,6 +87,7 @@ public class ScreenPinningRequest implements private final Lazy<NavigationBarController> mNavigationBarControllerLazy; private final AccessibilityManager mAccessibilityService; private final WindowManager mWindowManager; + private final ViewCaptureAwareWindowManager mViewCaptureAwareWindowManager; private final BroadcastDispatcher mBroadcastDispatcher; private final UserTracker mUserTracker; @@ -106,13 +111,16 @@ public class ScreenPinningRequest implements NavigationModeController navigationModeController, Lazy<NavigationBarController> navigationBarControllerLazy, BroadcastDispatcher broadcastDispatcher, - UserTracker userTracker) { + UserTracker userTracker, + Lazy<ViewCapture> daggerLazyViewCapture) { mContext = context; mNavigationBarControllerLazy = navigationBarControllerLazy; mAccessibilityService = (AccessibilityManager) mContext.getSystemService(Context.ACCESSIBILITY_SERVICE); mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); + mViewCaptureAwareWindowManager = new ViewCaptureAwareWindowManager(mWindowManager, + toKotlinLazy(daggerLazyViewCapture), enableViewCaptureTracing()); mNavBarMode = navigationModeController.addListener(this); mBroadcastDispatcher = broadcastDispatcher; mUserTracker = userTracker; @@ -123,7 +131,7 @@ public class ScreenPinningRequest implements public void clearPrompt() { if (mRequestWindow != null) { - mWindowManager.removeView(mRequestWindow); + mViewCaptureAwareWindowManager.removeView(mRequestWindow); mRequestWindow = null; } } @@ -144,7 +152,7 @@ public class ScreenPinningRequest implements // show the confirmation WindowManager.LayoutParams lp = getWindowLayoutParams(); - mWindowManager.addView(mRequestWindow, lp); + mViewCaptureAwareWindowManager.addView(mRequestWindow, lp); } @Override 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/ui/viewmodel/GoneSceneActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneActionsViewModel.kt new file mode 100644 index 000000000000..b707a5ae4739 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneActionsViewModel.kt @@ -0,0 +1,92 @@ +/* + * 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.scene.ui.viewmodel + +import androidx.compose.ui.Alignment +import com.android.compose.animation.scene.Edge +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.scene.shared.model.SceneFamilies +import com.android.systemui.scene.shared.model.TransitionKeys.OpenBottomShade +import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade +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.map + +class GoneSceneActionsViewModel +@AssistedInject +constructor( + private val shadeInteractor: ShadeInteractor, +) : SceneActionsViewModel() { + + override suspend fun hydrateActions(setActions: (Map<UserAction, UserActionResult>) -> Unit) { + shadeInteractor.shadeMode + .map { shadeMode -> + buildMap<UserAction, UserActionResult> { + if ( + shadeMode is ShadeMode.Single || + // TODO(b/338577208): Remove this once we add Dual Shade invocation + // zones. + shadeMode is ShadeMode.Dual + ) { + if (shadeInteractor.shadeAlignment == Alignment.BottomEnd) { + put( + Swipe( + pointerCount = 2, + fromSource = Edge.Bottom, + direction = SwipeDirection.Up, + ), + UserActionResult(SceneFamilies.QuickSettings, OpenBottomShade) + ) + } else { + put( + Swipe( + pointerCount = 2, + fromSource = Edge.Top, + direction = SwipeDirection.Down, + ), + UserActionResult(SceneFamilies.QuickSettings) + ) + } + } + + if (shadeInteractor.shadeAlignment == Alignment.BottomEnd) { + put(Swipe.Up, UserActionResult(SceneFamilies.NotifShade, OpenBottomShade)) + } else { + put( + Swipe.Down, + UserActionResult( + SceneFamilies.NotifShade, + ToSplitShade.takeIf { shadeMode is ShadeMode.Split } + ) + ) + } + } + } + .collectLatest { setActions(it) } + } + + @AssistedFactory + interface Factory { + fun create(): GoneSceneActionsViewModel + } +} diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt deleted file mode 100644 index b739ffe1dc65..000000000000 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt +++ /dev/null @@ -1,83 +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.scene.ui.viewmodel - -import androidx.compose.ui.Alignment -import com.android.compose.animation.scene.Edge -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.scene.shared.model.SceneFamilies -import com.android.systemui.scene.shared.model.TransitionKeys.OpenBottomShade -import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade -import com.android.systemui.shade.domain.interactor.ShadeInteractor -import com.android.systemui.shade.shared.model.ShadeMode -import javax.inject.Inject -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map - -@SysUISingleton -class GoneSceneViewModel -@Inject -constructor( - private val shadeInteractor: ShadeInteractor, -) { - val destinationScenes: Flow<Map<UserAction, UserActionResult>> = - shadeInteractor.shadeMode.map { shadeMode -> - buildMap { - if ( - shadeMode is ShadeMode.Single || - // TODO(b/338577208): Remove this once we add Dual Shade invocation zones. - shadeMode is ShadeMode.Dual - ) { - if (shadeInteractor.shadeAlignment == Alignment.BottomEnd) { - put( - Swipe( - pointerCount = 2, - fromSource = Edge.Bottom, - direction = SwipeDirection.Up, - ), - UserActionResult(SceneFamilies.QuickSettings, OpenBottomShade) - ) - } else { - put( - Swipe( - pointerCount = 2, - fromSource = Edge.Top, - direction = SwipeDirection.Down, - ), - UserActionResult(SceneFamilies.QuickSettings) - ) - } - } - - if (shadeInteractor.shadeAlignment == Alignment.BottomEnd) { - put(Swipe.Up, UserActionResult(SceneFamilies.NotifShade, OpenBottomShade)) - } else { - put( - Swipe.Down, - UserActionResult( - SceneFamilies.NotifShade, - ToSplitShade.takeIf { shadeMode is ShadeMode.Split } - ) - ) - } - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneActionsViewModel.kt new file mode 100644 index 000000000000..c2fd65b6d6cb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneActionsViewModel.kt @@ -0,0 +1,67 @@ +/* + * 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.scene.ui.viewmodel + +import com.android.compose.animation.scene.UserAction +import com.android.compose.animation.scene.UserActionResult +import com.android.systemui.lifecycle.SysUiViewModel +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** + * Base class for view-models that need to keep a map of scene actions (also known as "destination + * scenes") up-to-date. + * + * Subclasses need only to override [hydrateActions], suspending forever if they need; they don't + * need to worry about resetting the value of [actions] when the view-model is deactivated/canceled, + * this base class takes care of it. + */ +abstract class SceneActionsViewModel : SysUiViewModel() { + + private val _actions = MutableStateFlow<Map<UserAction, UserActionResult>>(emptyMap()) + /** + * [UserActionResult] by [UserAction] to be collected by the scene container to enable the right + * user input/gestures. + */ + val actions: StateFlow<Map<UserAction, UserActionResult>> = _actions.asStateFlow() + + final override suspend fun onActivated() { + try { + hydrateActions { state -> _actions.value = state } + awaitCancellation() + } finally { + _actions.value = emptyMap() + } + } + + /** + * Keeps the user actions up-to-date (AKA "hydrated"). + * + * Subclasses should implement this `suspend fun` by running coroutine work and calling + * [setActions] each time the actions should be published/updated. The work can safely suspend + * forever; the base class will take care of canceling it as needed. There's no need to handle + * cancellation in this method. + * + * The base class will also take care of resetting the [actions] flow back to the default value + * when this happens. + */ + protected abstract suspend fun hydrateActions( + setActions: (Map<UserAction, UserActionResult>) -> Unit, + ) +} 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 ab6067c9ec0c..b54bf6ca9ef8 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt @@ -125,7 +125,6 @@ class ScreenRecordPermissionDialogDelegate( super<BaseMediaProjectionPermissionDialogDelegate>.onCreate(dialog, savedInstanceState) setDialogTitle(R.string.screenrecord_permission_dialog_title) dialog.setTitle(R.string.screenrecord_title) - setStartButtonText(R.string.screenrecord_permission_dialog_continue) setStartButtonOnClickListener { v: View? -> onStartRecordingClicked?.run() if (selectedScreenShareOption.mode == ENTIRE_SCREEN) { @@ -272,12 +271,14 @@ class ScreenRecordPermissionDialogDelegate( ScreenShareOption( SINGLE_APP, R.string.screen_share_permission_dialog_option_single_app, - R.string.screenrecord_permission_dialog_warning_single_app + R.string.screenrecord_permission_dialog_warning_single_app, + startButtonText = R.string.screenrecord_permission_dialog_continue, ), ScreenShareOption( ENTIRE_SCREEN, R.string.screen_share_permission_dialog_option_entire_screen, - R.string.screenrecord_permission_dialog_warning_entire_screen + R.string.screenrecord_permission_dialog_warning_entire_screen, + startButtonText = R.string.screenrecord_permission_dialog_continue, ) ) } 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/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 257390fae048..104d4b5427d4 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -1097,14 +1097,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } @Override - public void onPulseExpansionAmountChanged(boolean expandingChanged) { - if (mKeyguardBypassController.getBypassEnabled()) { - // Position the notifications while dragging down while pulsing - requestScrollerTopPaddingUpdate(false /* animate */); - } - } - - @Override public void onDelayedDozeAmountAnimationRunning(boolean running) { // On running OR finished, the animation is no longer waiting to play setWillPlayDelayedDozeAmountAnimation(false); 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/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/NotificationShadeWindowModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/NotificationShadeWindowModel.kt index e1289af58f06..2f9848863059 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/NotificationShadeWindowModel.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/NotificationShadeWindowModel.kt @@ -17,8 +17,11 @@ package com.android.systemui.shade.ui.viewmodel import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED +import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -29,7 +32,11 @@ class NotificationShadeWindowModel @Inject constructor( keyguardTransitionInteractor: KeyguardTransitionInteractor, + keyguardInteractor: KeyguardInteractor, ) { val isKeyguardOccluded: Flow<Boolean> = - keyguardTransitionInteractor.transitionValue(OCCLUDED).map { it == 1f } + anyOf( + keyguardTransitionInteractor.transitionValue(OCCLUDED).map { it == 1f }, + keyguardTransitionInteractor.transitionValue(DREAMING).map { it == 1f }, + ) } 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/NotificationListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java index 49743bf85b8c..ccea254defaa 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java @@ -35,7 +35,6 @@ import com.android.systemui.statusbar.domain.interactor.SilentNotificationStatus import com.android.systemui.statusbar.notification.collection.NotifCollection; import com.android.systemui.statusbar.notification.collection.PipelineDumpable; import com.android.systemui.statusbar.notification.collection.PipelineDumper; -import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor; import com.android.systemui.statusbar.phone.CentralSurfaces; import com.android.systemui.statusbar.phone.NotificationListenerWithPlugins; import com.android.systemui.util.time.SystemClock; @@ -66,7 +65,6 @@ public class NotificationListener extends NotificationListenerWithPlugins implem private final SystemClock mSystemClock; private final Executor mMainExecutor; private final List<NotificationHandler> mNotificationHandlers = new ArrayList<>(); - private final ArrayList<NotificationSettingsListener> mSettingsListeners = new ArrayList<>(); private final Deque<RankingMap> mRankingMapQueue = new ConcurrentLinkedDeque<>(); private final Runnable mDispatchRankingUpdateRunnable = this::dispatchRankingUpdate; @@ -99,13 +97,6 @@ public class NotificationListener extends NotificationListenerWithPlugins implem mNotificationHandlers.add(handler); } - /** Registers a listener that's notified when any notification-related settings change. */ - @Deprecated - public void addNotificationSettingsListener(NotificationSettingsListener listener) { - NotificationIconContainerRefactor.assertInLegacyMode(); - mSettingsListeners.add(listener); - } - @Override public void onListenerConnected() { if (DEBUG) Log.d(TAG, "onListenerConnected"); @@ -237,13 +228,7 @@ public class NotificationListener extends NotificationListenerWithPlugins implem @Override public void onSilentStatusBarIconsVisibilityChanged(boolean hideSilentStatusIcons) { - if (NotificationIconContainerRefactor.isEnabled()) { - mStatusIconInteractor.setHideSilentStatusIcons(hideSilentStatusIcons); - } else { - for (NotificationSettingsListener listener : mSettingsListeners) { - listener.onStatusBarIconsBehaviorChanged(hideSilentStatusIcons); - } - } + mStatusIconInteractor.setHideSilentStatusIcons(hideSilentStatusIcons); } public final void unsnoozeNotification(@NonNull String key) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java index 28e3a8355f45..696e2225d286 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java @@ -48,7 +48,6 @@ import com.android.systemui.statusbar.notification.SourceType; import com.android.systemui.statusbar.notification.row.ActivatableNotificationView; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableView; -import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor; import com.android.systemui.statusbar.notification.stack.AmbientState; import com.android.systemui.statusbar.notification.stack.AnimationProperties; import com.android.systemui.statusbar.notification.stack.ExpandableViewState; @@ -153,11 +152,7 @@ public class NotificationShelf extends ActivatableNotificationView { R.dimen.notification_corner_animation_distance); mEnableNotificationClipping = res.getBoolean(R.bool.notification_enable_clipping); - if (NotificationIconContainerRefactor.isEnabled()) { - mShelfIcons.setOverrideIconColor(true); - } else { - mShelfIcons.setInNotificationIconShelf(true); - } + mShelfIcons.setOverrideIconColor(true); if (!mShowNotificationShelf) { setVisibility(GONE); } @@ -228,9 +223,6 @@ public class NotificationShelf extends ActivatableNotificationView { } else { viewState.setAlpha(1f - ambientState.getHideAmount()); } - if (!NotificationIconContainerRefactor.isEnabled()) { - viewState.belowSpeedBump = getSpeedBumpIndex() == 0; - } viewState.hideSensitive = false; viewState.setXTranslation(getTranslationX()); viewState.hasItemsInStableShelf = lastViewState.inShelf; @@ -276,30 +268,7 @@ public class NotificationShelf extends ActivatableNotificationView { } } - private int getSpeedBumpIndex() { - NotificationIconContainerRefactor.assertInLegacyMode(); - return mHostLayout.getSpeedBumpIndex(); - } - - /** - * @param fractionToShade Fraction of lockscreen to shade transition - * @param shortestWidth Shortest width to use for lockscreen shelf - */ - @VisibleForTesting - public void updateActualWidth(float fractionToShade, float shortestWidth) { - NotificationIconContainerRefactor.assertInLegacyMode(); - final float actualWidth = mAmbientState.isOnKeyguard() - ? MathUtils.lerp(shortestWidth, getWidth(), fractionToShade) - : getWidth(); - setBackgroundWidth((int) actualWidth); - if (mShelfIcons != null) { - mShelfIcons.setActualLayoutWidth((int) actualWidth); - } - mActualWidth = actualWidth; - } - private void setActualWidth(float actualWidth) { - if (NotificationIconContainerRefactor.isUnexpectedlyInLegacyMode()) return; setBackgroundWidth((int) actualWidth); if (mShelfIcons != null) { mShelfIcons.setActualLayoutWidth((int) actualWidth); @@ -482,25 +451,17 @@ public class NotificationShelf extends ActivatableNotificationView { final float fractionToShade = Interpolators.STANDARD.getInterpolation( mAmbientState.getFractionToShade()); - if (NotificationIconContainerRefactor.isEnabled()) { - if (mAmbientState.isOnKeyguard()) { - float numViews = MathUtils.min(numViewsInShelf, mMaxIconsOnLockscreen + 1); - float shortestWidth = mShelfIcons.calculateWidthFor(numViews); - float actualWidth = MathUtils.lerp(shortestWidth, getWidth(), fractionToShade); - setActualWidth(actualWidth); - } else { - setActualWidth(getWidth()); - } + if (mAmbientState.isOnKeyguard()) { + float numViews = MathUtils.min(numViewsInShelf, mMaxIconsOnLockscreen + 1); + float shortestWidth = mShelfIcons.calculateWidthFor(numViews); + float actualWidth = MathUtils.lerp(shortestWidth, getWidth(), fractionToShade); + setActualWidth(actualWidth); } else { - final float shortestWidth = mShelfIcons.calculateWidthFor(numViewsInShelf); - updateActualWidth(fractionToShade, shortestWidth); + setActualWidth(getWidth()); } // TODO(b/172289889) transition last icon in shelf to notification icon and vice versa. setVisibility(isHidden ? View.INVISIBLE : View.VISIBLE); - if (!NotificationIconContainerRefactor.isEnabled()) { - mShelfIcons.setSpeedBumpIndex(getSpeedBumpIndex()); - } mShelfIcons.calculateIconXTranslations(); mShelfIcons.applyIconStates(); for (int i = 0; i < getHostLayoutChildCount(); i++) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java index bbf0ae1700c5..3068460f1cc5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java @@ -64,7 +64,6 @@ import com.android.systemui.res.R; import com.android.systemui.statusbar.notification.NotificationContentDescription; import com.android.systemui.statusbar.notification.NotificationDozeHelper; import com.android.systemui.statusbar.notification.NotificationUtils; -import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor; import com.android.systemui.util.drawable.DrawableSize; import java.lang.annotation.Retention; @@ -906,12 +905,7 @@ public class StatusBarIconView extends AnimatedImageView implements StatusIconDi return mDotAppearAmount; } - public void setDozing(boolean dozing, boolean animate, long delay) { - setDozing(dozing, animate, delay, /* onChildCompleted= */ null); - } - public void setTintAlpha(float tintAlpha) { - if (NotificationIconContainerRefactor.isUnexpectedlyInLegacyMode()) return; setDozeAmount(tintAlpha); } @@ -921,15 +915,6 @@ public class StatusBarIconView extends AnimatedImageView implements StatusIconDi updateIconColor(); } - public void setDozing(boolean dozing, boolean animate, long delay, - @Nullable Runnable endRunnable) { - NotificationIconContainerRefactor.assertInLegacyMode(); - mDozer.setDozing(f -> { - setDozeAmount(f); - updateAllowAnimation(); - }, dozing, animate, delay, this, endRunnable); - } - private void updateAllowAnimation() { if (mDozeAmount == 0 || mDozeAmount == 1) { setAllowAnimation(mDozeAmount == 0); 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/events/PrivacyDotViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt index 9eb9ed5f2063..2930de2fd9ee 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt @@ -26,7 +26,6 @@ import android.widget.FrameLayout import androidx.core.animation.Animator import com.android.app.animation.Interpolators import com.android.internal.annotations.GuardedBy -import com.android.systemui.Flags.privacyDotUnfoldWrongCornerFix import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Main @@ -45,10 +44,10 @@ import com.android.systemui.util.leak.RotationUtils.ROTATION_NONE import com.android.systemui.util.leak.RotationUtils.ROTATION_SEASCAPE import com.android.systemui.util.leak.RotationUtils.ROTATION_UPSIDE_DOWN import com.android.systemui.util.leak.RotationUtils.Rotation -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch import java.util.concurrent.Executor import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch /** * Understands how to keep the persistent privacy dot in the corner of the screen in @@ -61,12 +60,13 @@ import javax.inject.Inject * Views will match the status bar top padding and status bar height so that the dot can appear to * reside directly after the status bar system contents (basically after the battery). * - * NOTE: any operation that modifies views directly must run on the provided executor, because - * these views are owned by ScreenDecorations and it runs in its own thread + * NOTE: any operation that modifies views directly must run on the provided executor, because these + * views are owned by ScreenDecorations and it runs in its own thread */ - @SysUISingleton -open class PrivacyDotViewController @Inject constructor( +open class PrivacyDotViewController +@Inject +constructor( @Main private val mainExecutor: Executor, @Application scope: CoroutineScope, private val stateController: StatusBarStateController, @@ -90,6 +90,7 @@ open class PrivacyDotViewController @Inject constructor( field = value scheduleUpdate() } + private val lock = Object() private var cancelRunnable: Runnable? = null @@ -106,46 +107,48 @@ open class PrivacyDotViewController @Inject constructor( get() = field init { - contentInsetsProvider.addCallback(object : StatusBarContentInsetsChangedListener { - override fun onStatusBarContentInsetsChanged() { - dlog("onStatusBarContentInsetsChanged: ") - setNewLayoutRects() + contentInsetsProvider.addCallback( + object : StatusBarContentInsetsChangedListener { + override fun onStatusBarContentInsetsChanged() { + dlog("onStatusBarContentInsetsChanged: ") + setNewLayoutRects() + } } - }) - - configurationController.addCallback(object : ConfigurationController.ConfigurationListener { - override fun onLayoutDirectionChanged(isRtl: Boolean) { - uiExecutor?.execute { - // If rtl changed, hide all dotes until the next state resolves - setCornerVisibilities(View.INVISIBLE) - - synchronized(this) { - val corner = selectDesignatedCorner(nextViewState.rotation, isRtl) - nextViewState = nextViewState.copy( - layoutRtl = isRtl, - designatedCorner = corner - ) + ) + + configurationController.addCallback( + object : ConfigurationController.ConfigurationListener { + override fun onLayoutDirectionChanged(isRtl: Boolean) { + uiExecutor?.execute { + // If rtl changed, hide all dotes until the next state resolves + setCornerVisibilities(View.INVISIBLE) + + synchronized(this) { + val corner = selectDesignatedCorner(nextViewState.rotation, isRtl) + nextViewState = + nextViewState.copy(layoutRtl = isRtl, designatedCorner = corner) + } } } } - }) + ) - stateController.addCallback(object : StatusBarStateController.StateListener { - override fun onExpandedChanged(isExpanded: Boolean) { - updateStatusBarState() - } + stateController.addCallback( + object : StatusBarStateController.StateListener { + override fun onExpandedChanged(isExpanded: Boolean) { + updateStatusBarState() + } - override fun onStateChanged(newState: Int) { - updateStatusBarState() + override fun onStateChanged(newState: Int) { + updateStatusBarState() + } } - }) + ) scope.launch { shadeInteractor?.isQsExpanded?.collect { isQsExpanded -> dlog("setQsExpanded $isQsExpanded") - synchronized(lock) { - nextViewState = nextViewState.copy(qsExpanded = isQsExpanded) - } + synchronized(lock) { nextViewState = nextViewState.copy(qsExpanded = isQsExpanded) } } } } @@ -179,11 +182,13 @@ open class PrivacyDotViewController @Inject constructor( val paddingTop = contentInsetsProvider.getStatusBarPaddingTop(rot) synchronized(lock) { - nextViewState = nextViewState.copy( + nextViewState = + nextViewState.copy( rotation = rot, paddingTop = paddingTop, designatedCorner = newCorner, - cornerIndex = index) + cornerIndex = index + ) } } @@ -192,14 +197,14 @@ open class PrivacyDotViewController @Inject constructor( dot.clearAnimation() if (animate) { dot.animate() - .setDuration(DURATION) - .setInterpolator(Interpolators.ALPHA_OUT) - .alpha(0f) - .withEndAction { - dot.visibility = View.INVISIBLE - showingListener?.onPrivacyDotHidden(dot) - } - .start() + .setDuration(DURATION) + .setInterpolator(Interpolators.ALPHA_OUT) + .alpha(0f) + .withEndAction { + dot.visibility = View.INVISIBLE + showingListener?.onPrivacyDotHidden(dot) + } + .start() } else { dot.visibility = View.INVISIBLE showingListener?.onPrivacyDotHidden(dot) @@ -213,10 +218,10 @@ open class PrivacyDotViewController @Inject constructor( dot.visibility = View.VISIBLE dot.alpha = 0f dot.animate() - .alpha(1f) - .setDuration(DURATION) - .setInterpolator(Interpolators.ALPHA_IN) - .start() + .alpha(1f) + .setDuration(DURATION) + .setInterpolator(Interpolators.ALPHA_IN) + .start() } else { dot.visibility = View.VISIBLE dot.alpha = 1f @@ -241,9 +246,9 @@ open class PrivacyDotViewController @Inject constructor( } // Set the dot's view gravity to hug the status bar - (corner.requireViewById<View>(R.id.privacy_dot) - .layoutParams as FrameLayout.LayoutParams) - .gravity = rotatedCorner.innerGravity() + (corner.requireViewById<View>(R.id.privacy_dot).layoutParams + as FrameLayout.LayoutParams) + .gravity = rotatedCorner.innerGravity() } } @@ -353,10 +358,7 @@ open class PrivacyDotViewController @Inject constructor( clearAnimation() visibility = View.VISIBLE alpha = 0f - animate() - .alpha(1.0f) - .setDuration(300) - .start() + animate().alpha(1.0f).setDuration(300).start() } } } @@ -405,15 +407,21 @@ open class PrivacyDotViewController @Inject constructor( private fun widthForCorner(corner: Int, left: Int, right: Int): Int { return when (corner) { - TOP_LEFT, BOTTOM_LEFT -> left - TOP_RIGHT, BOTTOM_RIGHT -> right + TOP_LEFT, + BOTTOM_LEFT -> left + TOP_RIGHT, + BOTTOM_RIGHT -> right else -> throw IllegalArgumentException("Unknown corner") } } fun initialize(topLeft: View, topRight: View, bottomLeft: View, bottomRight: View) { - if (this::tl.isInitialized && this::tr.isInitialized && - this::bl.isInitialized && this::br.isInitialized) { + if ( + this::tl.isInitialized && + this::tr.isInitialized && + this::bl.isInitialized && + this::br.isInitialized + ) { if (tl == topLeft && tr == topRight && bl == bottomLeft && br == bottomRight) { return } @@ -430,19 +438,17 @@ open class PrivacyDotViewController @Inject constructor( val index = dc.cornerIndex() - mainExecutor.execute { - animationScheduler.addCallback(systemStatusAnimationCallback) - } + mainExecutor.execute { animationScheduler.addCallback(systemStatusAnimationCallback) } val left = contentInsetsProvider.getStatusBarContentAreaForRotation(ROTATION_SEASCAPE) val top = contentInsetsProvider.getStatusBarContentAreaForRotation(ROTATION_NONE) val right = contentInsetsProvider.getStatusBarContentAreaForRotation(ROTATION_LANDSCAPE) - val bottom = contentInsetsProvider - .getStatusBarContentAreaForRotation(ROTATION_UPSIDE_DOWN) + val bottom = contentInsetsProvider.getStatusBarContentAreaForRotation(ROTATION_UPSIDE_DOWN) val paddingTop = contentInsetsProvider.getStatusBarPaddingTop() synchronized(lock) { - nextViewState = nextViewState.copy( + nextViewState = + nextViewState.copy( viewInitialized = true, designatedCorner = dc, cornerIndex = index, @@ -452,14 +458,12 @@ open class PrivacyDotViewController @Inject constructor( upsideDownRect = bottom, paddingTop = paddingTop, layoutRtl = rtl - ) + ) } } private fun updateStatusBarState() { - synchronized(lock) { - nextViewState = nextViewState.copy(shadeExpanded = isShadeInQs()) - } + synchronized(lock) { nextViewState = nextViewState.copy(shadeExpanded = isShadeInQs()) } } /** @@ -469,16 +473,14 @@ open class PrivacyDotViewController @Inject constructor( @GuardedBy("lock") private fun isShadeInQs(): Boolean { return (stateController.isExpanded && stateController.state == SHADE) || - (stateController.state == SHADE_LOCKED) + (stateController.state == SHADE_LOCKED) } private fun scheduleUpdate() { dlog("scheduleUpdate: ") cancelRunnable?.run() - cancelRunnable = uiExecutor?.executeDelayed({ - processNextViewState() - }, 100) + cancelRunnable = uiExecutor?.executeDelayed({ processNextViewState() }, 100) } @UiThread @@ -486,9 +488,7 @@ open class PrivacyDotViewController @Inject constructor( dlog("processNextViewState: ") val newState: ViewState - synchronized(lock) { - newState = nextViewState.copy() - } + synchronized(lock) { newState = nextViewState.copy() } resolveState(newState) } @@ -508,7 +508,7 @@ open class PrivacyDotViewController @Inject constructor( val designatedCornerChanged = state.designatedCorner != currentViewState.designatedCorner val rotationChanged = state.rotation != currentViewState.rotation - if (rotationChanged || (designatedCornerChanged && privacyDotUnfoldWrongCornerFix())) { + if (rotationChanged || designatedCornerChanged) { // A rotation has started, hide the views to avoid flicker updateRotations(state.rotation, state.paddingTop) } @@ -545,27 +545,29 @@ open class PrivacyDotViewController @Inject constructor( } private val systemStatusAnimationCallback: SystemStatusAnimationCallback = - object : SystemStatusAnimationCallback { - override fun onSystemStatusAnimationTransitionToPersistentDot( - contentDescr: String? - ): Animator? { - synchronized(lock) { - nextViewState = nextViewState.copy( - systemPrivacyEventIsActive = true, - contentDescription = contentDescr) + object : SystemStatusAnimationCallback { + override fun onSystemStatusAnimationTransitionToPersistentDot( + contentDescr: String? + ): Animator? { + synchronized(lock) { + nextViewState = + nextViewState.copy( + systemPrivacyEventIsActive = true, + contentDescription = contentDescr + ) + } + + return null } - return null - } + override fun onHidePersistentDot(): Animator? { + synchronized(lock) { + nextViewState = nextViewState.copy(systemPrivacyEventIsActive = false) + } - override fun onHidePersistentDot(): Animator? { - synchronized(lock) { - nextViewState = nextViewState.copy(systemPrivacyEventIsActive = false) + return null } - - return null } - } private fun View?.cornerIndex(): Int { if (this != null) { @@ -579,8 +581,7 @@ open class PrivacyDotViewController @Inject constructor( val left = contentInsetsProvider.getStatusBarContentAreaForRotation(ROTATION_SEASCAPE) val top = contentInsetsProvider.getStatusBarContentAreaForRotation(ROTATION_NONE) val right = contentInsetsProvider.getStatusBarContentAreaForRotation(ROTATION_LANDSCAPE) - val bottom = contentInsetsProvider - .getStatusBarContentAreaForRotation(ROTATION_UPSIDE_DOWN) + val bottom = contentInsetsProvider.getStatusBarContentAreaForRotation(ROTATION_UPSIDE_DOWN) return listOf(left, top, right, bottom) } @@ -589,17 +590,19 @@ open class PrivacyDotViewController @Inject constructor( val rects = getLayoutRects() synchronized(lock) { - nextViewState = nextViewState.copy( + nextViewState = + nextViewState.copy( seascapeRect = rects[0], portraitRect = rects[1], landscapeRect = rects[2], upsideDownRect = rects[3] - ) + ) } } interface ShowingListener { fun onPrivacyDotShown(v: View?) + fun onPrivacyDotHidden(v: View?) } } @@ -647,22 +650,18 @@ private fun Int.innerGravity(): Int { data class ViewState( val viewInitialized: Boolean = false, - val systemPrivacyEventIsActive: Boolean = false, val shadeExpanded: Boolean = false, val qsExpanded: Boolean = false, - val portraitRect: Rect? = null, val landscapeRect: Rect? = null, val upsideDownRect: Rect? = null, val seascapeRect: Rect? = null, val layoutRtl: Boolean = false, - val rotation: Int = 0, val paddingTop: Int = 0, val cornerIndex: Int = -1, val designatedCorner: View? = null, - val contentDescription: String? = null ) { fun shouldShowDot(): Boolean { @@ -671,11 +670,11 @@ data class ViewState( fun needsLayout(other: ViewState): Boolean { return rotation != other.rotation || - layoutRtl != other.layoutRtl || - portraitRect != other.portraitRect || - landscapeRect != other.landscapeRect || - upsideDownRect != other.upsideDownRect || - seascapeRect != other.seascapeRect + layoutRtl != other.layoutRtl || + portraitRect != other.portraitRect || + landscapeRect != other.landscapeRect || + upsideDownRect != other.upsideDownRect || + seascapeRect != other.seascapeRect } fun contentRectForRotation(@Rotation rot: Int): Rect { 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/NotificationWakeUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt index 9240c1c7a113..22c537cb93f4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt @@ -34,7 +34,6 @@ import com.android.systemui.shade.ShadeViewController import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.domain.interactor.NotificationsKeyguardInteractor -import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController import com.android.systemui.statusbar.notification.stack.StackStateAnimator import com.android.systemui.statusbar.phone.DozeParameters @@ -223,11 +222,6 @@ constructor( val nowExpanding = isPulseExpanding() val changed = nowExpanding != pulseExpanding pulseExpanding = nowExpanding - if (!NotificationIconContainerRefactor.isEnabled) { - for (listener in wakeUpListeners) { - listener.onPulseExpansionAmountChanged(changed) - } - } if (changed) { for (listener in wakeUpListeners) { listener.onPulseExpandingChanged(pulseExpanding) @@ -683,17 +677,6 @@ constructor( fun onFullyHiddenChanged(isFullyHidden: Boolean) {} /** - * Called whenever the pulseExpansion changes - * - * @param expandingChanged if the user has started or stopped expanding - */ - @Deprecated( - message = "Use onPulseExpandedChanged instead.", - replaceWith = ReplaceWith("onPulseExpandedChanged"), - ) - fun onPulseExpansionAmountChanged(expandingChanged: Boolean) {} - - /** * Called when the animator started by [scheduleDelayedDozeAmountAnimation] begins running * after the start delay, or after it ends/is cancelled. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt index 1511abd6b60c..f74c9a6a209f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt @@ -28,9 +28,7 @@ import com.android.systemui.statusbar.notification.collection.render.NotifStats import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.domain.interactor.RenderNotificationListInteractor import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor -import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor import com.android.systemui.statusbar.notification.stack.BUCKET_SILENT -import com.android.systemui.statusbar.phone.NotificationIconAreaController import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController import javax.inject.Inject @@ -43,7 +41,6 @@ class StackCoordinator @Inject internal constructor( private val groupExpansionManagerImpl: GroupExpansionManagerImpl, - private val notificationIconAreaController: NotificationIconAreaController, private val renderListInteractor: RenderNotificationListInteractor, private val activeNotificationsInteractor: ActiveNotificationsInteractor, private val sensitiveNotificationProtectionController: @@ -63,12 +60,7 @@ internal constructor( } else { controller.setNotifStats(notifStats) } - if (NotificationIconContainerRefactor.isEnabled || FooterViewRefactor.isEnabled) { - renderListInteractor.setRenderedList(entries) - } - if (!NotificationIconContainerRefactor.isEnabled) { - notificationIconAreaController.updateNotificationIcons(entries) - } + renderListInteractor.setRenderedList(entries) } private fun calculateNotifStats(entries: List<ListEntry>): NotifStats { @@ -76,9 +68,10 @@ internal constructor( var hasClearableAlertingNotifs = false var hasNonClearableSilentNotifs = false var hasClearableSilentNotifs = false - val isSensitiveContentProtectionActive = screenshareNotificationHiding() && - screenshareNotificationHidingBugFix() && - sensitiveNotificationProtectionController.isSensitiveStateActive + val isSensitiveContentProtectionActive = + screenshareNotificationHiding() && + screenshareNotificationHidingBugFix() && + sensitiveNotificationProtectionController.isSensitiveStateActive entries.forEach { val section = checkNotNull(it.section) { "Null section for ${it.key}" } val entry = checkNotNull(it.representativeEntry) { "Null notif entry for ${it.key}" } 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/icon/ui/viewbinder/NotificationIconAreaControllerViewBinderWrapperImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconAreaControllerViewBinderWrapperImpl.kt deleted file mode 100644 index 1bcab3f102c8..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconAreaControllerViewBinderWrapperImpl.kt +++ /dev/null @@ -1,74 +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.statusbar.notification.icon.ui.viewbinder - -import android.content.Context -import android.graphics.Rect -import android.view.View -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.statusbar.StatusBarIconView -import com.android.systemui.statusbar.notification.collection.ListEntry -import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor -import com.android.systemui.statusbar.phone.NotificationIconAreaController -import com.android.systemui.statusbar.phone.NotificationIconContainer -import javax.inject.Inject - -/** - * Controller class for [NotificationIconContainer]. This implementation serves as a temporary - * wrapper around [NotificationIconContainerViewBinder], so that external code can continue to - * depend on the [NotificationIconAreaController] interface. Once - * [LegacyNotificationIconAreaControllerImpl] is removed, this class can go away and the ViewBinder - * can be used directly. - */ -@SysUISingleton -class NotificationIconAreaControllerViewBinderWrapperImpl @Inject constructor() : - NotificationIconAreaController { - - /** Called by the Keyguard*ViewController whose view contains the aod icons. */ - override fun setupAodIcons(aodIcons: NotificationIconContainer?) = unsupported - - override fun setShelfIcons(icons: NotificationIconContainer) = unsupported - - override fun onDensityOrFontScaleChanged(context: Context) = unsupported - - /** Returns the view that represents the notification area. */ - override fun getNotificationInnerAreaView(): View? = unsupported - - /** Updates the notifications with the given list of notifications to display. */ - override fun updateNotificationIcons(entries: List<ListEntry>) = unsupported - - override fun updateAodNotificationIcons() = unsupported - - override fun showIconIsolated(icon: StatusBarIconView?, animated: Boolean) = unsupported - - override fun setIsolatedIconLocation(iconDrawingRect: Rect, requireStateUpdate: Boolean) = - unsupported - - override fun setAnimationsEnabled(enabled: Boolean) = unsupported - - override fun onThemeChanged() = unsupported - - override fun getHeight(): Int = unsupported - - companion object { - val unsupported: Nothing - get() = - error( - "Code path not supported when ${NotificationIconContainerRefactor.FLAG_NAME}" + - " is disabled" - ) - } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/StatusBarIconViewBindingFailureTracker.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/StatusBarIconViewBindingFailureTracker.kt index 0c114a2ac55d..931381fcba7a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/StatusBarIconViewBindingFailureTracker.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/StatusBarIconViewBindingFailureTracker.kt @@ -17,7 +17,6 @@ package com.android.systemui.statusbar.notification.icon.ui.viewbinder import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor import com.android.systemui.util.asIndenting import com.android.systemui.util.printCollection import dagger.Binds @@ -40,7 +39,6 @@ class StatusBarIconViewBindingFailureTracker @Inject constructor() : CoreStartab } override fun dump(pw: PrintWriter, args: Array<out String>) { - if (!NotificationIconContainerRefactor.isEnabled) return pw.asIndenting().run { printCollection("AOD Icon binding failures:", aodFailures) printCollection("Status Bar Icon binding failures:", statusBarFailures) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java index d2d0aaa63003..560028cb5640 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java @@ -46,7 +46,6 @@ import com.android.systemui.statusbar.notification.FakeShadowView; import com.android.systemui.statusbar.notification.NotificationUtils; import com.android.systemui.statusbar.notification.SourceType; import com.android.systemui.statusbar.notification.shared.NotificationHeadsUpCycling; -import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor; import com.android.systemui.statusbar.notification.shared.NotificationsImprovedHunAnimation; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout; import com.android.systemui.statusbar.notification.stack.StackStateAnimator; @@ -236,16 +235,6 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView setOutlineAlpha(alpha); } - @Override - public void setBelowSpeedBump(boolean below) { - NotificationIconContainerRefactor.assertInLegacyMode(); - super.setBelowSpeedBump(below); - if (below != mIsBelowSpeedBump) { - mIsBelowSpeedBump = below; - updateBackgroundTint(); - } - } - /** * Sets the tint color of the background */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java index 6becbd295264..afda4262bf9d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java @@ -40,7 +40,6 @@ import com.android.systemui.res.R; import com.android.systemui.statusbar.StatusBarIconView; import com.android.systemui.statusbar.notification.Roundable; import com.android.systemui.statusbar.notification.RoundableState; -import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor; import com.android.systemui.statusbar.notification.stack.ExpandableViewState; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout; import com.android.systemui.util.Compile; @@ -394,14 +393,6 @@ public abstract class ExpandableView extends FrameLayout implements Dumpable, Ro public abstract void performAddAnimation(long delay, long duration, boolean isHeadsUpAppear, Runnable onEndRunnable); - /** - * Set the notification appearance to be below the speed bump. - * @param below true if it is below. - */ - public void setBelowSpeedBump(boolean below) { - NotificationIconContainerRefactor.assertInLegacyMode(); - } - public int getPinnedHeadsUpHeight() { return getIntrinsicHeight(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationIconContainerRefactor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationIconContainerRefactor.kt deleted file mode 100644 index a08af7554467..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationIconContainerRefactor.kt +++ /dev/null @@ -1,52 +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.statusbar.notification.shared - -import com.android.systemui.Flags -import com.android.systemui.flags.FlagToken -import com.android.systemui.flags.RefactorFlagUtils - -/** Helper for reading or using the NotificationIconContainer refactor flag state. */ -@Suppress("NOTHING_TO_INLINE") -object NotificationIconContainerRefactor { - /** The aconfig flag name */ - const val FLAG_NAME = Flags.FLAG_NOTIFICATIONS_ICON_CONTAINER_REFACTOR - - /** A token used for dependency declaration */ - val token: FlagToken - get() = FlagToken(FLAG_NAME, isEnabled) - - /** Is the refactor enabled? */ - @JvmStatic - inline val isEnabled - get() = Flags.notificationsIconContainerRefactor() - - /** - * Called to ensure code is only run when the flag is enabled. This protects users from the - * unintended behaviors caused by accidentally running new logic, while also crashing on an eng - * build to ensure that the refactor author catches issues in testing. - */ - @JvmStatic - inline fun isUnexpectedlyInLegacyMode() = - RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME) - - /** - * Called to ensure code is only run when the flag is disabled. This will throw an exception if - * the flag is enabled to ensure that the refactor author catches issues in testing. - */ - @JvmStatic - inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME) -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt index 819527ee1eca..15dc1157aa69 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt @@ -21,9 +21,7 @@ import com.android.systemui.plugins.FalsingManager import com.android.systemui.statusbar.NotificationShelf import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerShelfViewBinder import com.android.systemui.statusbar.notification.row.ui.viewbinder.ActivatableNotificationViewBinder -import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor import com.android.systemui.statusbar.notification.shelf.ui.viewmodel.NotificationShelfViewModel -import com.android.systemui.statusbar.phone.NotificationIconAreaController import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch @@ -35,17 +33,10 @@ object NotificationShelfViewBinder { viewModel: NotificationShelfViewModel, falsingManager: FalsingManager, nicBinder: NotificationIconContainerShelfViewBinder, - notificationIconAreaController: NotificationIconAreaController, ): Unit = coroutineScope { ActivatableNotificationViewBinder.bind(viewModel, shelf, falsingManager) shelf.apply { - traceSection("NotifShelf#bindShelfIcons") { - if (NotificationIconContainerRefactor.isEnabled) { - launch { nicBinder.bind(shelfIcons) } - } else { - notificationIconAreaController.setShelfIcons(shelfIcons) - } - } + traceSection("NotifShelf#bindShelfIcons") { launch { nicBinder.bind(shelfIcons) } } launch { viewModel.canModifyColorOfNotifications.collect(::setCanModifyColorOfNotifications) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ExpandableViewState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ExpandableViewState.java index 83de2265b98e..69c9a4bf2dbb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ExpandableViewState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ExpandableViewState.java @@ -26,7 +26,6 @@ import com.android.app.animation.Interpolators; import com.android.systemui.res.R; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableView; -import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor; /** * A state of an expandable view @@ -157,11 +156,6 @@ public class ExpandableViewState extends ViewState { expandableView.setHideSensitive( this.hideSensitive, false /* animated */, 0 /* delay */, 0 /* duration */); - // apply below shelf speed bump - if (!NotificationIconContainerRefactor.isEnabled()) { - expandableView.setBelowSpeedBump(this.belowSpeedBump); - } - // apply clipping final float oldClipTopAmount = expandableView.getClipTopAmount(); if (oldClipTopAmount != this.clipTopAmount) { @@ -211,11 +205,6 @@ public class ExpandableViewState extends ViewState { abortAnimation(child, TAG_ANIMATOR_BOTTOM_INSET); } - // apply below the speed bump - if (!NotificationIconContainerRefactor.isEnabled()) { - expandableView.setBelowSpeedBump(this.belowSpeedBump); - } - // start hiding sensitive animation expandableView.setHideSensitive(this.hideSensitive, animationFilter.animateHideSensitive, properties.delay, properties.duration); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index 608fe95ee92a..41195aa0f72e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -1426,6 +1426,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { } public void setHeadsUpBoundaries(int height, int bottomBarHeight) { + SceneContainerFlag.assertInLegacyMode(); mView.setHeadsUpBoundaries(height, bottomBarHeight); } 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/NotificationListViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt index 5544f9389787..5572f8e5cbe8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt @@ -46,7 +46,6 @@ import com.android.systemui.statusbar.notification.stack.ui.view.NotificationSta import com.android.systemui.statusbar.notification.stack.ui.viewbinder.HideNotificationsBinder.bindHideList import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationListViewModel import com.android.systemui.statusbar.notification.ui.viewbinder.HeadsUpNotificationViewBinder -import com.android.systemui.statusbar.phone.NotificationIconAreaController import com.android.systemui.util.kotlin.awaitCancellationThenDispose import com.android.systemui.util.kotlin.getOrNull import com.android.systemui.util.ui.isAnimating @@ -74,7 +73,6 @@ constructor( private val configuration: ConfigurationState, private val falsingManager: FalsingManager, private val hunBinder: HeadsUpNotificationViewBinder, - private val iconAreaController: NotificationIconAreaController, private val loggerOptional: Optional<NotificationStatsLogger>, private val metricsLogger: MetricsLogger, private val nicBinder: NotificationIconContainerShelfViewBinder, @@ -128,7 +126,6 @@ constructor( viewModel.shelf, falsingManager, nicBinder, - iconAreaController, ) } @@ -183,12 +180,12 @@ constructor( launchNotificationSettings = { view -> notificationActivityStarter .get() - .startHistoryIntent(view, /* showHistory = */ false) + .startHistoryIntent(view, /* showHistory= */ false) }, launchNotificationHistory = { view -> notificationActivityStarter .get() - .startHistoryIntent(view, /* showHistory = */ true) + .startHistoryIntent(view, /* showHistory= */ true) }, ) launch { 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/NotificationsPlaceholderViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt index e8a784072808..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,19 +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> = 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 3a3b05bdd71d..db8cd575f77b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -209,7 +209,6 @@ import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator import com.android.systemui.statusbar.notification.init.NotificationsController; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.NotificationGutsManager; -import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor; import com.android.systemui.statusbar.notification.stack.NotificationListContainer; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController; @@ -237,10 +236,10 @@ import com.android.wm.shell.bubbles.Bubbles; import com.android.wm.shell.startingsurface.SplashscreenContentDrawer; import com.android.wm.shell.startingsurface.StartingSurface; -import dalvik.annotation.optimization.NeverCompile; - import dagger.Lazy; +import dalvik.annotation.optimization.NeverCompile; + import java.io.PrintWriter; import java.io.StringWriter; import java.util.Map; @@ -546,7 +545,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { protected final BatteryController mBatteryController; private UiModeManager mUiModeManager; private LogMaker mStatusBarStateLog; - protected final NotificationIconAreaController mNotificationIconAreaController; @Nullable private View mAmbientIndicationContainer; private final SysuiColorExtractor mColorExtractor; private final ScreenLifecycle mScreenLifecycle; @@ -683,7 +681,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { DemoModeController demoModeController, Lazy<NotificationShadeDepthController> notificationShadeDepthControllerLazy, StatusBarTouchableRegionManager statusBarTouchableRegionManager, - NotificationIconAreaController notificationIconAreaController, BrightnessSliderController.Factory brightnessSliderFactory, ScreenOffAnimationController screenOffAnimationController, WallpaperController wallpaperController, @@ -787,7 +784,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mUserInfoControllerImpl = userInfoControllerImpl; mIconPolicy = phoneStatusBarPolicy; mDemoModeController = demoModeController; - mNotificationIconAreaController = notificationIconAreaController; mBrightnessSliderFactory = brightnessSliderFactory; mWallpaperController = wallpaperController; mStatusBarSignalPolicy = statusBarSignalPolicy; @@ -2273,7 +2269,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; @@ -2652,9 +2651,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { !mDozeServiceHost.isPulsing()); mShadeSurface.setTouchAndAnimationDisabled(disabled); - if (!NotificationIconContainerRefactor.isEnabled()) { - mNotificationIconAreaController.setAnimationsEnabled(!disabled); - } } final ScreenLifecycle.Observer mScreenObserver = new ScreenLifecycle.Observer() { @@ -2989,7 +2985,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); } }; @@ -3053,9 +3049,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { } // TODO: Bring these out of CentralSurfaces. mUserInfoControllerImpl.onDensityOrFontScaleChanged(); - if (!NotificationIconContainerRefactor.isEnabled()) { - mNotificationIconAreaController.onDensityOrFontScaleChanged(mContext); - } } @Override @@ -3073,9 +3066,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { if (mAmbientIndicationContainer instanceof AutoReinflateContainer) { ((AutoReinflateContainer) mAmbientIndicationContainer).inflateLayout(); } - if (!NotificationIconContainerRefactor.isEnabled()) { - mNotificationIconAreaController.onThemeChanged(); - } } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java index ca1fb78bdb42..f13a593d08f2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java @@ -52,7 +52,6 @@ import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator; import com.android.systemui.statusbar.notification.collection.NotificationEntry; -import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor; import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.statusbar.policy.HeadsUpManager; @@ -106,7 +105,6 @@ public final class DozeServiceHost implements DozeHost { private final NotificationWakeUpCoordinator mNotificationWakeUpCoordinator; private NotificationShadeWindowViewController mNotificationShadeWindowViewController; private final AuthController mAuthController; - private final NotificationIconAreaController mNotificationIconAreaController; private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; private final ShadeLockscreenInteractor mShadeLockscreenInteractor; private View mAmbientIndicationContainer; @@ -129,7 +127,6 @@ public final class DozeServiceHost implements DozeHost { NotificationShadeWindowController notificationShadeWindowController, NotificationWakeUpCoordinator notificationWakeUpCoordinator, AuthController authController, - NotificationIconAreaController notificationIconAreaController, ShadeLockscreenInteractor shadeLockscreenInteractor, DozeInteractor dozeInteractor) { super(); @@ -149,7 +146,6 @@ public final class DozeServiceHost implements DozeHost { mNotificationShadeWindowController = notificationShadeWindowController; mNotificationWakeUpCoordinator = notificationWakeUpCoordinator; mAuthController = authController; - mNotificationIconAreaController = notificationIconAreaController; mShadeLockscreenInteractor = shadeLockscreenInteractor; mHeadsUpManager.addListener(mOnHeadsUpChangedListener); mDozeInteractor = dozeInteractor; @@ -196,13 +192,8 @@ public final class DozeServiceHost implements DozeHost { void fireNotificationPulse(NotificationEntry entry) { Runnable pulseSuppressedListener = () -> { - if (NotificationIconContainerRefactor.isEnabled()) { - mHeadsUpManager.removeNotification( - entry.getKey(), /* releaseImmediately= */ true, /* animate= */ false); - } else { - entry.setPulseSuppressed(true); - mNotificationIconAreaController.updateAodNotificationIcons(); - } + mHeadsUpManager.removeNotification( + entry.getKey(), /* releaseImmediately= */ true, /* animate= */ false); }; Assert.isMainThread(); for (Callback callback : mCallbacks) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java index f99a81e43797..8f94c0656836 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java @@ -42,7 +42,6 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationIconInteractor; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation; -import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor; import com.android.systemui.statusbar.notification.stack.NotificationRoundnessManager; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController; import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentScope; @@ -74,7 +73,6 @@ public class HeadsUpAppearanceController extends ViewController<HeadsUpStatusBar private static final SourceType HEADS_UP = SourceType.from("HeadsUp"); private static final SourceType PULSING = SourceType.from("Pulsing"); - private final NotificationIconAreaController mNotificationIconAreaController; private final HeadsUpManager mHeadsUpManager; private final NotificationStackScrollLayoutController mStackScrollerController; @@ -114,7 +112,6 @@ public class HeadsUpAppearanceController extends ViewController<HeadsUpStatusBar @VisibleForTesting @Inject public HeadsUpAppearanceController( - NotificationIconAreaController notificationIconAreaController, HeadsUpManager headsUpManager, StatusBarStateController stateController, PhoneStatusBarTransitions phoneStatusBarTransitions, @@ -132,7 +129,6 @@ public class HeadsUpAppearanceController extends ViewController<HeadsUpStatusBar HeadsUpNotificationIconInteractor headsUpNotificationIconInteractor, @Named(OPERATOR_NAME_FRAME_VIEW) Optional<View> operatorNameViewOptional) { super(headsUpStatusBarView); - mNotificationIconAreaController = notificationIconAreaController; mNotificationRoundnessManager = notificationRoundnessManager; mHeadsUpManager = headsUpManager; @@ -178,11 +174,8 @@ public class HeadsUpAppearanceController extends ViewController<HeadsUpStatusBar @Override protected void onViewAttached() { mHeadsUpManager.addListener(this); - mView.setOnDrawingRectChangedListener( - () -> updateIsolatedIconLocation(true /* requireUpdate */)); - if (NotificationIconContainerRefactor.isEnabled()) { - updateIsolatedIconLocation(true); - } + mView.setOnDrawingRectChangedListener(this::updateIsolatedIconLocation); + updateIsolatedIconLocation(); mWakeUpCoordinator.addListener(this); getShadeHeadsUpTracker().addTrackingHeadsUpListener(mSetTrackingHeadsUp); getShadeHeadsUpTracker().setHeadsUpAppearanceController(this); @@ -198,9 +191,7 @@ public class HeadsUpAppearanceController extends ViewController<HeadsUpStatusBar protected void onViewDetached() { mHeadsUpManager.removeListener(this); mView.setOnDrawingRectChangedListener(null); - if (NotificationIconContainerRefactor.isEnabled()) { - mHeadsUpNotificationIconInteractor.setIsolatedIconLocation(null); - } + mHeadsUpNotificationIconInteractor.setIsolatedIconLocation(null); mWakeUpCoordinator.removeListener(this); getShadeHeadsUpTracker().removeTrackingHeadsUpListener(mSetTrackingHeadsUp); getShadeHeadsUpTracker().setHeadsUpAppearanceController(null); @@ -208,14 +199,8 @@ public class HeadsUpAppearanceController extends ViewController<HeadsUpStatusBar mDarkIconDispatcher.removeDarkReceiver(this); } - private void updateIsolatedIconLocation(boolean requireStateUpdate) { - if (NotificationIconContainerRefactor.isEnabled()) { - mHeadsUpNotificationIconInteractor - .setIsolatedIconLocation(mView.getIconDrawingRect()); - } else { - mNotificationIconAreaController.setIsolatedIconLocation( - mView.getIconDrawingRect(), requireStateUpdate); - } + private void updateIsolatedIconLocation() { + mHeadsUpNotificationIconInteractor.setIsolatedIconLocation(mView.getIconDrawingRect()); } @Override @@ -251,14 +236,8 @@ public class HeadsUpAppearanceController extends ViewController<HeadsUpStatusBar setShown(true); animateIsolation = !isExpanded(); } - if (NotificationIconContainerRefactor.isEnabled()) { - mHeadsUpNotificationIconInteractor.setIsolatedIconNotificationKey( - newEntry == null ? null : newEntry.getRepresentativeEntry().getKey()); - } else { - updateIsolatedIconLocation(false /* requireUpdate */); - mNotificationIconAreaController.showIconIsolated(newEntry == null ? null - : newEntry.getIcons().getStatusBarIcon(), animateIsolation); - } + mHeadsUpNotificationIconInteractor.setIsolatedIconNotificationKey( + newEntry == null ? null : newEntry.getRepresentativeEntry().getKey()); } } 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/LegacyNotificationIconAreaControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java deleted file mode 100644 index f84efbbf9293..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java +++ /dev/null @@ -1,700 +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.statusbar.phone; - -import static com.android.systemui.Flags.newAodTransition; - -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Color; -import android.graphics.Rect; -import android.os.Bundle; -import android.os.Trace; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.FrameLayout; - -import androidx.annotation.ColorInt; -import androidx.annotation.NonNull; -import androidx.annotation.VisibleForTesting; -import androidx.collection.ArrayMap; - -import com.android.app.animation.Interpolators; -import com.android.internal.statusbar.StatusBarIcon; -import com.android.internal.util.ContrastColorUtil; -import com.android.settingslib.Utils; -import com.android.systemui.dagger.SysUISingleton; -import com.android.systemui.demomode.DemoMode; -import com.android.systemui.demomode.DemoModeController; -import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.keyguard.MigrateClocksToBlueprint; -import com.android.systemui.plugins.DarkIconDispatcher; -import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver; -import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.res.R; -import com.android.systemui.statusbar.CrossFadeHelper; -import com.android.systemui.statusbar.NotificationListener; -import com.android.systemui.statusbar.NotificationMediaManager; -import com.android.systemui.statusbar.StatusBarIconView; -import com.android.systemui.statusbar.StatusBarState; -import com.android.systemui.statusbar.notification.NotificationUtils; -import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator; -import com.android.systemui.statusbar.notification.collection.ListEntry; -import com.android.systemui.statusbar.notification.collection.NotificationEntry; -import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider; -import com.android.systemui.statusbar.window.StatusBarWindowController; -import com.android.wm.shell.bubbles.Bubbles; - -import org.jetbrains.annotations.NotNull; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.function.Function; - -import javax.inject.Inject; - -/** - * A controller for the space in the status bar to the left of the system icons. This area is - * normally reserved for notifications. - */ -@SysUISingleton -public class LegacyNotificationIconAreaControllerImpl implements - NotificationIconAreaController, - DarkReceiver, - StatusBarStateController.StateListener, - NotificationWakeUpCoordinator.WakeUpListener, - DemoMode { - - private static final long AOD_ICONS_APPEAR_DURATION = 200; - @ColorInt - private static final int DEFAULT_AOD_ICON_COLOR = 0xffffffff; - - private final ContrastColorUtil mContrastColorUtil; - private final Runnable mUpdateStatusBarIcons = this::updateStatusBarIcons; - private final StatusBarStateController mStatusBarStateController; - private final NotificationMediaManager mMediaManager; - private final NotificationWakeUpCoordinator mWakeUpCoordinator; - private final KeyguardBypassController mBypassController; - private final DozeParameters mDozeParameters; - private final SectionStyleProvider mSectionStyleProvider; - private final Optional<Bubbles> mBubblesOptional; - private final StatusBarWindowController mStatusBarWindowController; - private final ScreenOffAnimationController mScreenOffAnimationController; - - private int mIconSize; - private int mIconHPadding; - private int mIconTint = Color.WHITE; - - private List<ListEntry> mNotificationEntries = List.of(); - protected View mNotificationIconArea; - private NotificationIconContainer mNotificationIcons; - private NotificationIconContainer mShelfIcons; - private NotificationIconContainer mAodIcons; - private final ArrayList<Rect> mTintAreas = new ArrayList<>(); - private final Context mContext; - private int mAodIconAppearTranslation; - - private boolean mAnimationsEnabled; - private int mAodIconTint; - private boolean mAodIconsVisible; - private boolean mShowLowPriority = true; - - @VisibleForTesting - final NotificationListener.NotificationSettingsListener mSettingsListener = - new NotificationListener.NotificationSettingsListener() { - @Override - public void onStatusBarIconsBehaviorChanged(boolean hideSilentStatusIcons) { - mShowLowPriority = !hideSilentStatusIcons; - updateStatusBarIcons(); - } - }; - - @Inject - public LegacyNotificationIconAreaControllerImpl( - Context context, - StatusBarStateController statusBarStateController, - NotificationWakeUpCoordinator wakeUpCoordinator, - KeyguardBypassController keyguardBypassController, - NotificationMediaManager notificationMediaManager, - NotificationListener notificationListener, - DozeParameters dozeParameters, - SectionStyleProvider sectionStyleProvider, - Optional<Bubbles> bubblesOptional, - DemoModeController demoModeController, - DarkIconDispatcher darkIconDispatcher, - FeatureFlags featureFlags, - StatusBarWindowController statusBarWindowController, - ScreenOffAnimationController screenOffAnimationController) { - mContrastColorUtil = ContrastColorUtil.getInstance(context); - mContext = context; - mStatusBarStateController = statusBarStateController; - mStatusBarStateController.addCallback(this); - mMediaManager = notificationMediaManager; - mDozeParameters = dozeParameters; - mSectionStyleProvider = sectionStyleProvider; - mWakeUpCoordinator = wakeUpCoordinator; - wakeUpCoordinator.addListener(this); - mBypassController = keyguardBypassController; - mBubblesOptional = bubblesOptional; - demoModeController.addCallback(this); - mStatusBarWindowController = statusBarWindowController; - mScreenOffAnimationController = screenOffAnimationController; - notificationListener.addNotificationSettingsListener(mSettingsListener); - initializeNotificationAreaViews(context); - reloadAodColor(); - darkIconDispatcher.addDarkReceiver(this); - } - - protected View inflateIconArea(LayoutInflater inflater) { - return inflater.inflate(R.layout.notification_icon_area, null); - } - - /** - * Initializes the views that will represent the notification area. - */ - protected void initializeNotificationAreaViews(Context context) { - reloadDimens(context); - - LayoutInflater layoutInflater = LayoutInflater.from(context); - mNotificationIconArea = inflateIconArea(layoutInflater); - mNotificationIcons = mNotificationIconArea.findViewById(R.id.notificationIcons); - } - - /** - * Called by the Keyguard*ViewController whose view contains the aod icons. - */ - public void setupAodIcons(@NonNull NotificationIconContainer aodIcons) { - boolean changed = mAodIcons != null && aodIcons != mAodIcons; - if (changed) { - mAodIcons.setAnimationsEnabled(false); - mAodIcons.removeAllViews(); - } - mAodIcons = aodIcons; - mAodIcons.setOnLockScreen(true); - updateAodIconsVisibility(false /* animate */, changed); - updateAnimations(); - if (changed) { - updateAodNotificationIcons(); - } - updateIconLayoutParams(mContext); - } - - public void setShelfIcons(NotificationIconContainer icons) { - mShelfIcons = icons; - } - - public void onDensityOrFontScaleChanged(@NotNull Context context) { - updateIconLayoutParams(context); - } - - private void updateIconLayoutParams(Context context) { - reloadDimens(context); - final FrameLayout.LayoutParams params = generateIconLayoutParams(); - for (int i = 0; i < mNotificationIcons.getChildCount(); i++) { - View child = mNotificationIcons.getChildAt(i); - child.setLayoutParams(params); - } - if (mShelfIcons != null) { - for (int i = 0; i < mShelfIcons.getChildCount(); i++) { - View child = mShelfIcons.getChildAt(i); - child.setLayoutParams(params); - } - } - if (mAodIcons != null) { - for (int i = 0; i < mAodIcons.getChildCount(); i++) { - View child = mAodIcons.getChildAt(i); - child.setLayoutParams(params); - } - } - } - - @NonNull - private FrameLayout.LayoutParams generateIconLayoutParams() { - return new FrameLayout.LayoutParams( - mIconSize + 2 * mIconHPadding, mStatusBarWindowController.getStatusBarHeight()); - } - - private void reloadDimens(Context context) { - Resources res = context.getResources(); - mIconSize = res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_icon_size_sp); - mIconHPadding = res.getDimensionPixelSize(R.dimen.status_bar_icon_horizontal_margin); - mAodIconAppearTranslation = res.getDimensionPixelSize( - R.dimen.shelf_appear_translation); - } - - /** - * Returns the view that represents the notification area. - */ - public View getNotificationInnerAreaView() { - return mNotificationIconArea; - } - - /** - * See {@link com.android.systemui.statusbar.policy.DarkIconDispatcher#setIconsDarkArea}. - * Sets the color that should be used to tint any icons in the notification area. - * - * @param tintAreas the areas in which to tint the icons, specified in screen coordinates - * @param darkIntensity - */ - public void onDarkChanged(ArrayList<Rect> tintAreas, float darkIntensity, int iconTint) { - mTintAreas.clear(); - mTintAreas.addAll(tintAreas); - - if (DarkIconDispatcher.isInAreas(tintAreas, mNotificationIconArea)) { - mIconTint = iconTint; - } - - applyNotificationIconsTint(); - } - - protected boolean shouldShowNotificationIcon(NotificationEntry entry, - boolean showAmbient, boolean showLowPriority, boolean hideDismissed, - boolean hideRepliedMessages, boolean hideCurrentMedia, boolean hidePulsing) { - if (!showAmbient && mSectionStyleProvider.isMinimized(entry)) { - return false; - } - if (hideCurrentMedia && entry.getKey().equals(mMediaManager.getMediaNotificationKey())) { - return false; - } - if (!showLowPriority && mSectionStyleProvider.isSilent(entry)) { - return false; - } - if (entry.isRowDismissed() && hideDismissed) { - return false; - } - if (hideRepliedMessages && entry.isLastMessageFromReply()) { - return false; - } - // showAmbient == show in shade but not shelf - if (!showAmbient && entry.shouldSuppressStatusBar()) { - return false; - } - if (hidePulsing && entry.showingPulsing() - && (!mWakeUpCoordinator.getNotificationsFullyHidden() - || !entry.isPulseSuppressed())) { - return false; - } - if (mBubblesOptional.isPresent() - && mBubblesOptional.get().isBubbleExpanded(entry.getKey())) { - return false; - } - return true; - } - - /** - * Updates the notifications with the given list of notifications to display. - */ - public void updateNotificationIcons(List<ListEntry> entries) { - mNotificationEntries = entries; - updateNotificationIcons(); - } - - private void updateNotificationIcons() { - Trace.beginSection("NotificationIconAreaController.updateNotificationIcons"); - updateStatusBarIcons(); - updateShelfIcons(); - updateAodNotificationIcons(); - - applyNotificationIconsTint(); - Trace.endSection(); - } - - private void updateShelfIcons() { - if (mShelfIcons == null) { - return; - } - updateIconsForLayout(entry -> entry.getIcons().getShelfIcon(), mShelfIcons, - true /* showAmbient */, - true /* showLowPriority */, - false /* hideDismissed */, - false /* hideRepliedMessages */, - false /* hideCurrentMedia */, - false /* hidePulsing */); - } - - public void updateStatusBarIcons() { - updateIconsForLayout(entry -> entry.getIcons().getStatusBarIcon(), mNotificationIcons, - false /* showAmbient */, - mShowLowPriority, - true /* hideDismissed */, - true /* hideRepliedMessages */, - false /* hideCurrentMedia */, - false /* hidePulsing */); - } - - public void updateAodNotificationIcons() { - if (mAodIcons == null) { - return; - } - updateIconsForLayout(entry -> entry.getIcons().getAodIcon(), mAodIcons, - false /* showAmbient */, - true /* showLowPriority */, - true /* hideDismissed */, - true /* hideRepliedMessages */, - true /* hideCurrentMedia */, - mBypassController.getBypassEnabled() /* hidePulsing */); - } - - @VisibleForTesting - boolean shouldShouldLowPriorityIcons() { - return mShowLowPriority; - } - - /** - * Updates the notification icons for a host layout. This will ensure that the notification - * host layout will have the same icons like the ones in here. - * @param function A function to look up an icon view based on an entry - * @param hostLayout which layout should be updated - * @param showAmbient should ambient notification icons be shown - * @param showLowPriority should icons from silent notifications be shown - * @param hideDismissed should dismissed icons be hidden - * @param hideRepliedMessages should messages that have been replied to be hidden - * @param hidePulsing should pulsing notifications be hidden - */ - private void updateIconsForLayout(Function<NotificationEntry, StatusBarIconView> function, - NotificationIconContainer hostLayout, boolean showAmbient, boolean showLowPriority, - boolean hideDismissed, boolean hideRepliedMessages, boolean hideCurrentMedia, - boolean hidePulsing) { - ArrayList<StatusBarIconView> toShow = new ArrayList<>(mNotificationEntries.size()); - // Filter out ambient notifications and notification children. - for (int i = 0; i < mNotificationEntries.size(); i++) { - NotificationEntry entry = mNotificationEntries.get(i).getRepresentativeEntry(); - if (entry != null && entry.getRow() != null) { - if (shouldShowNotificationIcon(entry, showAmbient, showLowPriority, hideDismissed, - hideRepliedMessages, hideCurrentMedia, hidePulsing)) { - StatusBarIconView iconView = function.apply(entry); - if (iconView != null) { - toShow.add(iconView); - } - } - } - } - - // In case we are changing the suppression of a group, the replacement shouldn't flicker - // and it should just be replaced instead. We therefore look for notifications that were - // just replaced by the child or vice-versa to suppress this. - - ArrayMap<String, ArrayList<StatusBarIcon>> replacingIcons = new ArrayMap<>(); - ArrayList<View> toRemove = new ArrayList<>(); - for (int i = 0; i < hostLayout.getChildCount(); i++) { - View child = hostLayout.getChildAt(i); - if (!(child instanceof StatusBarIconView)) { - continue; - } - if (!toShow.contains(child)) { - boolean iconWasReplaced = false; - StatusBarIconView removedIcon = (StatusBarIconView) child; - String removedGroupKey = removedIcon.getNotification().getGroupKey(); - for (int j = 0; j < toShow.size(); j++) { - StatusBarIconView candidate = toShow.get(j); - if (candidate.getSourceIcon().sameAs((removedIcon.getSourceIcon())) - && candidate.getNotification().getGroupKey().equals(removedGroupKey)) { - if (!iconWasReplaced) { - iconWasReplaced = true; - } else { - iconWasReplaced = false; - break; - } - } - } - if (iconWasReplaced) { - ArrayList<StatusBarIcon> statusBarIcons = replacingIcons.get(removedGroupKey); - if (statusBarIcons == null) { - statusBarIcons = new ArrayList<>(); - replacingIcons.put(removedGroupKey, statusBarIcons); - } - statusBarIcons.add(removedIcon.getStatusBarIcon()); - } - toRemove.add(removedIcon); - } - } - // removing all duplicates - ArrayList<String> duplicates = new ArrayList<>(); - for (String key : replacingIcons.keySet()) { - ArrayList<StatusBarIcon> statusBarIcons = replacingIcons.get(key); - if (statusBarIcons.size() != 1) { - duplicates.add(key); - } - } - replacingIcons.removeAll(duplicates); - hostLayout.setReplacingIconsLegacy(replacingIcons); - - final int toRemoveCount = toRemove.size(); - for (int i = 0; i < toRemoveCount; i++) { - hostLayout.removeView(toRemove.get(i)); - } - - final FrameLayout.LayoutParams params = generateIconLayoutParams(); - for (int i = 0; i < toShow.size(); i++) { - StatusBarIconView v = toShow.get(i); - // The view might still be transiently added if it was just removed and added again - hostLayout.removeTransientView(v); - if (v.getParent() == null) { - if (hideDismissed) { - v.setOnDismissListener(mUpdateStatusBarIcons); - } - hostLayout.addView(v, i, params); - } - } - - hostLayout.setChangingViewPositions(true); - // Re-sort notification icons - final int childCount = hostLayout.getChildCount(); - for (int i = 0; i < childCount; i++) { - View actual = hostLayout.getChildAt(i); - StatusBarIconView expected = toShow.get(i); - if (actual == expected) { - continue; - } - hostLayout.removeView(expected); - hostLayout.addView(expected, i); - } - hostLayout.setChangingViewPositions(false); - hostLayout.setReplacingIconsLegacy(null); - } - - /** - * Applies {@link #mIconTint} to the notification icons. - */ - private void applyNotificationIconsTint() { - for (int i = 0; i < mNotificationIcons.getChildCount(); i++) { - final StatusBarIconView iv = (StatusBarIconView) mNotificationIcons.getChildAt(i); - if (iv.getWidth() != 0) { - updateTintForIcon(iv, mIconTint); - } else { - iv.executeOnLayout(() -> updateTintForIcon(iv, mIconTint)); - } - } - - updateAodIconColors(); - } - - private void updateTintForIcon(StatusBarIconView v, int tint) { - boolean isPreL = Boolean.TRUE.equals(v.getTag(R.id.icon_is_pre_L)); - int color = StatusBarIconView.NO_COLOR; - boolean colorize = !isPreL || NotificationUtils.isGrayscale(v, mContrastColorUtil); - if (colorize) { - color = DarkIconDispatcher.getTint(mTintAreas, v, tint); - } - v.setStaticDrawableColor(color); - v.setDecorColor(tint); - } - - public void showIconIsolated(StatusBarIconView icon, boolean animated) { - mNotificationIcons.showIconIsolatedLegacy(icon, animated); - } - - public void setIsolatedIconLocation(@NotNull Rect iconDrawingRect, boolean requireStateUpdate) { - mNotificationIcons.setIsolatedIconLocation(iconDrawingRect, requireStateUpdate); - } - - @Override - public void onDozingChanged(boolean isDozing) { - if (mAodIcons == null) { - return; - } - boolean animate = mDozeParameters.getAlwaysOn() - && !mDozeParameters.getDisplayNeedsBlanking(); - mAodIcons.setDozing(isDozing, animate, 0); - } - - public void setAnimationsEnabled(boolean enabled) { - mAnimationsEnabled = enabled; - updateAnimations(); - } - - @Override - public void onStateChanged(int newState) { - updateAodIconsVisibility(false /* animate */, false /* force */); - updateAnimations(); - } - - private void updateAnimations() { - boolean inShade = mStatusBarStateController.getState() == StatusBarState.SHADE; - if (mAodIcons != null) { - mAodIcons.setAnimationsEnabled(mAnimationsEnabled && !inShade); - } - mNotificationIcons.setAnimationsEnabled(mAnimationsEnabled && inShade); - } - - public void onThemeChanged() { - reloadAodColor(); - updateAodIconColors(); - } - - public int getHeight() { - return mAodIcons == null ? 0 : mAodIcons.getHeight(); - } - - public void appearAodIcons() { - if (mAodIcons == null) { - return; - } - if (mScreenOffAnimationController.shouldAnimateAodIcons()) { - if (!MigrateClocksToBlueprint.isEnabled()) { - mAodIcons.setTranslationY(-mAodIconAppearTranslation); - } - mAodIcons.setAlpha(0); - animateInAodIconTranslation(); - mAodIcons.animate() - .alpha(1) - .setInterpolator(Interpolators.LINEAR) - .setDuration(AOD_ICONS_APPEAR_DURATION) - .start(); - } else { - mAodIcons.setAlpha(1.0f); - if (!MigrateClocksToBlueprint.isEnabled()) { - mAodIcons.setTranslationY(0); - } - } - } - - private void animateInAodIconTranslation() { - if (!MigrateClocksToBlueprint.isEnabled()) { - mAodIcons.animate() - .setInterpolator(Interpolators.DECELERATE_QUINT) - .translationY(0) - .setDuration(AOD_ICONS_APPEAR_DURATION) - .start(); - } - } - - private void reloadAodColor() { - mAodIconTint = Utils.getColorAttrDefaultColor(mContext, - R.attr.wallpaperTextColor, DEFAULT_AOD_ICON_COLOR); - } - - private void updateAodIconColors() { - if (mAodIcons != null) { - for (int i = 0; i < mAodIcons.getChildCount(); i++) { - final StatusBarIconView iv = (StatusBarIconView) mAodIcons.getChildAt(i); - if (iv.getWidth() != 0) { - updateTintForIcon(iv, mAodIconTint); - } else { - iv.executeOnLayout(() -> updateTintForIcon(iv, mAodIconTint)); - } - } - } - } - - @Override - public void onFullyHiddenChanged(boolean fullyHidden) { - boolean animate = true; - if (!mBypassController.getBypassEnabled()) { - animate = mDozeParameters.getAlwaysOn() && !mDozeParameters.getDisplayNeedsBlanking(); - if (!newAodTransition()) { - // We only want the appear animations to happen when the notifications get fully - // hidden, since otherwise the unhide animation overlaps - animate &= fullyHidden; - } - } - updateAodIconsVisibility(animate, false /* force */); - updateAodNotificationIcons(); - updateAodIconColors(); - } - - @Override - public void onPulseExpansionAmountChanged(boolean expandingChanged) { - if (expandingChanged) { - updateAodIconsVisibility(true /* animate */, false /* force */); - } - } - - private void updateAodIconsVisibility(boolean animate, boolean forceUpdate) { - if (mAodIcons == null) { - return; - } - boolean visible = mBypassController.getBypassEnabled() - || mWakeUpCoordinator.getNotificationsFullyHidden(); - - // Hide the AOD icons if we're not in the KEYGUARD state unless the screen off animation is - // playing, in which case we want them to be visible since we're animating in the AOD UI and - // will be switching to KEYGUARD shortly. - if (mStatusBarStateController.getState() != StatusBarState.KEYGUARD - && !mScreenOffAnimationController.shouldShowAodIconsWhenShade()) { - visible = false; - } - if (visible && mWakeUpCoordinator.isPulseExpanding() - && !mBypassController.getBypassEnabled()) { - visible = false; - } - if (mAodIconsVisible != visible || forceUpdate) { - mAodIconsVisible = visible; - mAodIcons.animate().cancel(); - if (animate) { - if (newAodTransition()) { - // Let's make sure the icon are translated to 0, since we cancelled it above - animateInAodIconTranslation(); - if (mAodIconsVisible) { - CrossFadeHelper.fadeIn(mAodIcons); - } else { - CrossFadeHelper.fadeOut(mAodIcons); - } - } else { - boolean wasFullyInvisible = mAodIcons.getVisibility() != View.VISIBLE; - if (mAodIconsVisible) { - if (wasFullyInvisible) { - // No fading here, let's just appear the icons instead! - mAodIcons.setVisibility(View.VISIBLE); - mAodIcons.setAlpha(1.0f); - appearAodIcons(); - } else { - // Let's make sure the icon are translated to 0, since we cancelled it - // above - animateInAodIconTranslation(); - // We were fading out, let's fade in instead - CrossFadeHelper.fadeIn(mAodIcons); - } - } else { - // Let's make sure the icon are translated to 0, since we cancelled it above - animateInAodIconTranslation(); - CrossFadeHelper.fadeOut(mAodIcons); - } - } - } else { - mAodIcons.setAlpha(1.0f); - if (!MigrateClocksToBlueprint.isEnabled()) { - mAodIcons.setTranslationY(0); - } - mAodIcons.setVisibility(visible ? View.VISIBLE : View.INVISIBLE); - } - } - } - - @Override - public List<String> demoCommands() { - ArrayList<String> commands = new ArrayList<>(); - commands.add(DemoMode.COMMAND_NOTIFICATIONS); - return commands; - } - - @Override - public void dispatchDemoCommand(String command, Bundle args) { - if (mNotificationIconArea != null) { - String visible = args.getString("visible"); - int vis = "false".equals(visible) ? View.INVISIBLE : View.VISIBLE; - mNotificationIconArea.setVisibility(vis); - } - } - - @Override - public void onDemoModeFinished() { - if (mNotificationIconArea != null) { - mNotificationIconArea.setVisibility(View.VISIBLE); - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.kt deleted file mode 100644 index 4385a2e168c2..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.kt +++ /dev/null @@ -1,45 +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.statusbar.phone - -import android.content.Context -import android.graphics.Rect -import android.view.View -import com.android.systemui.statusbar.StatusBarIconView -import com.android.systemui.statusbar.notification.collection.ListEntry - -/** - * A controller for the space in the status bar to the left of the system icons. This area is - * normally reserved for notifications. - */ -interface NotificationIconAreaController { - /** Called by the Keyguard*ViewController whose view contains the aod icons. */ - fun setupAodIcons(aodIcons: NotificationIconContainer?) - fun setShelfIcons(icons: NotificationIconContainer) - fun onDensityOrFontScaleChanged(context: Context) - - /** Returns the view that represents the notification area. */ - fun getNotificationInnerAreaView(): View? - - /** Updates the notifications with the given list of notifications to display. */ - fun updateNotificationIcons(entries: List<@JvmSuppressWildcards ListEntry>) - fun updateAodNotificationIcons() - fun showIconIsolated(icon: StatusBarIconView?, animated: Boolean) - fun setIsolatedIconLocation(iconDrawingRect: Rect, requireStateUpdate: Boolean) - fun setAnimationsEnabled(enabled: Boolean) - fun onThemeChanged() - fun getHeight(): Int -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaControllerModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaControllerModule.kt deleted file mode 100644 index ba693708f401..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaControllerModule.kt +++ /dev/null @@ -1,36 +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.statusbar.phone - -import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconAreaControllerViewBinderWrapperImpl -import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor -import dagger.Module -import dagger.Provides -import javax.inject.Provider - -@Module -object NotificationIconAreaControllerModule { - @Provides - fun provideNotificationIconAreaControllerImpl( - legacyProvider: Provider<LegacyNotificationIconAreaControllerImpl>, - newProvider: Provider<NotificationIconAreaControllerViewBinderWrapperImpl>, - ): NotificationIconAreaController = - if (NotificationIconContainerRefactor.isEnabled) { - newProvider.get() - } else { - legacyProvider.get() - } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java index 8e3d678c152a..ecd62bd6943b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java @@ -26,7 +26,6 @@ import android.graphics.Paint; import android.graphics.Rect; import android.graphics.drawable.Icon; import android.util.AttributeSet; -import android.util.MathUtils; import android.util.Property; import android.view.ContextThemeWrapper; import android.view.View; @@ -42,7 +41,6 @@ import com.android.internal.statusbar.StatusBarIcon; import com.android.settingslib.Utils; import com.android.systemui.res.R; import com.android.systemui.statusbar.StatusBarIconView; -import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor; import com.android.systemui.statusbar.notification.stack.AnimationFilter; import com.android.systemui.statusbar.notification.stack.AnimationProperties; import com.android.systemui.statusbar.notification.stack.ViewState; @@ -243,7 +241,7 @@ public class NotificationIconContainer extends ViewGroup { @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { final int childCount = getChildCount(); - final int maxVisibleIcons = getMaxVisibleIcons(childCount); + final int maxVisibleIcons = mMaxIcons; final int width = MeasureSpec.getSize(widthMeasureSpec); final int childWidthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.UNSPECIFIED); int totalWidth = (int) (getActualPaddingStart() + getActualPaddingEnd()); @@ -284,22 +282,13 @@ public class NotificationIconContainer extends ViewGroup { @Override public String toString() { - if (NotificationIconContainerRefactor.isEnabled()) { - return super.toString() - + " {" - + " overrideIconColor=" + mOverrideIconColor - + ", maxIcons=" + mMaxIcons - + ", isStaticLayout=" + mIsStaticLayout - + ", themedTextColorPrimary=#" + Integer.toHexString(mThemedTextColorPrimary) - + " }"; - } else { - return "NotificationIconContainer(" - + "dozing=" + mDozing + " onLockScreen=" + mOnLockScreen - + " overrideIconColor=" + mOverrideIconColor - + " speedBumpIndex=" + mSpeedBumpIndex - + " themedTextColorPrimary=#" + Integer.toHexString(mThemedTextColorPrimary) - + ')'; - } + return super.toString() + + " {" + + " overrideIconColor=" + mOverrideIconColor + + ", maxIcons=" + mMaxIcons + + ", isStaticLayout=" + mIsStaticLayout + + ", themedTextColorPrimary=#" + Integer.toHexString(mThemedTextColorPrimary) + + " }"; } @VisibleForTesting @@ -349,13 +338,8 @@ public class NotificationIconContainer extends ViewGroup { } } if (child instanceof StatusBarIconView) { - if (NotificationIconContainerRefactor.isEnabled()) { - if (!mChangingViewPositions) { - ((StatusBarIconView) child).updateIconDimens(); - } - } else { + if (!mChangingViewPositions) { ((StatusBarIconView) child).updateIconDimens(); - ((StatusBarIconView) child).setDozing(mDozing, false, 0); } } } @@ -367,23 +351,11 @@ public class NotificationIconContainer extends ViewGroup { StatusBarIconView iconView = (StatusBarIconView) child; Icon sourceIcon = iconView.getSourceIcon(); String groupKey = iconView.getNotification().getGroupKey(); - if (NotificationIconContainerRefactor.isEnabled()) { - if (mReplacingIcons == null) { - return false; - } - StatusBarIcon replacedIcon = mReplacingIcons.get(groupKey); - return replacedIcon != null && sourceIcon.sameAs(replacedIcon.icon); - } else { - if (mReplacingIconsLegacy == null) { - return false; - } - ArrayList<StatusBarIcon> statusBarIcons = mReplacingIconsLegacy.get(groupKey); - if (statusBarIcons != null) { - StatusBarIcon replacedIcon = statusBarIcons.get(0); - return sourceIcon.sameAs(replacedIcon.icon); - } + if (mReplacingIcons == null) { return false; } + StatusBarIcon replacedIcon = mReplacingIcons.get(groupKey); + return replacedIcon != null && sourceIcon.sameAs(replacedIcon.icon); } @Override @@ -468,24 +440,14 @@ public class NotificationIconContainer extends ViewGroup { if (numIcons == 0) { return 0f; } - final float contentWidth; - if (NotificationIconContainerRefactor.isEnabled()) { - contentWidth = mIconSize * numIcons; - } else { - contentWidth = mIconSize * MathUtils.min(numIcons, mMaxIconsOnLockscreen + 1); - } + final float contentWidth = mIconSize * numIcons; return getActualPaddingStart() + contentWidth + getActualPaddingEnd(); } @VisibleForTesting boolean shouldForceOverflow(int i, int speedBumpIndex, float iconAppearAmount, int maxVisibleIcons) { - if (NotificationIconContainerRefactor.isEnabled()) { - return i >= maxVisibleIcons && iconAppearAmount > 0.0f; - } else { - return speedBumpIndex != -1 && i >= speedBumpIndex - && iconAppearAmount > 0.0f || i >= maxVisibleIcons; - } + return i >= maxVisibleIcons && iconAppearAmount > 0.0f; } @VisibleForTesting @@ -510,7 +472,7 @@ public class NotificationIconContainer extends ViewGroup { float translationX = getActualPaddingStart(); int firstOverflowIndex = -1; int childCount = getChildCount(); - int maxVisibleIcons = getMaxVisibleIcons(childCount); + int maxVisibleIcons = mMaxIcons; float layoutEnd = getLayoutEnd(); mVisualOverflowStart = 0; mFirstVisibleIconState = null; @@ -592,27 +554,15 @@ public class NotificationIconContainer extends ViewGroup { } private float getDrawingScale(View view) { - final boolean useIncreasedScale = NotificationIconContainerRefactor.isEnabled() - ? mUseIncreasedIconScale - : mOnLockScreen; - return useIncreasedScale && view instanceof StatusBarIconView + return mUseIncreasedIconScale && view instanceof StatusBarIconView ? ((StatusBarIconView) view).getIconScaleIncreased() : 1f; } public void setUseIncreasedIconScale(boolean useIncreasedIconScale) { - if (NotificationIconContainerRefactor.isUnexpectedlyInLegacyMode()) return; mUseIncreasedIconScale = useIncreasedIconScale; } - private int getMaxVisibleIcons(int childCount) { - if (NotificationIconContainerRefactor.isEnabled()) { - return mMaxIcons; - } else { - return mOnLockScreen ? mMaxIconsOnAod : mIsStaticLayout ? mMaxStaticIcons : childCount; - } - } - private float getLayoutEnd() { return getActualWidth() - getActualPaddingEnd(); } @@ -689,50 +639,11 @@ public class NotificationIconContainer extends ViewGroup { mChangingViewPositions = changingViewPositions; } - public void setDozing(boolean dozing, boolean animate, long delay) { - NotificationIconContainerRefactor.assertInLegacyMode(); - setDozing(dozing, animate, delay, /* endRunnable= */ null); - } - - private void setDozing(boolean dozing, boolean animate, long delay, - @Nullable Runnable endRunnable) { - NotificationIconContainerRefactor.assertInLegacyMode(); - mDozing = dozing; - mDisallowNextAnimation |= !animate; - final int childCount = getChildCount(); - // Track all the child invocations of setDozing, invoking the top-level endRunnable once - // they have all completed. - final Runnable onChildCompleted = endRunnable == null ? null : new Runnable() { - private int mPendingCallbacks = childCount; - - @Override - public void run() { - if (--mPendingCallbacks == 0) { - endRunnable.run(); - } - } - }; - for (int i = 0; i < childCount; i++) { - View view = getChildAt(i); - if (view instanceof StatusBarIconView) { - ((StatusBarIconView) view).setDozing(dozing, animate, delay, onChildCompleted); - } else if (onChildCompleted != null) { - onChildCompleted.run(); - } - } - } - public IconState getIconState(StatusBarIconView icon) { return mIconStates.get(icon); } - public void setSpeedBumpIndex(int speedBumpIndex) { - NotificationIconContainerRefactor.assertInLegacyMode(); - mSpeedBumpIndex = speedBumpIndex; - } - public void setMaxIconsAmount(int maxIcons) { - if (NotificationIconContainerRefactor.isUnexpectedlyInLegacyMode()) return; mMaxIcons = maxIcons; } @@ -754,36 +665,18 @@ public class NotificationIconContainer extends ViewGroup { mAnimationsEnabled = enabled; } - public void setReplacingIconsLegacy(ArrayMap<String, ArrayList<StatusBarIcon>> replacingIcons) { - NotificationIconContainerRefactor.assertInLegacyMode(); - mReplacingIconsLegacy = replacingIcons; - } - public void setReplacingIcons(ArrayMap<String, StatusBarIcon> replacingIcons) { - if (NotificationIconContainerRefactor.isUnexpectedlyInLegacyMode()) return; mReplacingIcons = replacingIcons; } - @Deprecated - public void showIconIsolatedLegacy(StatusBarIconView icon, boolean animated) { - NotificationIconContainerRefactor.assertInLegacyMode(); - if (animated) { - mIsolatedIconForAnimation = icon != null ? icon : mIsolatedIcon; - } - mIsolatedIcon = icon; - updateState(); - } - public void showIconIsolatedAnimated(StatusBarIconView icon, @Nullable Runnable onAnimationEnd) { - if (NotificationIconContainerRefactor.isUnexpectedlyInLegacyMode()) return; mIsolatedIconForAnimation = icon != null ? icon : mIsolatedIcon; mIsolatedIconAnimationEndRunnable = onAnimationEnd; showIconIsolated(icon); } public void showIconIsolated(StatusBarIconView icon) { - if (NotificationIconContainerRefactor.isUnexpectedlyInLegacyMode()) return; mIsolatedIcon = icon; updateState(); } @@ -795,23 +688,7 @@ public class NotificationIconContainer extends ViewGroup { } } - /** - * Set whether the device is on the lockscreen and which lockscreen mode the device is - * configured to. Depending on these values, the layout of the AOD icons change. - */ - public void setOnLockScreen(boolean onLockScreen) { - NotificationIconContainerRefactor.assertInLegacyMode(); - mOnLockScreen = onLockScreen; - } - - @Deprecated - public void setInNotificationIconShelf(boolean inShelf) { - NotificationIconContainerRefactor.assertInLegacyMode(); - mOverrideIconColor = inShelf; - } - public void setOverrideIconColor(boolean override) { - if (NotificationIconContainerRefactor.isUnexpectedlyInLegacyMode()) return; mOverrideIconColor = override; } @@ -922,14 +799,9 @@ public class NotificationIconContainer extends ViewGroup { } } icon.setVisibleState(visibleState, animationsAllowed); - if (NotificationIconContainerRefactor.isEnabled()) { - if (mOverrideIconColor) { - icon.setIconColor(mThemedTextColorPrimary, - /* animate= */ needsCannedAnimation && animationsAllowed); - } - } else { - icon.setIconColor(mOverrideIconColor ? mThemedTextColorPrimary : iconColor, - needsCannedAnimation && animationsAllowed); + if (mOverrideIconColor) { + icon.setIconColor(mThemedTextColorPrimary, + /* animate= */ needsCannedAnimation && animationsAllowed); } if (animate) { animateTo(icon, animationProperties); 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..f8f9b77c2b22 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. @@ -968,7 +975,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb hideBouncer(false /* destroyView */); } } else { - showBouncerOrKeyguard(hideBouncerWhenShowing); + showBouncerOrKeyguard(hideBouncerWhenShowing, isFalsingReset); } if (hideBouncerWhenShowing) { hideAlternateBouncer(true); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java index 07c190d89393..5be4ba222174 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java @@ -59,8 +59,6 @@ import com.android.systemui.statusbar.disableflags.DisableFlagsLogger.DisableSta import com.android.systemui.statusbar.events.SystemStatusAnimationCallback; import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler; import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerStatusBarViewBinder; -import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor; -import com.android.systemui.statusbar.phone.NotificationIconAreaController; import com.android.systemui.statusbar.phone.NotificationIconContainer; import com.android.systemui.statusbar.phone.PhoneStatusBarView; import com.android.systemui.statusbar.phone.StatusBarHideIconsForBouncerManager; @@ -140,7 +138,6 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue private final OngoingCallController mOngoingCallController; private final SystemStatusAnimationScheduler mAnimationScheduler; private final StatusBarLocationPublisher mLocationPublisher; - private final NotificationIconAreaController mNotificationIconAreaController; private final ShadeExpansionStateManager mShadeExpansionStateManager; private final StatusBarIconController mStatusBarIconController; private final CarrierConfigTracker mCarrierConfigTracker; @@ -237,7 +234,6 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue OngoingCallController ongoingCallController, SystemStatusAnimationScheduler animationScheduler, StatusBarLocationPublisher locationPublisher, - NotificationIconAreaController notificationIconAreaController, ShadeExpansionStateManager shadeExpansionStateManager, StatusBarIconController statusBarIconController, DarkIconManager.Factory darkIconManagerFactory, @@ -262,7 +258,6 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue mOngoingCallController = ongoingCallController; mAnimationScheduler = animationScheduler; mLocationPublisher = locationPublisher; - mNotificationIconAreaController = notificationIconAreaController; mShadeExpansionStateManager = shadeExpansionStateManager; mStatusBarIconController = statusBarIconController; mCollapsedStatusBarViewModel = collapsedStatusBarViewModel; @@ -313,18 +308,14 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mStatusBarWindowStateController.addListener(mStatusBarWindowStateListener); - if (NotificationIconContainerRefactor.isEnabled()) { - mDemoModeController.addCallback(mDemoModeCallback); - } + mDemoModeController.addCallback(mDemoModeCallback); } @Override public void onDestroy() { super.onDestroy(); mStatusBarWindowStateController.removeListener(mStatusBarWindowStateListener); - if (NotificationIconContainerRefactor.isEnabled()) { - mDemoModeController.removeCallback(mDemoModeCallback); - } + mDemoModeController.removeCallback(mDemoModeCallback); } @Override @@ -471,11 +462,9 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue mStartableStates.put(startable, Startable.State.STOPPED); } mDumpManager.unregisterDumpable(getClass().getSimpleName()); - if (NotificationIconContainerRefactor.isEnabled()) { - if (mNicBindingDisposable != null) { - mNicBindingDisposable.dispose(); - mNicBindingDisposable = null; - } + if (mNicBindingDisposable != null) { + mNicBindingDisposable.dispose(); + mNicBindingDisposable = null; } } @@ -483,22 +472,12 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue public void initNotificationIconArea() { Trace.beginSection("CollapsedStatusBarFragment#initNotifIconArea"); ViewGroup notificationIconArea = mStatusBar.requireViewById(R.id.notification_icon_area); - if (NotificationIconContainerRefactor.isEnabled()) { - LayoutInflater.from(getContext()) - .inflate(R.layout.notification_icon_area, notificationIconArea, true); - NotificationIconContainer notificationIcons = - notificationIconArea.requireViewById(R.id.notificationIcons); - mNotificationIconAreaInner = notificationIcons; - mNicBindingDisposable = mNicViewBinder.bindWhileAttached(notificationIcons); - } else { - mNotificationIconAreaInner = - mNotificationIconAreaController.getNotificationInnerAreaView(); - if (mNotificationIconAreaInner.getParent() != null) { - ((ViewGroup) mNotificationIconAreaInner.getParent()) - .removeView(mNotificationIconAreaInner); - } - notificationIconArea.addView(mNotificationIconAreaInner); - } + LayoutInflater.from(getContext()) + .inflate(R.layout.notification_icon_area, notificationIconArea, true); + NotificationIconContainer notificationIcons = + notificationIconArea.requireViewById(R.id.notificationIcons); + mNotificationIconAreaInner = notificationIcons; + mNicBindingDisposable = mNicViewBinder.bindWhileAttached(notificationIcons); updateNotificationIconAreaAndOngoingActivityChip(/* animate= */ false); Trace.endSection(); 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 9422878f628b..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 @@ -23,9 +23,9 @@ import android.provider.Settings.ACTION_AUTOMATIC_ZEN_RULE_SETTINGS import android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID import com.android.settingslib.notification.modes.EnableZenModeDialog import com.android.settingslib.notification.modes.ZenMode -import com.android.settingslib.notification.modes.ZenModeDialogMetricsLogger import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.qs.tiles.dialog.QSZenModeDialogMetricsLogger import com.android.systemui.res.R import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor @@ -50,7 +50,7 @@ constructor( @Background val bgDispatcher: CoroutineDispatcher, private val dialogDelegate: ModesDialogDelegate, ) { - private val zenDialogMetricsLogger = ZenModeDialogMetricsLogger(context) + private val zenDialogMetricsLogger = QSZenModeDialogMetricsLogger(context) // Modes that should be displayed in the dialog private val visibleModes: Flow<List<ZenMode>> = @@ -131,7 +131,7 @@ constructor( val on = context.resources.getString(R.string.zen_mode_on) val off = context.resources.getString(R.string.zen_mode_off) - return mode.rule.triggerDescription ?: if (mode.isActive) on else off + return mode.getDynamicDescription(context) ?: if (mode.isActive) on else off } private fun makeZenModeDialog(): Dialog { diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/ActionKeyTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/ActionKeyTutorialScreen.kt new file mode 100644 index 000000000000..f7f26314bb50 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/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.touchpad.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.res.R +import com.android.systemui.touchpad.tutorial.ui.composable.TutorialActionState.FINISHED +import com.android.systemui.touchpad.tutorial.ui.composable.TutorialActionState.NOT_STARTED + +@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/touchpad/tutorial/ui/composable/ActionTutorialContent.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/ActionTutorialContent.kt new file mode 100644 index 000000000000..2b7f6742e335 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/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.touchpad.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.touchpad.tutorial.ui.composable.TutorialActionState.FINISHED +import com.android.systemui.touchpad.tutorial.ui.composable.TutorialActionState.IN_PROGRESS +import com.android.systemui.touchpad.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/BackGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt index 1b00ae2103f0..5980e1de85b4 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt @@ -38,8 +38,8 @@ fun BackGestureTutorialScreen( TutorialScreenConfig.Strings( titleResId = R.string.touchpad_back_gesture_action_title, bodyResId = R.string.touchpad_back_gesture_guidance, - titleSuccessResId = R.string.touchpad_tutorial_gesture_done, - bodySuccessResId = R.string.touchpad_back_gesture_finished + titleSuccessResId = R.string.touchpad_back_gesture_success_title, + bodySuccessResId = R.string.touchpad_back_gesture_success_body ), animations = TutorialScreenConfig.Animations( diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt index 416c562d212d..dfe9c0df9ab7 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,18 @@ 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.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 +42,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 +73,11 @@ fun GestureTutorialScreen( ) } TouchpadGesturesHandlingBox(gestureHandler, gestureState) { - GestureTutorialContent(gestureState, onDoneButtonClicked, screenConfig) + ActionTutorialContent( + gestureState.toTutorialActionState(), + onDoneButtonClicked, + screenConfig + ) } } @@ -135,165 +108,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..ed3110c04131 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt @@ -0,0 +1,84 @@ +/* + * 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.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/view/TouchpadTutorialActivity.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt index 088a8fd95e60..48e6397a0a42 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.touchpad.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/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/sensors/ProximityCheck.java b/packages/SystemUI/src/com/android/systemui/util/sensors/ProximityCheck.java index 373417b7bd68..5b9a6c556ec6 100644 --- a/packages/SystemUI/src/com/android/systemui/util/sensors/ProximityCheck.java +++ b/packages/SystemUI/src/com/android/systemui/util/sensors/ProximityCheck.java @@ -86,13 +86,15 @@ public class ProximityCheck implements Runnable { } private void onProximityEvent(ThresholdSensorEvent proximityEvent) { + // Move the callbacks to a local to avoid ConcurrentModificationException List<Consumer<Boolean>> oldCallbacks = mCallbacks; mCallbacks = new ArrayList<>(); + // Unregister from the ProximitySensor to ensure a re-entrant check will re-register + unregister(); + // Notify the callbacks oldCallbacks.forEach( booleanConsumer -> booleanConsumer.accept( proximityEvent == null ? null : proximityEvent.getBelow())); - unregister(); - mRegistered.set(false); } } 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/tests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java index 319b61512f12..2bb9e68a357a 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java @@ -55,7 +55,6 @@ import com.android.systemui.shared.clocks.ClockRegistry; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController; import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerAlwaysOnDisplayViewBinder; -import com.android.systemui.statusbar.phone.NotificationIconAreaController; import com.android.systemui.statusbar.phone.NotificationIconContainer; import com.android.systemui.util.concurrency.FakeExecutor; import com.android.systemui.util.settings.SecureSettings; @@ -78,8 +77,6 @@ public class KeyguardClockSwitchControllerBaseTest extends SysuiTestCase { @Mock KeyguardSliceViewController mKeyguardSliceViewController; @Mock - NotificationIconAreaController mNotificationIconAreaController; - @Mock LockscreenSmartspaceController mSmartspaceController; @Mock @@ -176,7 +173,6 @@ public class KeyguardClockSwitchControllerBaseTest extends SysuiTestCase { mStatusBarStateController, mClockRegistry, mKeyguardSliceViewController, - mNotificationIconAreaController, mSmartspaceController, mock(NotificationIconContainerAlwaysOnDisplayViewBinder.class), mKeyguardUnlockAnimationController, 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/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt index 512d946e92d5..534f25c33900 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 @@ -503,11 +503,13 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa collectLastValue(kosmos.promptViewModel.iconViewModel.contentDescriptionId) val shouldAnimateIconView by collectLastValue(kosmos.promptViewModel.iconViewModel.shouldAnimateIconView) + val message by collectLastValue(kosmos.promptViewModel.message) verifyIconSize() kosmos.promptViewModel.showAuthenticated( modality = testCase.authenticatedModality, - dismissAfterDelay = DELAY + dismissAfterDelay = DELAY, + "TEST" ) if (testCase.isFingerprintOnly) { @@ -531,6 +533,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa assertThat(iconContentDescriptionId) .isEqualTo(R.string.biometric_dialog_face_icon_description_authenticated) assertThat(shouldAnimateIconView).isEqualTo(true) + assertThat(message).isEqualTo(PromptMessage.Empty) } } } 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/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractorTest.kt index 79e312fec5f8..77337d36a6b1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractorTest.kt @@ -21,8 +21,12 @@ import android.content.mockedContext import android.hardware.fingerprint.FingerprintManager import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.compose.animation.scene.ObservableTransitionState import com.android.systemui.SysuiTestCase import com.android.systemui.bouncer.data.repository.keyguardBouncerRepository +import com.android.systemui.communal.data.repository.communalSceneRepository +import com.android.systemui.communal.data.repository.fakeCommunalSceneRepository +import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.data.repository.biometricSettingsRepository import com.android.systemui.keyguard.data.repository.deviceEntryFingerprintAuthRepository @@ -41,6 +45,7 @@ import com.android.systemui.testKosmos import com.android.systemui.util.mockito.any import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Test @@ -65,6 +70,7 @@ class OccludingAppDeviceEntryInteractorTest : SysuiTestCase() { private val bouncerRepository = kosmos.keyguardBouncerRepository private val powerRepository = kosmos.fakePowerRepository private val biometricSettingsRepository = kosmos.biometricSettingsRepository + private val communalSceneRepository = kosmos.communalSceneRepository private val mockedContext = kosmos.mockedContext private val mockedActivityStarter = kosmos.activityStarter @@ -143,6 +149,20 @@ class OccludingAppDeviceEntryInteractorTest : SysuiTestCase() { } @Test + fun lockout_onOccludingApp_onCommunal_neverGoToHomeScreen() = + testScope.runTest { + givenOnOccludingApp(isOnOccludingApp = true, isOnCommunal = true) + fingerprintAuthRepository.setAuthenticationStatus( + ErrorFingerprintAuthenticationStatus( + FingerprintManager.FINGERPRINT_ERROR_LOCKOUT, + "lockoutTest" + ) + ) + runCurrent() + verifyNeverGoToHomeScreen() + } + + @Test fun message_fpFailOnOccludingApp_thenNotOnOccludingApp() = testScope.runTest { val message by collectLastValue(underTest.message) @@ -261,7 +281,10 @@ class OccludingAppDeviceEntryInteractorTest : SysuiTestCase() { assertThat(message).isNull() } - private suspend fun givenOnOccludingApp(isOnOccludingApp: Boolean) { + private suspend fun givenOnOccludingApp( + isOnOccludingApp: Boolean, + isOnCommunal: Boolean = false + ) { powerRepository.setInteractive(true) keyguardRepository.setIsDozing(false) keyguardRepository.setKeyguardOccluded(isOnOccludingApp) @@ -270,6 +293,14 @@ class OccludingAppDeviceEntryInteractorTest : SysuiTestCase() { bouncerRepository.setPrimaryShow(!isOnOccludingApp) bouncerRepository.setAlternateVisible(!isOnOccludingApp) + kosmos.fakeCommunalSceneRepository.setTransitionState( + flowOf( + ObservableTransitionState.Idle( + if (isOnCommunal) CommunalScenes.Communal else CommunalScenes.Blank + ) + ) + ) + if (isOnOccludingApp) { kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( from = KeyguardState.LOCKSCREEN, diff --git a/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.kt index 20cb1e129f49..0ac04b61f13d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.kt @@ -28,6 +28,7 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.FlowValue import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.coroutines.collectValues import com.android.systemui.util.mockito.kotlinArgumentCaptor import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever @@ -456,8 +457,20 @@ class DisplayRepositoryTest : SysuiTestCase() { assertThat(value?.ids()).containsExactly(DEFAULT_DISPLAY) } + @Test + fun displayFlow_emitsCorrectDisplaysAtFirst() = + testScope.runTest { + setDisplays(0, 1, 2) + + val values: List<Set<Display>> by collectValues(displayRepository.displays) + + assertThat(values.toIdSets()).containsExactly(setOf(0, 1, 2)) + } + private fun Iterable<Display>.ids(): List<Int> = map { it.displayId } + private fun Iterable<Set<Display>>.toIdSets(): List<Set<Int>> = map { it.ids().toSet() } + // Wrapper to capture the displayListener. private fun TestScope.latestDisplayFlowValue(): FlowValue<Set<Display>?> { val flowValue = collectLastValue(displayRepository.displays) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt index e44bc7b43fb1..313292a5fab8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt @@ -102,34 +102,6 @@ class AlternateBouncerViewModelTest : SysuiTestCase() { } @Test - fun forcePluginOpen() = - testScope.runTest { - val forcePluginOpen by collectLastValue(underTest.forcePluginOpen) - - transitionRepository.sendTransitionSteps( - listOf( - stepToAlternateBouncer(0f, TransitionState.STARTED), - stepToAlternateBouncer(.4f), - stepToAlternateBouncer(.6f), - stepToAlternateBouncer(1f), - ), - testScope, - ) - assertThat(forcePluginOpen).isTrue() - - transitionRepository.sendTransitionSteps( - listOf( - stepFromAlternateBouncer(0f, TransitionState.STARTED), - stepFromAlternateBouncer(.3f), - stepFromAlternateBouncer(.6f), - stepFromAlternateBouncer(1f), - ), - testScope, - ) - assertThat(forcePluginOpen).isFalse() - } - - @Test fun registerForDismissGestures() = testScope.runTest { val registerForDismissGestures by collectLastValue(underTest.registerForDismissGestures) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt index 77977f3f1115..24bea2ce51c7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt @@ -195,9 +195,7 @@ class KeyguardQuickAffordancesCombinedViewModelTest : SysuiTestCase() { mSetFlagsRule.enableFlags(AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR) val featureFlags = - FakeFeatureFlags().apply { - set(Flags.LOCK_SCREEN_LONG_PRESS_ENABLED, false) - } + FakeFeatureFlags().apply { set(Flags.LOCK_SCREEN_LONG_PRESS_ENABLED, false) } val withDeps = KeyguardInteractorFactory.create(featureFlags = featureFlags) keyguardInteractor = withDeps.keyguardInteractor @@ -289,6 +287,7 @@ class KeyguardQuickAffordancesCombinedViewModelTest : SysuiTestCase() { underTest = KeyguardQuickAffordancesCombinedViewModel( + applicationScope = testScope.backgroundScope, quickAffordanceInteractor = KeyguardQuickAffordanceInteractor( keyguardInteractor = keyguardInteractor, diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/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/mediaprojection/permission/SystemCastPermissionDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/permission/SystemCastPermissionDialogDelegateTest.kt index 59602dcca091..6495b66cc148 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/permission/SystemCastPermissionDialogDelegateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/permission/SystemCastPermissionDialogDelegateTest.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 @@ -117,6 +118,36 @@ class SystemCastPermissionDialogDelegateTest : 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_cast_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, @@ -144,4 +175,10 @@ class SystemCastPermissionDialogDelegateTest : 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/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/NotificationListenerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationListenerTest.java index ef1c927f22d7..27a1bb5ea556 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationListenerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationListenerTest.java @@ -24,7 +24,6 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.when; import android.app.Notification; import android.app.NotificationManager; @@ -41,7 +40,6 @@ import com.android.systemui.plugins.PluginManager; import com.android.systemui.statusbar.NotificationListener.NotificationHandler; import com.android.systemui.statusbar.data.repository.NotificationListenerSettingsRepository; import com.android.systemui.statusbar.domain.interactor.SilentNotificationStatusIconsVisibilityInteractor; -import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor; import com.android.systemui.util.concurrency.FakeExecutor; import com.android.systemui.util.time.FakeSystemClock; @@ -149,30 +147,4 @@ public class NotificationListenerTest extends SysuiTestCase { verify(mNotificationHandler).onNotificationRankingUpdate(eq(ranking3)); verifyNoMoreInteractions(mNotificationHandler); } - - @Test - public void testOnConnectReadStatusBarSetting() { - mSetFlagsRule.disableFlags(NotificationIconContainerRefactor.FLAG_NAME); - NotificationListener.NotificationSettingsListener settingsListener = - mock(NotificationListener.NotificationSettingsListener.class); - mListener.addNotificationSettingsListener(settingsListener); - - when(mNotificationManager.shouldHideSilentStatusBarIcons()).thenReturn(true); - - mListener.onListenerConnected(); - - verify(settingsListener).onStatusBarIconsBehaviorChanged(true); - } - - @Test - public void testOnStatusBarIconsBehaviorChanged() { - mSetFlagsRule.disableFlags(NotificationIconContainerRefactor.FLAG_NAME); - NotificationListener.NotificationSettingsListener settingsListener = - mock(NotificationListener.NotificationSettingsListener.class); - mListener.addNotificationSettingsListener(settingsListener); - - mListener.onSilentStatusBarIconsVisibilityChanged(true); - - verify(settingsListener).onStatusBarIconsBehaviorChanged(true); - } } 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/collection/coordinator/StackCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt index c7513de7a41a..ad6aca1dcd4f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt @@ -34,10 +34,8 @@ import com.android.systemui.statusbar.notification.collection.render.NotifStats import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.domain.interactor.RenderNotificationListInteractor import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor -import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor import com.android.systemui.statusbar.notification.stack.BUCKET_ALERTING import com.android.systemui.statusbar.notification.stack.BUCKET_SILENT -import com.android.systemui.statusbar.phone.NotificationIconAreaController import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.withArgCaptor @@ -47,8 +45,8 @@ import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.verify import org.mockito.Mockito.verifyZeroInteractions -import org.mockito.MockitoAnnotations.initMocks import org.mockito.Mockito.`when` as whenever +import org.mockito.MockitoAnnotations.initMocks @SmallTest @RunWith(AndroidJUnit4::class) @@ -61,10 +59,10 @@ class StackCoordinatorTest : SysuiTestCase() { @Mock private lateinit var pipeline: NotifPipeline @Mock private lateinit var groupExpansionManagerImpl: GroupExpansionManagerImpl - @Mock private lateinit var notificationIconAreaController: NotificationIconAreaController @Mock private lateinit var renderListInteractor: RenderNotificationListInteractor @Mock private lateinit var activeNotificationsInteractor: ActiveNotificationsInteractor - @Mock private lateinit var sensitiveNotificationProtectionController: + @Mock + private lateinit var sensitiveNotificationProtectionController: SensitiveNotificationProtectionController @Mock private lateinit var stackController: NotifStackController @Mock private lateinit var section: NotifSection @@ -73,14 +71,12 @@ class StackCoordinatorTest : SysuiTestCase() { fun setUp() { initMocks(this) - whenever(sensitiveNotificationProtectionController.isSensitiveStateActive) - .thenReturn(false) + whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(false) entry = NotificationEntryBuilder().setSection(section).build() coordinator = StackCoordinator( groupExpansionManagerImpl, - notificationIconAreaController, renderListInteractor, activeNotificationsInteractor, sensitiveNotificationProtectionController, @@ -92,15 +88,7 @@ class StackCoordinatorTest : SysuiTestCase() { } @Test - @DisableFlags(NotificationIconContainerRefactor.FLAG_NAME) - fun testUpdateNotificationIcons() { - afterRenderListListener.onAfterRenderList(listOf(entry), stackController) - verify(notificationIconAreaController).updateNotificationIcons(eq(listOf(entry))) - } - - @Test - @EnableFlags(NotificationIconContainerRefactor.FLAG_NAME) - fun testSetRenderedListOnInteractor_iconContainerFlagOn() { + fun testSetRenderedListOnInteractor() { afterRenderListListener.onAfterRenderList(listOf(entry), stackController) verify(renderListInteractor).setRenderedList(eq(listOf(entry))) } 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/notification/stack/NotificationShelfTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt index c1f2cb77f411..e4945fc6e847 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt @@ -1,6 +1,5 @@ package com.android.systemui.statusbar.notification.stack -import android.platform.test.annotations.DisableFlags import android.service.notification.StatusBarNotification import android.testing.TestableLooper.RunWithLooper import android.view.LayoutInflater @@ -21,7 +20,6 @@ import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.ExpandableView -import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm.StackScrollAlgorithmState import com.android.systemui.util.mockito.mock import junit.framework.Assert.assertEquals @@ -72,32 +70,6 @@ open class NotificationShelfTest : SysuiTestCase() { } @Test - @DisableFlags(NotificationIconContainerRefactor.FLAG_NAME) - fun testShadeWidth_BasedOnFractionToShade() { - setFractionToShade(0f) - setOnLockscreen(true) - - shelf.updateActualWidth(/* fractionToShade */ 0f, /* shortestWidth */ 10f) - assertTrue(shelf.actualWidth == 10) - - shelf.updateActualWidth(/* fractionToShade */ 0.5f, /* shortestWidth */ 10f) - assertTrue(shelf.actualWidth == 20) - - shelf.updateActualWidth(/* fractionToShade */ 1f, /* shortestWidth */ 10f) - assertTrue(shelf.actualWidth == 30) - } - - @Test - @DisableFlags(NotificationIconContainerRefactor.FLAG_NAME) - fun testShelfIsLong_WhenNotOnLockscreen() { - setFractionToShade(0f) - setOnLockscreen(false) - - shelf.updateActualWidth(/* fraction */ 0f, /* shortestWidth */ 10f) - assertTrue(shelf.actualWidth == 30) - } - - @Test fun testX_inViewForClick() { val isXInView = shelf.isXInView(/* localX */ 5f, /* slop */ 5f, /* left */ 0f, /* right */ 10f) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java index a6b27294546c..e9c16c207ea8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java @@ -295,7 +295,6 @@ public class CentralSurfacesImplTest extends SysuiTestCase { @Mock private Bubbles mBubbles; @Mock private NoteTaskController mNoteTaskController; @Mock private NotificationShadeWindowController mNotificationShadeWindowController; - @Mock private NotificationIconAreaController mNotificationIconAreaController; @Mock private NotificationShadeWindowViewController mNotificationShadeWindowViewController; @Mock private Lazy<NotificationShadeWindowViewController> mNotificationShadeWindowViewControllerLazy; @@ -580,7 +579,6 @@ public class CentralSurfacesImplTest extends SysuiTestCase { mDemoModeController, mNotificationShadeDepthControllerLazy, mStatusBarTouchableRegionManager, - mNotificationIconAreaController, mBrightnessSliderFactory, mScreenOffAnimationController, mWallpaperController, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java index 0d06b6431e1c..dd03ab393ce9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java @@ -110,7 +110,6 @@ public class HeadsUpAppearanceControllerTest extends SysuiTestCase { mNotificationRoundnessManager = mock(NotificationRoundnessManager.class); when(mShadeViewController.getShadeHeadsUpTracker()).thenReturn(mShadeHeadsUpTracker); mHeadsUpAppearanceController = new HeadsUpAppearanceController( - mock(NotificationIconAreaController.class), mHeadsUpManager, mStatusbarStateController, mPhoneStatusBarTransitions, @@ -197,7 +196,6 @@ public class HeadsUpAppearanceControllerTest extends SysuiTestCase { when(mStackScrollerController.getExpandedHeight()).thenReturn(expandedHeight); HeadsUpAppearanceController newController = new HeadsUpAppearanceController( - mock(NotificationIconAreaController.class), mHeadsUpManager, mStatusbarStateController, mPhoneStatusBarTransitions, 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/LegacyNotificationIconAreaControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImplTest.java deleted file mode 100644 index 8dfbb37f8189..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImplTest.java +++ /dev/null @@ -1,129 +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.statusbar.phone; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -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.systemui.Flags; -import com.android.systemui.SysuiTestCase; -import com.android.systemui.demomode.DemoModeController; -import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.plugins.DarkIconDispatcher; -import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.statusbar.NotificationListener; -import com.android.systemui.statusbar.NotificationMediaManager; -import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator; -import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider; -import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor; -import com.android.systemui.statusbar.window.StatusBarWindowController; -import com.android.wm.shell.bubbles.Bubbles; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import java.util.Optional; - -@SmallTest -@RunWith(AndroidJUnit4.class) -@TestableLooper.RunWithLooper -@DisableFlags(NotificationIconContainerRefactor.FLAG_NAME) -public class LegacyNotificationIconAreaControllerImplTest extends SysuiTestCase { - - @Mock - private NotificationListener mListener; - @Mock - StatusBarStateController mStatusBarStateController; - @Mock - NotificationWakeUpCoordinator mWakeUpCoordinator; - @Mock - KeyguardBypassController mKeyguardBypassController; - @Mock - NotificationMediaManager mNotificationMediaManager; - @Mock - DozeParameters mDozeParameters; - @Mock - SectionStyleProvider mSectionStyleProvider; - @Mock - DarkIconDispatcher mDarkIconDispatcher; - @Mock - StatusBarWindowController mStatusBarWindowController; - @Mock - ScreenOffAnimationController mScreenOffAnimationController; - private LegacyNotificationIconAreaControllerImpl mController; - @Mock - private Bubbles mBubbles; - @Mock private DemoModeController mDemoModeController; - @Mock - private NotificationIconContainer mAodIcons; - @Mock - private FeatureFlags mFeatureFlags; - - @Before - public void setup() { - MockitoAnnotations.initMocks(this); - mController = new LegacyNotificationIconAreaControllerImpl( - mContext, - mStatusBarStateController, - mWakeUpCoordinator, - mKeyguardBypassController, - mNotificationMediaManager, - mListener, - mDozeParameters, - mSectionStyleProvider, - Optional.of(mBubbles), - mDemoModeController, - mDarkIconDispatcher, - mFeatureFlags, - mStatusBarWindowController, - mScreenOffAnimationController); - } - - @Test - public void testNotificationIcons_settingHideIcons() { - mController.mSettingsListener.onStatusBarIconsBehaviorChanged(true); - - assertFalse(mController.shouldShouldLowPriorityIcons()); - } - - @Test - public void testNotificationIcons_settingShowIcons() { - mController.mSettingsListener.onStatusBarIconsBehaviorChanged(false); - - assertTrue(mController.shouldShouldLowPriorityIcons()); - } - - @Test - @DisableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) - public void testAppearResetsTranslation() { - mController.setupAodIcons(mAodIcons); - when(mDozeParameters.shouldControlScreenOff()).thenReturn(false); - mController.appearAodIcons(); - verify(mAodIcons).setTranslationY(0); - verify(mAodIcons).setAlpha(1.0f); - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationIconContainerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationIconContainerTest.kt index 9d97e5a5686e..15958ef4af8a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationIconContainerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationIconContainerTest.kt @@ -23,7 +23,6 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.StatusBarIconView.STATE_DOT import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN -import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor import junit.framework.Assert.assertEquals import junit.framework.Assert.assertFalse import junit.framework.Assert.assertTrue @@ -73,18 +72,6 @@ class NotificationIconContainerTest : SysuiTestCase() { } @Test - fun calculateWidthFor_fiveIcons_widthForFourIcons() { - mSetFlagsRule.disableFlags(NotificationIconContainerRefactor.FLAG_NAME) - iconContainer.setActualPaddingStart(10f) - iconContainer.setActualPaddingEnd(10f) - iconContainer.setIconSize(10) - assertEquals( - /* expected= */ iconContainer.calculateWidthFor(/* numIcons= */ 5f), - /* actual= */ 60f - ) - } - - @Test fun calculateIconXTranslations_shortShelfOneIcon_atCorrectXWithoutOverflowDot() { iconContainer.setActualPaddingStart(10f) iconContainer.setActualPaddingEnd(10f) @@ -213,19 +200,6 @@ class NotificationIconContainerTest : SysuiTestCase() { } @Test - fun shouldForceOverflow_appearingAboveSpeedBump_true() { - mSetFlagsRule.disableFlags(NotificationIconContainerRefactor.FLAG_NAME) - val forceOverflow = - iconContainer.shouldForceOverflow( - /* i= */ 1, - /* speedBumpIndex= */ 0, - /* iconAppearAmount= */ 1f, - /* maxVisibleIcons= */ 5 - ) - assertTrue(forceOverflow) - } - - @Test fun shouldForceOverflow_moreThanMaxVisible_true() { val forceOverflow = iconContainer.shouldForceOverflow( 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/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java index 58ad83546e01..bea027f0e98b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java @@ -39,7 +39,6 @@ import android.platform.test.annotations.EnableFlags; import android.provider.Settings; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper.RunWithLooper; -import android.view.LayoutInflater; import android.view.View; import androidx.test.filters.SmallTest; @@ -64,7 +63,6 @@ import com.android.systemui.statusbar.disableflags.DisableFlagsLogger; import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler; import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerStatusBarViewBinder; import com.android.systemui.statusbar.phone.HeadsUpAppearanceController; -import com.android.systemui.statusbar.phone.NotificationIconAreaController; import com.android.systemui.statusbar.phone.StatusBarHideIconsForBouncerManager; import com.android.systemui.statusbar.phone.StatusBarLocationPublisher; import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentComponent; @@ -95,7 +93,6 @@ import java.util.List; @RunWithLooper(setAsMainLooper = true) @SmallTest public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { - private NotificationIconAreaController mMockNotificationAreaController; private ShadeExpansionStateManager mShadeExpansionStateManager; private OngoingCallController mOngoingCallController; private SystemStatusAnimationScheduler mAnimationScheduler; @@ -931,13 +928,11 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { mCollapsedStatusBarViewModel = new FakeCollapsedStatusBarViewModel(); mCollapsedStatusBarViewBinder = new FakeCollapsedStatusBarViewBinder(); - setUpNotificationIconAreaController(); return new CollapsedStatusBarFragment( mStatusBarFragmentComponentFactory, mOngoingCallController, mAnimationScheduler, mLocationPublisher, - mMockNotificationAreaController, mShadeExpansionStateManager, mStatusBarIconController, mIconManagerFactory, @@ -970,14 +965,6 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { .thenReturn(mHeadsUpAppearanceController); } - private void setUpNotificationIconAreaController() { - mMockNotificationAreaController = mock(NotificationIconAreaController.class); - View notificationAreaInner = - LayoutInflater.from(mContext).inflate(R.layout.notification_icon_area, null); - when(mMockNotificationAreaController.getNotificationInnerAreaView()) - .thenReturn(notificationAreaInner); - } - /** * Configure mocks to return values consistent with the secure camera animating itself launched * over the keyguard. 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/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractorKosmos.kt index d5411ad77ce4..3680e651246b 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractorKosmos.kt @@ -19,6 +19,7 @@ package com.android.systemui.deviceentry.domain.interactor import android.content.mockedContext import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor +import com.android.systemui.communal.domain.interactor.communalSceneInteractor import com.android.systemui.keyguard.data.repository.deviceEntryFingerprintAuthRepository import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor @@ -42,5 +43,6 @@ val Kosmos.occludingAppDeviceEntryInteractor by activityStarter = activityStarter, powerInteractor = powerInteractor, keyguardTransitionInteractor = keyguardTransitionInteractor, + communalSceneInteractor = communalSceneInteractor, ) } 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/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/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt index 550ecb3ea7af..19b32bce77e7 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt @@ -20,7 +20,6 @@ import com.android.systemui.biometrics.authController import com.android.systemui.keyguard.domain.interactor.keyguardBlueprintInteractor import com.android.systemui.keyguard.domain.interactor.keyguardClockInteractor import com.android.systemui.kosmos.Kosmos -import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.scene.domain.interactor.sceneContainerOcclusionInteractor import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.unfold.domain.interactor.unfoldTransitionInteractor @@ -33,7 +32,6 @@ val Kosmos.lockscreenContentViewModel by authController = authController, touchHandling = keyguardTouchHandlingViewModel, shadeInteractor = shadeInteractor, - applicationScope = applicationCoroutineScope, unfoldTransitionInteractor = unfoldTransitionInteractor, occlusionInteractor = sceneContainerOcclusionInteractor, ) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneActionsViewModelKosmos.kt index 299b22ef963f..5d70ed6a634c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneActionsViewModelKosmos.kt @@ -18,13 +18,11 @@ package com.android.systemui.qs.ui.viewmodel import com.android.systemui.kosmos.Kosmos import com.android.systemui.shade.domain.interactor.shadeInteractor -import com.android.systemui.shade.ui.viewmodel.overlayShadeViewModel -val Kosmos.quickSettingsShadeSceneViewModel: QuickSettingsShadeSceneViewModel by +val Kosmos.quickSettingsShadeSceneActionsViewModel: QuickSettingsShadeSceneActionsViewModel by Kosmos.Fixture { - QuickSettingsShadeSceneViewModel( + QuickSettingsShadeSceneActionsViewModel( shadeInteractor = shadeInteractor, - overlayShadeViewModel = overlayShadeViewModel, quickSettingsContainerViewModel = quickSettingsContainerViewModel, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneContentViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneContentViewModelKosmos.kt new file mode 100644 index 000000000000..5ad5cb28e549 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneContentViewModelKosmos.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.android.systemui.qs.ui.viewmodel + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.shade.ui.viewmodel.overlayShadeViewModelFactory +import kotlinx.coroutines.ExperimentalCoroutinesApi + +val Kosmos.quickSettingsShadeSceneContentViewModel: QuickSettingsShadeSceneContentViewModel by + Kosmos.Fixture { + QuickSettingsShadeSceneContentViewModel( + overlayShadeViewModelFactory = overlayShadeViewModelFactory, + quickSettingsContainerViewModel = quickSettingsContainerViewModel, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/brightness/ui/viewmodel/BrightnessMirrorViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/brightness/ui/viewmodel/BrightnessMirrorViewModelKosmos.kt index 8fb370caee09..32a561474b4d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/brightness/ui/viewmodel/BrightnessMirrorViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/brightness/ui/viewmodel/BrightnessMirrorViewModelKosmos.kt @@ -18,15 +18,23 @@ package com.android.systemui.settings.brightness.ui.viewmodel import android.content.res.mainResources import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.settings.brightness.domain.interactor.brightnessMirrorShowingInteractor import com.android.systemui.settings.brightness.ui.viewModel.BrightnessMirrorViewModel import com.android.systemui.settings.brightnessSliderControllerFactory -val Kosmos.brightnessMirrorViewModel by - Kosmos.Fixture { - BrightnessMirrorViewModel( - brightnessMirrorShowingInteractor, - mainResources, - brightnessSliderControllerFactory, - ) +val Kosmos.brightnessMirrorViewModel by Fixture { + BrightnessMirrorViewModel( + brightnessMirrorShowingInteractor, + mainResources, + brightnessSliderControllerFactory, + ) +} + +val Kosmos.brightnessMirrorViewModelFactory by Fixture { + object : BrightnessMirrorViewModel.Factory { + override fun create(): BrightnessMirrorViewModel { + return brightnessMirrorViewModel + } } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/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/NotificationsShadeWindowModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeWindowModelKosmos.kt index cd4fab8d2970..6252d4498a5e 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeWindowModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeWindowModelKosmos.kt @@ -16,8 +16,14 @@ package com.android.systemui.shade.ui.viewmodel +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.kosmos.Kosmos val Kosmos.notificationShadeWindowModel: NotificationShadeWindowModel by - Kosmos.Fixture { NotificationShadeWindowModel(keyguardTransitionInteractor) } + Kosmos.Fixture { + NotificationShadeWindowModel( + keyguardTransitionInteractor, + keyguardInteractor, + ) + } 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/statusbar/phone/NotificationIconAreaControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneActionsViewModelKosmos.kt index d44e061a95ea..2387aa856fe6 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/NotificationIconAreaControllerKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneActionsViewModelKosmos.kt @@ -14,10 +14,16 @@ * limitations under the License. */ -package com.android.systemui.statusbar.phone +package com.android.systemui.shade.ui.viewmodel import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture -import com.android.systemui.util.mockito.mock +import com.android.systemui.qs.ui.adapter.qsSceneAdapter +import com.android.systemui.shade.domain.interactor.shadeInteractor -var Kosmos.notificationIconAreaController by Fixture { mock<NotificationIconAreaController>() } +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/viewbinder/NotificationListViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt index ee3216b2243d..bc1363ac3d5c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt @@ -29,22 +29,20 @@ import com.android.systemui.statusbar.notification.stack.displaySwitchNotificati import com.android.systemui.statusbar.notification.stack.ui.view.notificationStatsLogger import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationListViewModel import com.android.systemui.statusbar.notification.ui.viewbinder.headsUpNotificationViewBinder -import com.android.systemui.statusbar.phone.notificationIconAreaController import java.util.Optional val Kosmos.notificationListViewBinder by Fixture { NotificationListViewBinder( - viewModel = notificationListViewModel, backgroundDispatcher = testDispatcher, + hiderTracker = displaySwitchNotificationsHiderTracker, configuration = configurationState, falsingManager = falsingManager, hunBinder = headsUpNotificationViewBinder, - iconAreaController = notificationIconAreaController, loggerOptional = Optional.of(notificationStatsLogger), metricsLogger = metricsLogger, - hiderTracker = displaySwitchNotificationsHiderTracker, nicBinder = notificationIconContainerShelfViewBinder, notificationActivityStarter = { notificationActivityStarter }, silentHeaderController = silentHeaderController, + viewModel = notificationListViewModel, ) } 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/statusbar/phone/DozeServiceHostKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/DozeServiceHostKosmos.kt index 0b5a68ed2e57..4a6757db9481 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/DozeServiceHostKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/DozeServiceHostKosmos.kt @@ -53,7 +53,6 @@ val Kosmos.dozeServiceHost: DozeServiceHost by notificationShadeWindowController, notificationWakeUpCoordinator, authController, - notificationIconAreaController, shadeLockscreenInteractor, dozeInteractor, ) 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/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 2de3c5ef1967..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: [ - ":framework-minus-apex.ravenwood-base{hoststubgen_framework-minus-apex_stats.csv}", - ":framework-minus-apex.ravenwood-base{hoststubgen_framework-minus-apex_apis.csv}", - ":framework-minus-apex.ravenwood-base{hoststubgen_framework-minus-apex_keep_all.txt}", - ":framework-minus-apex.ravenwood-base{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/ravenwood/texts/ravenwood-framework-jarjar-rules.txt b/ravenwood/texts/ravenwood-framework-jarjar-rules.txt index 2eeb9042d732..afef56433d5b 100644 --- a/ravenwood/texts/ravenwood-framework-jarjar-rules.txt +++ b/ravenwood/texts/ravenwood-framework-jarjar-rules.txt @@ -1,2 +1 @@ -# To avoid VerifyError on nano proto files (b/324063814) -rule com.**.nano.** devicenano.@0 +# Applying jarjar on framework-minux-apex is too slow, so we don't use jarjar for now. b/313930116
\ No newline at end of file diff --git a/ravenwood/texts/ravenwood-framework-policies.txt b/ravenwood/texts/ravenwood-framework-policies.txt index 9d29a051d092..4012bdc5be62 100644 --- a/ravenwood/texts/ravenwood-framework-policies.txt +++ b/ravenwood/texts/ravenwood-framework-policies.txt @@ -9,6 +9,11 @@ class :feature_flags keepclass # Keep all sysprops generated code implementations class :sysprops keepclass +# To avoid VerifyError on nano proto files (b/324063814), we rename nano proto classes. +# Note: The "rename" directive must use shashes (/) as a package name separator. +rename com/.*/nano/ devicenano/ +rename android/.*/nano/ devicenano/ + # Exported to Mainline modules; cannot use annotations class com.android.internal.util.FastXmlSerializer keepclass class com.android.internal.util.FileRotator keepclass 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/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/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/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/audio/TEST_MAPPING b/services/core/java/com/android/server/audio/TEST_MAPPING index 2cea32af2396..f050090e69c1 100644 --- a/services/core/java/com/android/server/audio/TEST_MAPPING +++ b/services/core/java/com/android/server/audio/TEST_MAPPING @@ -10,6 +10,9 @@ "include-filter": "android.media.audio.cts.AudioFocusTest" }, { + "include-filter": "android.media.audio.cts.AudioPlaybackCaptureTest" + }, + { "include-filter": "android.media.audio.cts.SpatializerTest" } ] 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/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/DelayedMessageBuffer.java b/services/core/java/com/android/server/hdmi/DelayedMessageBuffer.java index 46b4f48165f9..44907457f649 100644 --- a/services/core/java/com/android/server/hdmi/DelayedMessageBuffer.java +++ b/services/core/java/com/android/server/hdmi/DelayedMessageBuffer.java @@ -17,8 +17,10 @@ package com.android.server.hdmi; import android.hardware.hdmi.HdmiDeviceInfo; + import java.util.ArrayList; import java.util.Iterator; +import java.util.List; /** * Buffer storage to keep incoming messages for later processing. Used to @@ -83,6 +85,16 @@ final class DelayedMessageBuffer { return false; } + List<HdmiCecMessage> getBufferedMessagesWithOpcode(int opcode) { + List<HdmiCecMessage> messages = new ArrayList<>(); + for (HdmiCecMessage message : mBuffer) { + if (message.getOpcode() == opcode) { + messages.add(message); + } + } + return messages; + } + void processAllMessages() { // Use the copied buffer. ArrayList<HdmiCecMessage> copiedBuffer = new ArrayList<>(mBuffer); diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java index 49888db98755..154710faf5d1 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java @@ -217,7 +217,9 @@ public class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice { resetSelectRequestBuffer(); launchDeviceDiscovery(); startQueuedActions(); - if (!mDelayedMessageBuffer.isBuffered(Constants.MESSAGE_ACTIVE_SOURCE)) { + List<HdmiCecMessage> bufferedActiveSource = mDelayedMessageBuffer + .getBufferedMessagesWithOpcode(Constants.MESSAGE_ACTIVE_SOURCE); + if (bufferedActiveSource.isEmpty()) { if (hasAction(RequestActiveSourceAction.class)) { Slog.i(TAG, "RequestActiveSourceAction is in progress. Restarting."); removeAction(RequestActiveSourceAction.class); @@ -236,7 +238,31 @@ public class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice { } } })); + } else { + addCecDeviceForBufferedActiveSource(bufferedActiveSource.get(0)); + } + } + + // Add a new CEC device with known information from the buffered <Active Source> message. This + // helps TvInputCallback#onInputAdded to be called such that the message can be processed and + // the TV to switch to the new active input. + @ServiceThreadOnly + private void addCecDeviceForBufferedActiveSource(HdmiCecMessage bufferedActiveSource) { + assertRunOnServiceThread(); + if (bufferedActiveSource == null) { + return; } + int source = bufferedActiveSource.getSource(); + int physicalAddress = HdmiUtils.twoBytesToInt(bufferedActiveSource.getParams()); + List<Integer> deviceTypes = HdmiUtils.getTypeFromAddress(source); + HdmiDeviceInfo newDevice = HdmiDeviceInfo.cecDeviceBuilder() + .setLogicalAddress(source) + .setPhysicalAddress(physicalAddress) + .setDisplayName(HdmiUtils.getDefaultDeviceName(source)) + .setDeviceType(deviceTypes.get(0)) + .setVendorId(Constants.VENDOR_ID_UNKNOWN) + .build(); + mService.getHdmiCecNetwork().addCecDevice(newDevice); } @ServiceThreadOnly 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/DefaultImeVisibilityApplier.java b/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java index 5c939bc14ec7..600cf7f06981 100644 --- a/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java +++ b/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java @@ -202,7 +202,7 @@ final class DefaultImeVisibilityApplier { break; case STATE_HIDE_IME_EXPLICIT: if (Flags.refactorInsetsController()) { - setImeVisibilityOnFocusedWindowClient(false, userId, statsToken); + mService.setImeVisibilityOnFocusedWindowClient(false, userData, statsToken); } else { mService.hideCurrentInputLocked(windowToken, statsToken, 0 /* flags */, null /* resultReceiver */, reason, userId); @@ -210,7 +210,7 @@ final class DefaultImeVisibilityApplier { break; case STATE_HIDE_IME_NOT_ALWAYS: if (Flags.refactorInsetsController()) { - setImeVisibilityOnFocusedWindowClient(false, userId, statsToken); + mService.setImeVisibilityOnFocusedWindowClient(false, userData, statsToken); } else { mService.hideCurrentInputLocked(windowToken, statsToken, InputMethodManager.HIDE_NOT_ALWAYS, null /* resultReceiver */, reason, @@ -221,7 +221,7 @@ final class DefaultImeVisibilityApplier { if (Flags.refactorInsetsController()) { // This can be triggered by IMMS#startInputOrWindowGainedFocus. We need to // set the requestedVisibleTypes in InsetsController first, before applying it. - setImeVisibilityOnFocusedWindowClient(true, userId, statsToken); + mService.setImeVisibilityOnFocusedWindowClient(true, userData, statsToken); } else { mService.showCurrentInputLocked(windowToken, statsToken, InputMethodManager.SHOW_IMPLICIT, MotionEvent.TOOL_TYPE_UNKNOWN, @@ -276,19 +276,4 @@ final class DefaultImeVisibilityApplier { } return false; } - - @GuardedBy("ImfLock.class") - private void setImeVisibilityOnFocusedWindowClient(boolean visibility, @UserIdInt int userId, - @NonNull ImeTracker.Token statsToken) { - final var userData = mService.getUserData(userId); - if (userData.mImeBindingState != null - && userData.mImeBindingState.mFocusedWindowClient != null - && userData.mImeBindingState.mFocusedWindowClient.mClient != null) { - userData.mImeBindingState.mFocusedWindowClient.mClient.setImeVisibility(visibility, - statsToken); - } else { - ImeTracker.forLogging().onFailed(statsToken, - ImeTracker.PHASE_SERVER_SET_VISIBILITY_ON_FOCUSED_WINDOW); - } - } } diff --git a/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java b/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java index a380bc1ca171..0047ec20d691 100644 --- a/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java +++ b/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java @@ -112,7 +112,8 @@ final class IInputMethodInvoker { } @AnyThread - void initializeInternal(IBinder token, IInputMethodPrivilegedOperations privilegedOperations, + void initializeInternal(@NonNull IBinder token, + @NonNull IInputMethodPrivilegedOperations privilegedOperations, @InputMethodNavButtonFlags int navigationBarFlags) { final IInputMethod.InitParams params = new IInputMethod.InitParams(); params.token = token; 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 dba04656e48f..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); /** @@ -238,6 +241,33 @@ public abstract class InputMethodManagerInternal { public abstract void removeImeSurface(int displayId); /** + * Called when a non-IME-focusable overlay window being the IME layering target (e.g. a + * window with {@link android.view.WindowManager.LayoutParams#FLAG_NOT_FOCUSABLE} and + * {@link android.view.WindowManager.LayoutParams#FLAG_ALT_FOCUSABLE_IM} flags) + * has changed its window visibility. + * + * @param hasVisibleOverlay whether such an overlay window exists or not + * @param displayId the display ID where the overlay window exists + */ + public abstract void setHasVisibleImeLayeringOverlay(boolean hasVisibleOverlay, int displayId); + + /** + * Called when the visibility of IME input target window has changed. + * + * @param imeInputTarget the window token of the IME input target window + * @param visibleAndNotRemoved {@code true} when the new window is made visible by + * {@code imeInputTarget} and the IME input target window has not + * been removed. The new window is considered to be visible when + * switching to the new visible IME input target window and + * starting input, or the existing input target becomes visible. + * In contrast, {@code false} when closing the input target, or the + * existing input target becomes invisible + * @param displayId the display for which to update the IME window status + */ + public abstract void onImeInputTargetVisibilityChanged(@NonNull IBinder imeInputTarget, + boolean visibleAndNotRemoved, int displayId); + + /** * Updates the IME visibility, back disposition and show IME picker status for SystemUI. * TODO(b/189923292): Making SystemUI to be true IME icon controller vs. presenter that * controlled by IMMS. @@ -349,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; } @@ -389,6 +419,16 @@ public abstract class InputMethodManagerInternal { public void removeImeSurface(int displayId) { } + @Override + public void setHasVisibleImeLayeringOverlay(boolean hasVisibleOverlay, + int displayId) { + } + + @Override + public void onImeInputTargetVisibilityChanged(@NonNull IBinder imeInputTarget, + boolean visibleAndNotRemoved, int displayId) { + } + @ImfLockFree @Override public void updateImeWindowStatus(boolean disableImeIcon, int displayId) { diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 1c0a08288ff4..8afbd56728e4 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -44,16 +44,17 @@ import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.INVALID_DISPLAY; import static android.view.WindowManager.DISPLAY_IME_POLICY_HIDE; import static android.view.WindowManager.DISPLAY_IME_POLICY_LOCAL; -import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING; import static android.view.inputmethod.ConnectionlessHandwritingCallback.CONNECTIONLESS_HANDWRITING_ERROR_OTHER; import static android.view.inputmethod.ConnectionlessHandwritingCallback.CONNECTIONLESS_HANDWRITING_ERROR_UNSUPPORTED; 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; @@ -82,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; @@ -188,7 +188,6 @@ import com.android.server.inputmethod.InputMethodSubtypeSwitchingController.ImeS import com.android.server.pm.UserManagerInternal; import com.android.server.statusbar.StatusBarManagerInternal; import com.android.server.utils.PriorityDump; -import com.android.server.wm.ImeTargetChangeListener; import com.android.server.wm.WindowManagerInternal; import java.io.FileDescriptor; @@ -275,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"; @@ -305,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 @@ -358,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; } /** @@ -371,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; } /** @@ -387,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; @@ -398,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 @@ -568,7 +553,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @GuardedBy("ImfLock.class") @Nullable IInputMethodInvoker getCurMethodLocked() { - return getInputMethodBindingController(mCurrentUserId).getCurMethod(); + return getInputMethodBindingController(mCurrentImeUserId).getCurMethod(); } /** @@ -614,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); } } @@ -677,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 { @@ -720,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); } @@ -799,63 +784,70 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. final int userId = getChangingUserId(); final var userData = getUserData(userId); - // Instantiating InputMethodInfo requires disk I/O. - // Do them before acquiring the lock to minimize the chances of ANR (b/340221861). userData.mRawInputMethodMap.set(queryRawInputMethodServiceMap(mContext, userId)); - synchronized (ImfLock.class) { - final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); + final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); - InputMethodInfo curIm = null; - String curInputMethodId = settings.getSelectedInputMethod(); - final List<InputMethodInfo> methodList = settings.getMethodList(); + InputMethodInfo curIm = null; + String curInputMethodId = settings.getSelectedInputMethod(); + final List<InputMethodInfo> methodList = settings.getMethodList(); - final ArrayList<String> imesToClearAdditionalSubtypes = new ArrayList<>(); - final int numImes = methodList.size(); - for (int i = 0; i < numImes; i++) { - InputMethodInfo imi = methodList.get(i); - final String imiId = imi.getId(); - if (imiId.equals(curInputMethodId)) { - curIm = imi; - } - if (mDataClearedPackages.contains(imi.getPackageName())) { - imesToClearAdditionalSubtypes.add(imiId); - } - int change = isPackageDisappearing(imi.getPackageName()); - if (change == PACKAGE_PERMANENT_CHANGE) { - Slog.i(TAG, "Input method uninstalled, disabling: " + imi.getComponent()); - setInputMethodEnabledLocked(imi.getId(), false, userId); - } else if (change == PACKAGE_UPDATING) { - Slog.i(TAG, "Input method reinstalling, clearing additional subtypes: " - + imi.getComponent()); - imesToClearAdditionalSubtypes.add(imiId); - } + final ArrayList<String> imesToClearAdditionalSubtypes = new ArrayList<>(); + final ArrayList<String> imesToBeDisabled = new ArrayList<>(); + final int numImes = methodList.size(); + for (int i = 0; i < numImes; i++) { + InputMethodInfo imi = methodList.get(i); + final String imiId = imi.getId(); + if (imiId.equals(curInputMethodId)) { + curIm = imi; } - - // Clear additional subtypes as a batch operation. - final var additionalSubtypeMap = AdditionalSubtypeMapRepository.get(userId); - final AdditionalSubtypeMap newAdditionalSubtypeMap = - additionalSubtypeMap.cloneWithRemoveOrSelf(imesToClearAdditionalSubtypes); - final boolean additionalSubtypeChanged = - (newAdditionalSubtypeMap != additionalSubtypeMap); - if (additionalSubtypeChanged) { - AdditionalSubtypeMapRepository.putAndSave(userId, newAdditionalSubtypeMap, - settings.getMethodMap()); + if (mDataClearedPackages.contains(imi.getPackageName())) { + imesToClearAdditionalSubtypes.add(imiId); + } + int change = isPackageDisappearing(imi.getPackageName()); + if (change == PACKAGE_PERMANENT_CHANGE) { + Slog.i(TAG, "Input method uninstalled, disabling: " + imi.getComponent()); + imesToBeDisabled.add(imi.getId()); + } else if (change == PACKAGE_UPDATING) { + Slog.i(TAG, "Input method reinstalling, clearing additional subtypes: " + + imi.getComponent()); + imesToClearAdditionalSubtypes.add(imiId); } + } + + // Clear additional subtypes as a batch operation. + final var additionalSubtypeMap = AdditionalSubtypeMapRepository.get(userId); + final AdditionalSubtypeMap newAdditionalSubtypeMap = + additionalSubtypeMap.cloneWithRemoveOrSelf(imesToClearAdditionalSubtypes); + final boolean additionalSubtypeChanged = + (newAdditionalSubtypeMap != additionalSubtypeMap); + if (additionalSubtypeChanged) { + AdditionalSubtypeMapRepository.putAndSave(userId, newAdditionalSubtypeMap, + settings.getMethodMap()); + } - final var newMethodMap = userData.mRawInputMethodMap.get().toInputMethodMap( - newAdditionalSubtypeMap, - DirectBootAwareness.AUTO, - mUserManagerInternal.isUserUnlockingOrUnlocked(userId)); + final var newMethodMap = userData.mRawInputMethodMap.get().toInputMethodMap( + newAdditionalSubtypeMap, + DirectBootAwareness.AUTO, + userData.mIsUnlockingOrUnlocked.get()); - if (InputMethodMap.areSame(settings.getMethodMap(), newMethodMap)) { - // No update in the actual IME map. + final boolean noUpdate = InputMethodMap.areSame(settings.getMethodMap(), newMethodMap); + if (noUpdate && imesToBeDisabled.isEmpty()) { + return; + } + + // Here we start remaining tasks that need to be done with the lock (b/340221861). + synchronized (ImfLock.class) { + final int numImesToBeDisabled = imesToBeDisabled.size(); + for (int i = 0; i < numImesToBeDisabled; ++i) { + setInputMethodEnabledLocked(imesToBeDisabled.get(i), false /* enabled */, + userId); + } + if (noUpdate) { return; } - - final InputMethodSettings newSettings = - InputMethodSettings.create(newMethodMap, userId); - InputMethodSettingsRepository.put(userId, newSettings); + InputMethodSettingsRepository.put(userId, + InputMethodSettings.create(newMethodMap, userId)); postInputMethodSettingUpdatedLocked(false /* resetDefaultEnabledIme */, userId); boolean changed = false; @@ -964,37 +956,6 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. InputMethodDrawsNavBarResourceMonitor.registerCallback(context, mService.mIoHandler, mService::onUpdateResourceOverlay); - // Also hook up ImeTargetChangeListener. - // TODO(b/356876005): Merge this into InputMethodManagerInternal. - final var windowManagerInternal = mService.mWindowManagerInternal; - windowManagerInternal.setInputMethodTargetChangeListener(new ImeTargetChangeListener() { - @Override - public void onImeTargetOverlayVisibilityChanged(@NonNull IBinder overlayWindowToken, - @WindowManager.LayoutParams.WindowType int windowType, boolean visible, - boolean removed, int displayId) { - // Ignoring the starting window since it's ok to cover the IME target - // window in temporary without affecting the IME visibility. - final boolean hasOverlay = visible && !removed - && windowType != TYPE_APPLICATION_STARTING; - synchronized (ImfLock.class) { - final var userId = mService.resolveImeUserIdFromDisplayIdLocked(displayId); - mService.getUserData(userId).mVisibilityStateComputer - .setHasVisibleImeLayeringOverlay(hasOverlay); - } - } - - @Override - public void onImeInputTargetVisibilityChanged(IBinder imeInputTarget, - boolean visibleRequested, boolean removed, int displayId) { - final boolean visibleAndNotRemoved = visibleRequested && !removed; - synchronized (ImfLock.class) { - final var userId = mService.resolveImeUserIdFromDisplayIdLocked(displayId); - mService.getUserData(userId).mVisibilityStateComputer - .onImeInputTargetVisibilityChanged(imeInputTarget, - visibleAndNotRemoved); - } - } - }); // Also schedule user init tasks onto an I/O thread. initializeUsersAsync(mService.mUserManagerInternal.getUserIds()); } @@ -1096,13 +1057,16 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. public void onUserUnlocking(@NonNull TargetUser user) { // Called on ActivityManager thread. Do not block the calling thread. 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, + userUnlocked); + final var newSettings = InputMethodSettings.create(methodMap, userId); + InputMethodSettingsRepository.put(userId, newSettings); mService.mIoHandler.post(() -> { - final var userData = mService.getUserData(userId); - final var methodMap = userData.mRawInputMethodMap.get().toInputMethodMap( - AdditionalSubtypeMapRepository.get(userId), DirectBootAwareness.AUTO, true); - final var newSettings = InputMethodSettings.create(methodMap, userId); - InputMethodSettingsRepository.put(userId, newSettings); synchronized (ImfLock.class) { if (!mService.mSystemReady) { return; @@ -1146,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); @@ -1168,16 +1135,19 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. public void onUserStopped(@NonNull TargetUser user) { final int userId = user.getUserIdentifier(); // Called on ActivityManager thread. + + // Following operations should be trivial and fast enough, so do not dispatch them to + // the IO thread. SecureSettingsWrapper.onUserStopped(userId); - mService.mIoHandler.post(() -> { - final var userData = mService.getUserData(userId); - final var additionalSubtypeMap = AdditionalSubtypeMapRepository.get(userId); - final var rawMethodMap = userData.mRawInputMethodMap.get(); - final var methodMap = rawMethodMap.toInputMethodMap(additionalSubtypeMap, - DirectBootAwareness.AUTO, false /* userUnlocked */); - InputMethodSettingsRepository.put(userId, - InputMethodSettings.create(methodMap, userId)); - }); + final var userData = mService.getUserData(userId); + 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, + InputMethodSettings.create(methodMap, userId)); } } @@ -1227,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); @@ -1243,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); @@ -1311,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 @@ -1326,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); @@ -1351,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); @@ -1377,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), @@ -1440,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) { @@ -1516,27 +1496,6 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. }); } - /** - * Returns true iff the caller is identified to be the current input method with the token. - * - * @param token the window token given to the input method when it was started - * @param userData {@link UserData} of the calling IME process - * @return true if and only if non-null valid token is specified - */ - @GuardedBy("ImfLock.class") - private boolean calledWithValidTokenLocked(@NonNull IBinder token, @NonNull UserData userData) { - if (token == null) { - throw new InvalidParameterException("token must not be null."); - } - final var bindingController = userData.mBindingController; - if (token != bindingController.getCurToken()) { - Slog.e(TAG, "Ignoring " + Debug.getCaller() + " due to an invalid token." - + " uid:" + Binder.getCallingUid() + " token:" + token); - return false; - } - return true; - } - @BinderThread @Nullable @Override @@ -1647,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 @@ -1678,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()); @@ -1853,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); @@ -2061,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; } @@ -2593,45 +2552,36 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } @BinderThread - private void updateStatusIcon(@NonNull IBinder token, String packageName, - @DrawableRes int iconId, @NonNull UserData userData) { + @GuardedBy("ImfLock.class") + private void updateStatusIconLocked(String packageName, @DrawableRes int iconId, + @NonNull UserData userData) { final int userId = userData.mUserId; - synchronized (ImfLock.class) { - // 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) { - return; - } - if (!calledWithValidTokenLocked(token, userData)) { - return; - } - final long ident = Binder.clearCallingIdentity(); + // 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 != mCurrentImeUserId) { + return; + } + if (iconId == 0) { + if (DEBUG) Slog.d(TAG, "hide the small icon for the input method"); + hideStatusBarIconLocked(userId); + } else if (packageName != null) { + if (DEBUG) Slog.d(TAG, "show a small icon for the input method"); + final PackageManager userAwarePackageManager = + getPackageManagerForUser(mContext, userId); + ApplicationInfo applicationInfo = null; try { - if (iconId == 0) { - if (DEBUG) Slog.d(TAG, "hide the small icon for the input method"); - hideStatusBarIconLocked(userId); - } else if (packageName != null) { - if (DEBUG) Slog.d(TAG, "show a small icon for the input method"); - final PackageManager userAwarePackageManager = - getPackageManagerForUser(mContext, userId); - ApplicationInfo applicationInfo = null; - try { - applicationInfo = userAwarePackageManager.getApplicationInfo(packageName, - PackageManager.ApplicationInfoFlags.of(0)); - } catch (PackageManager.NameNotFoundException e) { - } - final CharSequence contentDescription = applicationInfo != null - ? userAwarePackageManager.getApplicationLabel(applicationInfo) - : null; - if (mStatusBarManagerInternal != null) { - mStatusBarManagerInternal.setIcon(mSlotIme, packageName, iconId, 0, - contentDescription != null - ? contentDescription.toString() : null); - mStatusBarManagerInternal.setIconVisibility(mSlotIme, true); - } - } - } finally { - Binder.restoreCallingIdentity(ident); + applicationInfo = userAwarePackageManager.getApplicationInfo(packageName, + PackageManager.ApplicationInfoFlags.of(0)); + } catch (PackageManager.NameNotFoundException e) { + } + final CharSequence contentDescription = applicationInfo != null + ? userAwarePackageManager.getApplicationLabel(applicationInfo) + : null; + if (mStatusBarManagerInternal != null) { + mStatusBarManagerInternal.setIcon(mSlotIme, packageName, iconId, 0, + contentDescription != null + ? contentDescription.toString() : null); + mStatusBarManagerInternal.setIconVisibility(mSlotIme, true); } } } @@ -2640,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) { @@ -2672,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) { @@ -2688,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 @@ -2704,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()); @@ -2772,29 +2720,25 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } @BinderThread + @GuardedBy("ImfLock.class") @SuppressWarnings("deprecation") - private void setImeWindowStatus(@NonNull IBinder token, int vis, int backDisposition, + private void setImeWindowStatusLocked(int vis, int backDisposition, @NonNull UserData userData) { final int topFocusedDisplayId = mWindowManagerInternal.getTopFocusedDisplayId(); final int userId = userData.mUserId; - synchronized (ImfLock.class) { - if (!calledWithValidTokenLocked(token, userData)) { - return; - } - final var bindingController = userData.mBindingController; - // Skip update IME status when current token display is not same as focused display. - // Note that we still need to update IME status when focusing external display - // that does not support system decoration and fallback to show IME on default - // display since it is intentional behavior. - final int tokenDisplayId = bindingController.getCurTokenDisplayId(); - if (tokenDisplayId != topFocusedDisplayId && tokenDisplayId != FALLBACK_DISPLAY_ID) { - return; - } - bindingController.setImeWindowVis(vis); - bindingController.setBackDisposition(backDisposition); - updateSystemUiLocked(vis, backDisposition, userId); + final var bindingController = userData.mBindingController; + // Skip update IME status when current token display is not same as focused display. + // Note that we still need to update IME status when focusing external display + // that does not support system decoration and fallback to show IME on default + // display since it is intentional behavior. + final int tokenDisplayId = bindingController.getCurTokenDisplayId(); + if (tokenDisplayId != topFocusedDisplayId && tokenDisplayId != FALLBACK_DISPLAY_ID) { + return; } + bindingController.setImeWindowVis(vis); + bindingController.setBackDisposition(backDisposition); + updateSystemUiLocked(vis, backDisposition, userId); final boolean dismissImeOnBackKeyPressed; switch (backDisposition) { @@ -2813,19 +2757,14 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } @BinderThread - private void reportStartInput(@NonNull IBinder token, IBinder startInputToken, - @NonNull UserData userData) { - synchronized (ImfLock.class) { - if (!calledWithValidTokenLocked(token, userData)) { - return; - } - final IBinder targetWindow = mImeTargetWindowMap.get(startInputToken); - if (targetWindow != null) { - mWindowManagerInternal.updateInputMethodTargetWindow(targetWindow); - } - final var visibilityStateComputer = userData.mVisibilityStateComputer; - visibilityStateComputer.setLastImeTargetWindow(targetWindow); + @GuardedBy("ImfLock.class") + private void reportStartInputLocked(IBinder startInputToken, @NonNull UserData userData) { + final IBinder targetWindow = mImeTargetWindowMap.get(startInputToken); + if (targetWindow != null) { + mWindowManagerInternal.updateInputMethodTargetWindow(targetWindow); } + final var visibilityStateComputer = userData.mVisibilityStateComputer; + visibilityStateComputer.setLastImeTargetWindow(targetWindow); } @GuardedBy("ImfLock.class") @@ -2851,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); @@ -2864,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 @@ -2872,8 +2811,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. final Boolean windowPerceptible = focusedWindowToken != null ? mFocusedWindowPerceptible.get(focusedWindowToken) : null; - // TODO: Move this clearing calling identity block to setImeWindowStatus after making sure - // all updateSystemUi happens on system privilege. + // TODO: Move this clearing calling identity block to setImeWindowStatusLocked after making + // sure all updateSystemUi happens on system privilege. final long ident = Binder.clearCallingIdentity(); try { if (windowPerceptible != null && !windowPerceptible) { @@ -2886,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 @@ -2908,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); } } @@ -2976,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); @@ -2999,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) { @@ -3026,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(), @@ -3071,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(). @@ -3147,11 +3095,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. if (Flags.refactorInsetsController()) { final var visibilityStateComputer = userData.mVisibilityStateComputer; boolean wasVisible = visibilityStateComputer.isInputShown(); - if (userData.mImeBindingState != null - && userData.mImeBindingState.mFocusedWindowClient != null - && userData.mImeBindingState.mFocusedWindowClient.mClient != null) { - userData.mImeBindingState.mFocusedWindowClient.mClient - .setImeVisibility(true, statsToken); + if (setImeVisibilityOnFocusedWindowClient(false, userData, statsToken)) { if (resultReceiver != null) { resultReceiver.send( wasVisible ? InputMethodManager.RESULT_UNCHANGED_SHOWN @@ -3600,13 +3544,9 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.hideSoftInput"); if (DEBUG) Slog.v(TAG, "Client requesting input be hidden"); if (Flags.refactorInsetsController()) { - if (userData.mImeBindingState != null - && userData.mImeBindingState.mFocusedWindowClient != null - && userData.mImeBindingState.mFocusedWindowClient.mClient != null) { - boolean wasVisible = visibilityStateComputer.isInputShown(); - // TODO add windowToken to interface - userData.mImeBindingState.mFocusedWindowClient.mClient - .setImeVisibility(false, statsToken); + boolean wasVisible = visibilityStateComputer.isInputShown(); + // TODO add windowToken to interface + if (setImeVisibilityOnFocusedWindowClient(false, userData, statsToken)) { if (resultReceiver != null) { resultReceiver.send(wasVisible ? InputMethodManager.RESULT_HIDDEN : InputMethodManager.RESULT_UNCHANGED_HIDDEN, null); @@ -3776,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); @@ -3823,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. @@ -4085,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(); } @@ -4094,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. */ @@ -4106,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()); } } @@ -4131,35 +4072,24 @@ 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); } - @BinderThread - private void onImeSwitchButtonClickFromClient(@NonNull IBinder token, int displayId, - @NonNull UserData userData) { - synchronized (ImfLock.class) { - if (!calledWithValidTokenLocked(token, userData)) { - return; - } - onImeSwitchButtonClickLocked(token, displayId, userData); - } - } - @IInputMethodManagerImpl.PermissionVerified(allOf = { Manifest.permission.INTERACT_ACROSS_USERS_FULL, Manifest.permission.WRITE_SECURE_SETTINGS}) @@ -4168,13 +4098,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. synchronized (ImfLock.class) { final int userId = resolveImeUserIdFromDisplayIdLocked(displayId); final var userData = getUserData(userId); - final var bindingController = userData.mBindingController; - final var curToken = bindingController.getCurToken(); - if (curToken == null) { - return; - } - onImeSwitchButtonClickLocked(curToken, displayId, userData); + onImeSwitchButtonClickLocked(displayId, userData); } } @@ -4182,17 +4107,16 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. * Handles a click on the IME switch button. Depending on the number of enabled IME subtypes, * this will either switch to the next IME/subtype, or show the input method picker dialog. * - * @param token The token identifying the input method that triggered this. * @param displayId The ID of the display where the input method picker dialog should be shown. * @param userData The data of the user for which to switch IMEs or show the picker dialog. */ + @BinderThread @GuardedBy("ImfLock.class") - private void onImeSwitchButtonClickLocked(@NonNull IBinder token, int displayId, - @NonNull UserData userData) { + private void onImeSwitchButtonClickLocked(int displayId, @NonNull UserData userData) { final int userId = userData.mUserId; final var settings = InputMethodSettingsRepository.get(userId); if (hasMultipleSubtypesForSwitcher(true /* nonAuxOnly */, settings)) { - switchToNextInputMethodLocked(token, false /* onlyCurrentIme */, userData); + switchToNextInputMethodLocked(false /* onlyCurrentIme */, userData); } else { showInputMethodPickerFromSystem( InputMethodManager.SHOW_IM_PICKER_MODE_INCLUDE_AUXILIARY_SUBTYPES, displayId); @@ -4206,143 +4130,102 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } @BinderThread - private void setInputMethod(@NonNull IBinder token, String id, @NonNull UserData userData) { + @GuardedBy("ImfLock.class") + private void setInputMethodAndSubtypeLocked(String id, @Nullable InputMethodSubtype subtype, + @NonNull UserData userData) { final int callingUid = Binder.getCallingUid(); final int userId = userData.mUserId; - synchronized (ImfLock.class) { - if (!calledWithValidTokenLocked(token, userData)) { - return; - } - final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); - final InputMethodInfo imi = settings.getMethodMap().get(id); - if (imi == null || !canCallerAccessInputMethod( - imi.getPackageName(), callingUid, userId, settings)) { - throw getExceptionForUnknownImeId(id); - } - setInputMethodWithSubtypeIdLocked(token, id, NOT_A_SUBTYPE_ID, userId); + final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); + final InputMethodInfo imi = settings.getMethodMap().get(id); + if (imi == null || !canCallerAccessInputMethod( + imi.getPackageName(), callingUid, userId, settings)) { + throw getExceptionForUnknownImeId(id); } + final int subtypeIndex = subtype != null + ? SubtypeUtils.getSubtypeIndexFromHashCode(imi, subtype.hashCode()) + : NOT_A_SUBTYPE_INDEX; + setInputMethodWithSubtypeIndexLocked(id, subtypeIndex, userId); } @BinderThread - private void setInputMethodAndSubtype(@NonNull IBinder token, String id, - InputMethodSubtype subtype, @NonNull UserData userData) { - final int callingUid = Binder.getCallingUid(); + @GuardedBy("ImfLock.class") + private boolean switchToPreviousInputMethodLocked(@NonNull UserData userData) { final int userId = userData.mUserId; - synchronized (ImfLock.class) { - if (!calledWithValidTokenLocked(token, userData)) { - return; - } - final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); - final InputMethodInfo imi = settings.getMethodMap().get(id); - if (imi == null || !canCallerAccessInputMethod( - imi.getPackageName(), callingUid, userId, settings)) { - throw getExceptionForUnknownImeId(id); - } - if (subtype != null) { - setInputMethodWithSubtypeIdLocked(token, id, - SubtypeUtils.getSubtypeIdFromHashCode(imi, subtype.hashCode()), userId); - } else { - setInputMethod(token, id, userData); - } + final var bindingController = userData.mBindingController; + final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); + final Pair<String, String> lastIme = settings.getLastInputMethodAndSubtype(); + final InputMethodInfo lastImi; + if (lastIme != null) { + lastImi = settings.getMethodMap().get(lastIme.first); + } else { + lastImi = null; } - } - - @BinderThread - private boolean switchToPreviousInputMethod(@NonNull IBinder token, - @NonNull UserData userData) { - final int userId = userData.mUserId; - synchronized (ImfLock.class) { - if (!calledWithValidTokenLocked(token, userData)) { - return false; - } - final var bindingController = userData.mBindingController; - final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); - final Pair<String, String> lastIme = settings.getLastInputMethodAndSubtype(); - final InputMethodInfo lastImi; - if (lastIme != null) { - lastImi = settings.getMethodMap().get(lastIme.first); + final var currentSubtype = bindingController.getCurrentSubtype(); + String targetLastImiId = null; + 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_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; + subtypeIndex = SubtypeUtils.getSubtypeIndexFromHashCode(lastImi, lastSubtypeHash); + } + } + + if (TextUtils.isEmpty(targetLastImiId) + && !InputMethodUtils.canAddToLastInputMethod(currentSubtype)) { + // This is a safety net. If the currentSubtype can't be added to the history + // 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(); + final int enabledCount = enabled.size(); + final String locale; + if (currentSubtype != null + && !TextUtils.isEmpty(currentSubtype.getLocale())) { + locale = currentSubtype.getLocale(); } else { - lastImi = null; - } - final var currentSubtype = bindingController.getCurrentSubtype(); - String targetLastImiId = null; - int subtypeId = NOT_A_SUBTYPE_ID; - 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 - : 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); - } - } - - if (TextUtils.isEmpty(targetLastImiId) - && !InputMethodUtils.canAddToLastInputMethod(currentSubtype)) { - // This is a safety net. If the currentSubtype can't be added to the history - // 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; - } - } + 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; } } } } - - if (!TextUtils.isEmpty(targetLastImiId)) { - if (DEBUG) { - Slog.d(TAG, "Switch to: " + lastImi.getId() + ", " + lastIme.second - + ", from: " + bindingController.getSelectedMethodId() + ", " - + subtypeId); - } - setInputMethodWithSubtypeIdLocked(token, targetLastImiId, subtypeId, userId); - return true; - } else { - return false; - } } - } - @BinderThread - private boolean switchToNextInputMethod(@NonNull IBinder token, boolean onlyCurrentIme, - @NonNull UserData userData) { - synchronized (ImfLock.class) { - if (!calledWithValidTokenLocked(token, userData)) { - return false; + if (!TextUtils.isEmpty(targetLastImiId)) { + if (DEBUG) { + Slog.d(TAG, "Switch to: " + lastImi.getId() + ", " + lastIme.second + + ", from: " + bindingController.getSelectedMethodId() + ", " + + subtypeIndex); } - return switchToNextInputMethodLocked(token, onlyCurrentIme, userData); + setInputMethodWithSubtypeIndexLocked(targetLastImiId, subtypeIndex, userId); + return true; + } else { + return false; } } + @BinderThread @GuardedBy("ImfLock.class") - private boolean switchToNextInputMethodLocked(@Nullable IBinder token, boolean onlyCurrentIme, + private boolean switchToNextInputMethodLocked(boolean onlyCurrentIme, @NonNull UserData userData) { final var bindingController = userData.mBindingController; final var currentImi = bindingController.getSelectedMethod(); @@ -4353,26 +4236,21 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. if (nextSubtype == null) { return false; } - setInputMethodWithSubtypeIdLocked(token, nextSubtype.mImi.getId(), - nextSubtype.mSubtypeId, userData.mUserId); + setInputMethodWithSubtypeIndexLocked(nextSubtype.mImi.getId(), nextSubtype.mSubtypeIndex, + userData.mUserId); return true; } @BinderThread - private boolean shouldOfferSwitchingToNextInputMethod(@NonNull IBinder token, - @NonNull UserData userData) { - synchronized (ImfLock.class) { - if (!calledWithValidTokenLocked(token, userData)) { - return false; - } - final var bindingController = userData.mBindingController; - final var currentImi = bindingController.getSelectedMethod(); - final ImeSubtypeListItem nextSubtype = userData.mSwitchingController - .getNextInputMethodLocked(false /* onlyCurrentIme */, currentImi, - bindingController.getCurrentSubtype(), - MODE_AUTO, true /* forward */); - return nextSubtype != null; - } + @GuardedBy("ImfLock.class") + private boolean shouldOfferSwitchingToNextInputMethodLocked(@NonNull UserData userData) { + final var bindingController = userData.mBindingController; + final var currentImi = bindingController.getSelectedMethod(); + final ImeSubtypeListItem nextSubtype = userData.mSwitchingController + .getNextInputMethodLocked(false /* onlyCurrentIme */, currentImi, + bindingController.getCurrentSubtype(), + MODE_AUTO, true /* forward */); + return nextSubtype != null; } @Override @@ -4424,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); @@ -4575,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(); @@ -4764,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; @@ -4791,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()); } @@ -4801,95 +4679,61 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } @BinderThread - private void notifyUserAction(@NonNull IBinder token, @NonNull UserData userData) { + @GuardedBy("ImfLock.class") + private void notifyUserActionLocked(@NonNull UserData userData) { if (DEBUG) { Slog.d(TAG, "Got the notification of a user action."); } - synchronized (ImfLock.class) { - final var bindingController = userData.mBindingController; - if (bindingController.getCurToken() != token) { - if (DEBUG) { - Slog.d(TAG, "Ignoring the user action notification from IMEs that are no longer" - + " active."); - } - return; - } - final InputMethodInfo imi = bindingController.getSelectedMethod(); - if (imi != null) { - userData.mSwitchingController.onUserActionLocked(imi, - bindingController.getCurrentSubtype()); - } + final var bindingController = userData.mBindingController; + final InputMethodInfo imi = bindingController.getSelectedMethod(); + if (imi != null) { + userData.mSwitchingController.onUserActionLocked(imi, + bindingController.getCurrentSubtype()); } } @BinderThread - private void applyImeVisibility(IBinder token, IBinder windowToken, boolean setVisible, + @GuardedBy("ImfLock.class") + private void applyImeVisibilityLocked(IBinder windowToken, boolean setVisible, @NonNull ImeTracker.Token statsToken, @NonNull UserData userData) { try { - Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.applyImeVisibility"); + Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.applyImeVisibilityLocked"); final int userId = userData.mUserId; - synchronized (ImfLock.class) { - if (!calledWithValidTokenLocked(token, userData)) { - ImeTracker.forLogging().onFailed(statsToken, - ImeTracker.PHASE_SERVER_CURRENT_ACTIVE_IME); - return; - } - ImeTracker.forLogging().onProgress(statsToken, - ImeTracker.PHASE_SERVER_CURRENT_ACTIVE_IME); - final var visibilityStateComputer = userData.mVisibilityStateComputer; - final IBinder requestToken = visibilityStateComputer.getWindowTokenFrom( - windowToken, userId); - mVisibilityApplier.applyImeVisibility(requestToken, statsToken, - setVisible ? STATE_SHOW_IME : STATE_HIDE_IME, - SoftInputShowHideReason.NOT_SET /* ignore reason */, userId); - } + final var visibilityStateComputer = userData.mVisibilityStateComputer; + final IBinder requestToken = visibilityStateComputer.getWindowTokenFrom( + windowToken, userId); + mVisibilityApplier.applyImeVisibility(requestToken, statsToken, + setVisible ? STATE_SHOW_IME : STATE_HIDE_IME, + SoftInputShowHideReason.NOT_SET /* ignore reason */, userId); } finally { Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); } } @BinderThread - private void resetStylusHandwriting(int requestId) { - synchronized (ImfLock.class) { - final OptionalInt curRequest = mHwController.getCurrentRequestId(); - if (!curRequest.isPresent() || curRequest.getAsInt() != requestId) { - Slog.w(TAG, "IME requested to finish handwriting with a mismatched requestId: " - + requestId); - } - removeVirtualStylusIdForTestSessionLocked(); - scheduleResetStylusHandwriting(); + @GuardedBy("ImfLock.class") + private void resetStylusHandwritingLocked(int requestId) { + final OptionalInt curRequest = mHwController.getCurrentRequestId(); + if (!curRequest.isPresent() || curRequest.getAsInt() != requestId) { + Slog.w(TAG, "IME requested to finish handwriting with a mismatched requestId: " + + requestId); } + removeVirtualStylusIdForTestSessionLocked(); + scheduleResetStylusHandwriting(); } @GuardedBy("ImfLock.class") - private void setInputMethodWithSubtypeIdLocked(IBinder token, String id, int subtypeId, + private void setInputMethodWithSubtypeIndexLocked(String id, int subtypeIndex, @UserIdInt int userId) { - final var bindingController = getInputMethodBindingController(userId); - if (token == null) { - if (mContext.checkCallingOrSelfPermission( - android.Manifest.permission.WRITE_SECURE_SETTINGS) - != PackageManager.PERMISSION_GRANTED) { - throw new SecurityException( - "Using null token requires permission " - + android.Manifest.permission.WRITE_SECURE_SETTINGS); - } - } else if (bindingController.getCurToken() != token) { - Slog.w(TAG, "Ignoring setInputMethod of uid " + Binder.getCallingUid() - + " token: " + token); - return; - } else { - // Called with current IME's token. - final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); - if (settings.getMethodMap().get(id) != null - && settings.getEnabledInputMethodListWithFilter( - (info) -> info.getId().equals(id)).isEmpty()) { - throw new IllegalStateException("Requested IME is not enabled: " + id); - } + final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); + if (settings.getMethodMap().get(id) != null + && settings.getEnabledInputMethodListWithFilter( + (info) -> info.getId().equals(id)).isEmpty()) { + throw new IllegalStateException("Requested IME is not enabled: " + id); } - final long ident = Binder.clearCallingIdentity(); try { - setInputMethodLocked(id, subtypeId, userId); + setInputMethodLocked(id, subtypeIndex, userId); } finally { Binder.restoreCallingIdentity(ident); } @@ -4924,83 +4768,36 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } @BinderThread - private void hideMySoftInput(@NonNull IBinder token, @NonNull ImeTracker.Token statsToken, + @GuardedBy("ImfLock.class") + private void hideMySoftInputLocked(@NonNull ImeTracker.Token statsToken, @InputMethodManager.HideFlags int flags, @SoftInputShowHideReason int reason, @NonNull UserData userData) { - try { - Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.hideMySoftInput"); - final int userId = userData.mUserId; - synchronized (ImfLock.class) { - if (!calledWithValidTokenLocked(token, userData)) { - ImeTracker.forLogging().onFailed(statsToken, - ImeTracker.PHASE_SERVER_CURRENT_ACTIVE_IME); - return; - } - ImeTracker.forLogging().onProgress(statsToken, - ImeTracker.PHASE_SERVER_CURRENT_ACTIVE_IME); - final long ident = Binder.clearCallingIdentity(); - try { - if (Flags.refactorInsetsController()) { - userData.mCurClient.mClient.setImeVisibility(false, statsToken); - // TODO we will loose the flags here - if (userData.mImeBindingState != null - && userData.mImeBindingState.mFocusedWindowClient != null - && userData.mImeBindingState.mFocusedWindowClient.mClient != null) { - userData.mImeBindingState.mFocusedWindowClient.mClient - .setImeVisibility(false, statsToken); - } - } else { - final var visibilityStateComputer = userData.mVisibilityStateComputer; - hideCurrentInputLocked(visibilityStateComputer.getLastImeTargetWindow(), - statsToken, flags, null /* resultReceiver */, reason, userId); - } - } finally { - Binder.restoreCallingIdentity(ident); - } - } - } finally { - Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); + final int userId = userData.mUserId; + if (Flags.refactorInsetsController()) { + userData.mCurClient.mClient.setImeVisibility(false, statsToken); + // TODO we will loose the flags here + setImeVisibilityOnFocusedWindowClient(false, userData, statsToken); + } else { + final var visibilityStateComputer = userData.mVisibilityStateComputer; + hideCurrentInputLocked(visibilityStateComputer.getLastImeTargetWindow(), + statsToken, flags, null /* resultReceiver */, reason, userId); } } @BinderThread - private void showMySoftInput(@NonNull IBinder token, @NonNull ImeTracker.Token statsToken, + @GuardedBy("ImfLock.class") + private void showMySoftInputLocked(@NonNull ImeTracker.Token statsToken, @InputMethodManager.ShowFlags int flags, @SoftInputShowHideReason int reason, @NonNull UserData userData) { - try { - Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.showMySoftInput"); - final int userId = userData.mUserId; - synchronized (ImfLock.class) { - if (!calledWithValidTokenLocked(token, userData)) { - ImeTracker.forLogging().onFailed(statsToken, - ImeTracker.PHASE_SERVER_CURRENT_ACTIVE_IME); - return; - } - ImeTracker.forLogging().onProgress(statsToken, - ImeTracker.PHASE_SERVER_CURRENT_ACTIVE_IME); - final long ident = Binder.clearCallingIdentity(); - try { - if (Flags.refactorInsetsController()) { - userData.mCurClient.mClient.setImeVisibility(false, statsToken); - // TODO we will loose the flags here - if (userData.mImeBindingState != null - && userData.mImeBindingState.mFocusedWindowClient != null - && userData.mImeBindingState.mFocusedWindowClient.mClient != null) { - userData.mImeBindingState.mFocusedWindowClient.mClient - .setImeVisibility(true, statsToken); - } - } else { - final var visibilityStateComputer = userData.mVisibilityStateComputer; - showCurrentInputLocked(visibilityStateComputer.getLastImeTargetWindow(), - statsToken, flags, MotionEvent.TOOL_TYPE_UNKNOWN, - null /* resultReceiver */, reason, userId); - } - } finally { - Binder.restoreCallingIdentity(ident); - } - } - } finally { - Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); + final int userId = userData.mUserId; + if (Flags.refactorInsetsController()) { + userData.mCurClient.mClient.setImeVisibility(true, statsToken); + setImeVisibilityOnFocusedWindowClient(true, userData, statsToken); + } else { + final var visibilityStateComputer = userData.mVisibilityStateComputer; + showCurrentInputLocked(visibilityStateComputer.getLastImeTargetWindow(), + statsToken, flags, MotionEvent.TOOL_TYPE_UNKNOWN, + null /* resultReceiver */, reason, userId); } } @@ -5097,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( @@ -5111,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); } } @@ -5150,13 +4948,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. final int userId = resolveImeUserIdFromDisplayIdLocked(originatingDisplayId); final var userData = getUserData(userId); if (Flags.refactorInsetsController()) { - if (userData.mImeBindingState != null - && userData.mImeBindingState.mFocusedWindowClient != null - && userData.mImeBindingState.mFocusedWindowClient.mClient != null) { - userData.mImeBindingState.mFocusedWindowClient.mClient - .setImeVisibility(false, - null /* TODO(b329229469) check statsToken */); - } + setImeVisibilityOnFocusedWindowClient(false, userData, + null /* TODO(b329229469) check statsToken */); } else { hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow, @@ -5168,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 @@ -5206,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) { @@ -5234,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()) { @@ -5308,7 +5102,9 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. private record HandwritingRequest(int requestId, int pid, @NonNull UserData userData) { } @BinderThread - private void onStylusHandwritingReady(int requestId, int pid, @NonNull UserData userData) { + @GuardedBy("ImfLock.class") + private void onStylusHandwritingReadyLocked(int requestId, int pid, + @NonNull UserData userData) { mHandler.obtainMessage(MSG_START_HANDWRITING, new HandwritingRequest(requestId, pid, userData)).sendToTarget(); } @@ -5316,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; @@ -5656,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); @@ -5666,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 { @@ -5707,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); } /** @@ -5744,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; } @@ -5807,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 = @@ -5827,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; } @@ -5905,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); } } @@ -5990,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 { @@ -6006,6 +5802,25 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. mHandler.obtainMessage(MSG_REMOVE_IME_SURFACE).sendToTarget(); } + @Override + public void setHasVisibleImeLayeringOverlay(boolean hasVisibleOverlay, int displayId) { + synchronized (ImfLock.class) { + final var userId = resolveImeUserIdFromDisplayIdLocked(displayId); + getUserData(userId).mVisibilityStateComputer.setHasVisibleImeLayeringOverlay( + hasVisibleOverlay); + } + } + + @Override + public void onImeInputTargetVisibilityChanged(@NonNull IBinder imeInputTarget, + boolean visibleAndNotRemoved, int displayId) { + synchronized (ImfLock.class) { + final var userId = resolveImeUserIdFromDisplayIdLocked(displayId); + getUserData(userId).mVisibilityStateComputer.onImeInputTargetVisibilityChanged( + imeInputTarget, visibleAndNotRemoved); + } + } + @ImfLockFree @Override public void updateImeWindowStatus(boolean disableImeIcon, int displayId) { @@ -6118,71 +5933,54 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } @BinderThread - private IInputContentUriToken createInputContentUriToken(@Nullable IBinder token, - @Nullable Uri contentUri, @Nullable String packageName, @NonNull UserData userData) { - if (token == null) { - throw new NullPointerException("token"); - } - if (packageName == null) { - throw new NullPointerException("packageName"); - } - if (contentUri == null) { - throw new NullPointerException("contentUri"); - } + @GuardedBy("ImfLock.class") + @Nullable + private IInputContentUriToken createInputContentUriTokenLocked(@NonNull Uri contentUri, + @NonNull String packageName, @NonNull UserData userData) { + Objects.requireNonNull(packageName, "packageName must not be null"); + Objects.requireNonNull(contentUri, "contentUri must not be null"); final String contentUriScheme = contentUri.getScheme(); if (!"content".equals(contentUriScheme)) { throw new InvalidParameterException("contentUri must have content scheme"); } - synchronized (ImfLock.class) { - final int uid = Binder.getCallingUid(); - final var bindingController = userData.mBindingController; - if (bindingController.getSelectedMethodId() == null) { - return null; - } - if (bindingController.getCurToken() != token) { - Slog.e(TAG, "Ignoring createInputContentUriToken mCurToken=" - + bindingController.getCurToken() + " token=" + token); - return null; - } - // We cannot simply distinguish a bad IME that reports an arbitrary package name from - // an unfortunate IME whose internal state is already obsolete due to the asynchronous - // nature of our system. Let's compare it with our internal record. - final var curPackageName = userData.mCurEditorInfo != null - ? userData.mCurEditorInfo.packageName : null; - if (!TextUtils.equals(curPackageName, packageName)) { - Slog.e(TAG, "Ignoring createInputContentUriToken mCurEditorInfo.packageName=" - + curPackageName + " packageName=" + packageName); - return null; - } - // This user ID can never be spoofed. - final int appUserId = UserHandle.getUserId(userData.mCurClient.mUid); - // This user ID may be invalid if "contentUri" embedded an invalid user ID. - final int contentUriOwnerUserId = ContentProvider.getUserIdFromUri(contentUri, - userData.mUserId); - final Uri contentUriWithoutUserId = ContentProvider.getUriWithoutUserId(contentUri); - // Note: InputContentUriTokenHandler.take() checks whether the IME (specified by "uid") - // actually has the right to grant a read permission for "contentUriWithoutUserId" that - // is claimed to belong to "contentUriOwnerUserId". For example, specifying random - // content URI and/or contentUriOwnerUserId just results in a SecurityException thrown - // from InputContentUriTokenHandler.take() and can never be allowed beyond what is - // actually allowed to "uid", which is guaranteed to be the IME's one. - return new InputContentUriTokenHandler(contentUriWithoutUserId, uid, - packageName, contentUriOwnerUserId, appUserId); + final int uid = Binder.getCallingUid(); + final var bindingController = userData.mBindingController; + if (bindingController.getSelectedMethodId() == null) { + return null; } + // We cannot simply distinguish a bad IME that reports an arbitrary package name from + // an unfortunate IME whose internal state is already obsolete due to the asynchronous + // nature of our system. Let's compare it with our internal record. + final var curPackageName = userData.mCurEditorInfo != null + ? userData.mCurEditorInfo.packageName : null; + if (!TextUtils.equals(curPackageName, packageName)) { + Slog.e(TAG, "Ignoring createInputContentUriTokenLocked mCurEditorInfo.packageName=" + + curPackageName + " packageName=" + packageName); + return null; + } + // This user ID can never be spoofed. + final int appUserId = UserHandle.getUserId(userData.mCurClient.mUid); + // This user ID may be invalid if "contentUri" embedded an invalid user ID. + final int contentUriOwnerUserId = ContentProvider.getUserIdFromUri(contentUri, + userData.mUserId); + final Uri contentUriWithoutUserId = ContentProvider.getUriWithoutUserId(contentUri); + // Note: InputContentUriTokenHandler.take() checks whether the IME (specified by "uid") + // actually has the right to grant a read permission for "contentUriWithoutUserId" that + // is claimed to belong to "contentUriOwnerUserId". For example, specifying random + // content URI and/or contentUriOwnerUserId just results in a SecurityException thrown + // from InputContentUriTokenHandler.take() and can never be allowed beyond what is + // actually allowed to "uid", which is guaranteed to be the IME's one. + return new InputContentUriTokenHandler(contentUriWithoutUserId, uid, + packageName, contentUriOwnerUserId, appUserId); } @BinderThread - private void reportFullscreenMode(@NonNull IBinder token, boolean fullscreen, - @NonNull UserData userData) { - synchronized (ImfLock.class) { - if (!calledWithValidTokenLocked(token, userData)) { - return; - } - if (userData.mCurClient != null && userData.mCurClient.mClient != null) { - userData.mInFullscreenMode = fullscreen; - userData.mCurClient.mClient.reportFullscreenMode(fullscreen); - } + @GuardedBy("ImfLock.class") + private void reportFullscreenModeLocked(boolean fullscreen, @NonNull UserData userData) { + if (userData.mCurClient != null && userData.mCurClient.mClient != null) { + userData.mInFullscreenMode = fullscreen; + userData.mCurClient.mClient.reportFullscreenMode(fullscreen); } } @@ -6273,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:"); @@ -6305,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=" @@ -6323,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()); @@ -6346,7 +6145,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. }; mUserDataRepository.forAllUserData(userDataDump); - if (mNewInputMethodSwitcherMenuEnabled) { + if (Flags.imeSwitcherRevamp()) { p.println(" menuControllerNew:"); mMenuControllerNew.dump(p, " "); } else { @@ -6397,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) { @@ -6628,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()) { @@ -6674,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; @@ -6769,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); @@ -6789,6 +6588,28 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. out.print(imeId); out.print(" selected for user #"); out.println(userId); + + // Workaround for b/354782333. + final InputMethodSettings settings = + InputMethodSettingsRepository.get(userId); + final var bindingController = getInputMethodBindingController(userId); + final int deviceId = bindingController.getDeviceIdToShowIme(); + final String settingsValue; + if (deviceId == DEVICE_ID_DEFAULT) { + settingsValue = settings.getSelectedInputMethod(); + } else { + settingsValue = settings.getSelectedDefaultDeviceInputMethod(); + } + if (!TextUtils.equals(settingsValue, imeId)) { + Slog.w(TAG, "DEFAULT_INPUT_METHOD=" + settingsValue + + " is not updated. Fixing it up to " + imeId + + " See b/354782333."); + if (deviceId == DEVICE_ID_DEFAULT) { + settings.putSelectedInputMethod(imeId); + } else { + settings.putSelectedDefaultDeviceInputMethod(imeId); + } + } } hasFailed |= failedToSelectUnknownIme; } @@ -6810,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; @@ -6825,16 +6646,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); final var userData = getUserData(userId); if (Flags.refactorInsetsController()) { - if (userData.mImeBindingState != null - && userData.mImeBindingState.mFocusedWindowClient != null - && userData.mImeBindingState.mFocusedWindowClient.mClient - != null) { - userData.mImeBindingState.mFocusedWindowClient.mClient - .setImeVisibility(false, - null /* TODO(b329229469) initialize statsToken here? */); - } else { - // TODO(b329229469): ImeTracker? - } + setImeVisibilityOnFocusedWindowClient(false, userData, + null /* TODO(b329229469) initialize statsToken here? */); } else { hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow, 0 /* flags */, @@ -6873,6 +6686,23 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. return ShellCommandResult.SUCCESS; } + @GuardedBy("ImfLock.class") + boolean setImeVisibilityOnFocusedWindowClient(boolean visible, UserData userData, + @NonNull ImeTracker.Token statsToken) { + if (Flags.refactorInsetsController()) { + if (userData.mImeBindingState != null + && userData.mImeBindingState.mFocusedWindowClient != null + && userData.mImeBindingState.mFocusedWindowClient.mClient != null) { + userData.mImeBindingState.mFocusedWindowClient.mClient.setImeVisibility(visible, + statsToken); + return true; + } + ImeTracker.forLogging().onFailed(statsToken, + ImeTracker.PHASE_SERVER_SET_VISIBILITY_ON_FOCUSED_WINDOW); + } + return false; + } + /** * Handles {@code adb shell cmd input_method tracing start/stop/save-for-bugreport}. * @@ -6966,13 +6796,14 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. private static final class InputMethodPrivilegedOperationsImpl extends IInputMethodPrivilegedOperations.Stub { + @NonNull private final InputMethodManagerService mImms; @NonNull private final IBinder mToken; @NonNull private final UserData mUserData; - InputMethodPrivilegedOperationsImpl(InputMethodManagerService imms, + InputMethodPrivilegedOperationsImpl(@NonNull InputMethodManagerService imms, @NonNull IBinder token, @NonNull UserData userData) { mImms = imms; mToken = token; @@ -6982,19 +6813,34 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @BinderThread @Override public void setImeWindowStatusAsync(int vis, int backDisposition) { - mImms.setImeWindowStatus(mToken, vis, backDisposition, mUserData); + synchronized (ImfLock.class) { + if (!calledWithValidTokenLocked(mToken, mUserData)) { + return; + } + mImms.setImeWindowStatusLocked(vis, backDisposition, mUserData); + } } @BinderThread @Override public void reportStartInputAsync(IBinder startInputToken) { - mImms.reportStartInput(mToken, startInputToken, mUserData); + synchronized (ImfLock.class) { + if (!calledWithValidTokenLocked(mToken, mUserData)) { + return; + } + mImms.reportStartInputLocked(startInputToken, mUserData); + } } @BinderThread @Override public void setHandwritingSurfaceNotTouchable(boolean notTouchable) { - mImms.mHwController.setNotTouchable(notTouchable); + synchronized (ImfLock.class) { + if (!calledWithValidTokenLocked(mToken, mUserData)) { + return; + } + mImms.mHwController.setNotTouchable(notTouchable); + } } @BinderThread @@ -7003,8 +6849,14 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. AndroidFuture future /* T=IBinder */) { @SuppressWarnings("unchecked") final AndroidFuture<IBinder> typedFuture = future; try { - typedFuture.complete(mImms.createInputContentUriToken( - mToken, contentUri, packageName, mUserData).asBinder()); + synchronized (ImfLock.class) { + if (!calledWithValidTokenLocked(mToken, mUserData)) { + typedFuture.complete(null); + return; + } + typedFuture.complete(mImms.createInputContentUriTokenLocked( + contentUri, packageName, mUserData).asBinder()); + } } catch (Throwable e) { typedFuture.completeExceptionally(e); } @@ -7013,7 +6865,12 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @BinderThread @Override public void reportFullscreenModeAsync(boolean fullscreen) { - mImms.reportFullscreenMode(mToken, fullscreen, mUserData); + synchronized (ImfLock.class) { + if (!calledWithValidTokenLocked(mToken, mUserData)) { + return; + } + mImms.reportFullscreenModeLocked(fullscreen, mUserData); + } } @BinderThread @@ -7021,8 +6878,14 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. public void setInputMethod(String id, AndroidFuture future /* T=Void */) { @SuppressWarnings("unchecked") final AndroidFuture<Void> typedFuture = future; try { - mImms.setInputMethod(mToken, id, mUserData); - typedFuture.complete(null); + synchronized (ImfLock.class) { + if (!calledWithValidTokenLocked(mToken, mUserData)) { + typedFuture.complete(null); + return; + } + mImms.setInputMethodAndSubtypeLocked(id, null /* subtype */, mUserData); + typedFuture.complete(null); + } } catch (Throwable e) { typedFuture.completeExceptionally(e); } @@ -7034,8 +6897,14 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. AndroidFuture future /* T=Void */) { @SuppressWarnings("unchecked") final AndroidFuture<Void> typedFuture = future; try { - mImms.setInputMethodAndSubtype(mToken, id, subtype, mUserData); - typedFuture.complete(null); + synchronized (ImfLock.class) { + if (!calledWithValidTokenLocked(mToken, mUserData)) { + typedFuture.complete(null); + return; + } + mImms.setInputMethodAndSubtypeLocked(id, subtype, mUserData); + typedFuture.complete(null); + } } catch (Throwable e) { typedFuture.completeExceptionally(e); } @@ -7048,8 +6917,25 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. AndroidFuture future /* T=Void */) { @SuppressWarnings("unchecked") final AndroidFuture<Void> typedFuture = future; try { - mImms.hideMySoftInput(mToken, statsToken, flags, reason, mUserData); - typedFuture.complete(null); + synchronized (ImfLock.class) { + if (!calledWithValidTokenLocked(mToken, mUserData)) { + ImeTracker.forLogging().onFailed(statsToken, + ImeTracker.PHASE_SERVER_CURRENT_ACTIVE_IME); + typedFuture.complete(null); + return; + } + ImeTracker.forLogging().onProgress(statsToken, + ImeTracker.PHASE_SERVER_CURRENT_ACTIVE_IME); + Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.hideMySoftInput"); + final long ident = Binder.clearCallingIdentity(); + try { + mImms.hideMySoftInputLocked(statsToken, flags, reason, mUserData); + typedFuture.complete(null); + } finally { + Binder.restoreCallingIdentity(ident); + Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); + } + } } catch (Throwable e) { typedFuture.completeExceptionally(e); } @@ -7062,8 +6948,25 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. AndroidFuture future /* T=Void */) { @SuppressWarnings("unchecked") final AndroidFuture<Void> typedFuture = future; try { - mImms.showMySoftInput(mToken, statsToken, flags, reason, mUserData); - typedFuture.complete(null); + synchronized (ImfLock.class) { + if (!calledWithValidTokenLocked(mToken, mUserData)) { + ImeTracker.forLogging().onFailed(statsToken, + ImeTracker.PHASE_SERVER_CURRENT_ACTIVE_IME); + typedFuture.complete(null); + return; + } + ImeTracker.forLogging().onProgress(statsToken, + ImeTracker.PHASE_SERVER_CURRENT_ACTIVE_IME); + Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.showMySoftInput"); + final long ident = Binder.clearCallingIdentity(); + try { + mImms.showMySoftInputLocked(statsToken, flags, reason, mUserData); + typedFuture.complete(null); + } finally { + Binder.restoreCallingIdentity(ident); + Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); + } + } } catch (Throwable e) { typedFuture.completeExceptionally(e); } @@ -7072,7 +6975,17 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @BinderThread @Override public void updateStatusIconAsync(String packageName, @DrawableRes int iconId) { - mImms.updateStatusIcon(mToken, packageName, iconId, mUserData); + synchronized (ImfLock.class) { + if (!calledWithValidTokenLocked(mToken, mUserData)) { + return; + } + final long ident = Binder.clearCallingIdentity(); + try { + mImms.updateStatusIconLocked(packageName, iconId, mUserData); + } finally { + Binder.restoreCallingIdentity(ident); + } + } } @BinderThread @@ -7080,7 +6993,13 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. public void switchToPreviousInputMethod(AndroidFuture future /* T=Boolean */) { @SuppressWarnings("unchecked") final AndroidFuture<Boolean> typedFuture = future; try { - typedFuture.complete(mImms.switchToPreviousInputMethod(mToken, mUserData)); + synchronized (ImfLock.class) { + if (!calledWithValidTokenLocked(mToken, mUserData)) { + typedFuture.complete(false); + return; + } + typedFuture.complete(mImms.switchToPreviousInputMethodLocked(mUserData)); + } } catch (Throwable e) { typedFuture.completeExceptionally(e); } @@ -7092,8 +7011,14 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. AndroidFuture future /* T=Boolean */) { @SuppressWarnings("unchecked") final AndroidFuture<Boolean> typedFuture = future; try { - typedFuture.complete(mImms.switchToNextInputMethod(mToken, onlyCurrentIme, - mUserData)); + synchronized (ImfLock.class) { + if (!calledWithValidTokenLocked(mToken, mUserData)) { + typedFuture.complete(false); + return; + } + typedFuture.complete(mImms.switchToNextInputMethodLocked(onlyCurrentIme, + mUserData)); + } } catch (Throwable e) { typedFuture.completeExceptionally(e); } @@ -7104,8 +7029,14 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. public void shouldOfferSwitchingToNextInputMethod(AndroidFuture future /* T=Boolean */) { @SuppressWarnings("unchecked") final AndroidFuture<Boolean> typedFuture = future; try { - typedFuture.complete(mImms.shouldOfferSwitchingToNextInputMethod(mToken, - mUserData)); + synchronized (ImfLock.class) { + if (!calledWithValidTokenLocked(mToken, mUserData)) { + typedFuture.complete(false); + return; + } + typedFuture.complete(mImms.shouldOfferSwitchingToNextInputMethodLocked( + mUserData)); + } } catch (Throwable e) { typedFuture.completeExceptionally(e); } @@ -7114,39 +7045,68 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @BinderThread @Override public void onImeSwitchButtonClickFromClient(int displayId) { - mImms.onImeSwitchButtonClickFromClient(mToken, displayId, mUserData); + synchronized (ImfLock.class) { + if (!calledWithValidTokenLocked(mToken, mUserData)) { + return; + } + mImms.onImeSwitchButtonClickLocked(displayId, mUserData); + } } @BinderThread @Override public void notifyUserActionAsync() { - mImms.notifyUserAction(mToken, mUserData); + synchronized (ImfLock.class) { + if (!calledWithValidTokenLocked(mToken, mUserData)) { + return; + } + mImms.notifyUserActionLocked(mUserData); + } } @BinderThread @Override public void applyImeVisibilityAsync(IBinder windowToken, boolean setVisible, @NonNull ImeTracker.Token statsToken) { - mImms.applyImeVisibility(mToken, windowToken, setVisible, statsToken, mUserData); + synchronized (ImfLock.class) { + if (!calledWithValidTokenLocked(mToken, mUserData)) { + ImeTracker.forLogging().onFailed(statsToken, + ImeTracker.PHASE_SERVER_CURRENT_ACTIVE_IME); + return; + } + ImeTracker.forLogging().onProgress(statsToken, + ImeTracker.PHASE_SERVER_CURRENT_ACTIVE_IME); + mImms.applyImeVisibilityLocked(windowToken, setVisible, statsToken, mUserData); + } } @BinderThread @Override public void onStylusHandwritingReady(int requestId, int pid) { - mImms.onStylusHandwritingReady(requestId, pid, mUserData); + synchronized (ImfLock.class) { + if (!calledWithValidTokenLocked(mToken, mUserData)) { + return; + } + mImms.onStylusHandwritingReadyLocked(requestId, pid, mUserData); + } } @BinderThread @Override public void resetStylusHandwriting(int requestId) { - mImms.resetStylusHandwriting(requestId); + synchronized (ImfLock.class) { + if (!calledWithValidTokenLocked(mToken, mUserData)) { + return; + } + mImms.resetStylusHandwritingLocked(requestId); + } } @BinderThread @Override public void switchKeyboardLayoutAsync(int direction) { synchronized (ImfLock.class) { - if (!mImms.calledWithValidTokenLocked(mToken, mUserData)) { + if (!calledWithValidTokenLocked(mToken, mUserData)) { return; } final long ident = Binder.clearCallingIdentity(); @@ -7157,5 +7117,25 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } } } + + /** + * Returns true iff the caller is identified to be the current input method with the token. + * + * @param token the window token given to the input method when it was started + * @param userData {@link UserData} of the calling IME process + * @return true if and only if non-null valid token is specified + */ + @GuardedBy("ImfLock.class") + private static boolean calledWithValidTokenLocked(@NonNull IBinder token, + @NonNull UserData userData) { + Objects.requireNonNull(token, "token must not be null"); + final var bindingController = userData.mBindingController; + if (token != bindingController.getCurToken()) { + Slog.e(TAG, "Ignoring " + Debug.getCaller() + " due to an invalid token." + + " uid:" + Binder.getCallingUid() + " token:" + token); + return false; + } + return true; + } } } 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/location/contexthub/ContextHubTestModeManager.java b/services/core/java/com/android/server/location/contexthub/ContextHubTestModeManager.java index f2714dbd7e5f..2bb3be6a3332 100644 --- a/services/core/java/com/android/server/location/contexthub/ContextHubTestModeManager.java +++ b/services/core/java/com/android/server/location/contexthub/ContextHubTestModeManager.java @@ -17,10 +17,12 @@ package com.android.server.location.contexthub; import android.chre.flags.Flags; +import android.hardware.location.ContextHubTransaction; import android.hardware.location.NanoAppMessage; import android.util.Log; -import java.util.Random; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.Callable; /** * A class to manage behaviors during test mode. This is used for testing. @@ -29,32 +31,31 @@ import java.util.Random; public class ContextHubTestModeManager { private static final String TAG = "ContextHubTestModeManager"; - /** Probability of duplicating a message. */ - private static final double MESSAGE_DROP_PROBABILITY = 0.05; - - /** Probability of duplicating a message. */ - private static final double MESSAGE_DUPLICATION_PROBABILITY = 0.05; + private static final int DROP_MESSAGE_TO_HOST_EVENT = 0; + private static final int DROP_MESSAGE_TO_CONTEXT_HUB_EVENT = 1; + private static final int DUPLICATE_MESSAGE_TO_HOST_EVENT = 2; + private static final int DUPLICATE_MESSAGE_TO_CONTEXT_HUB_EVENT = 3; + private static final int NUMBER_OF_EVENTS = 4; /** The number of total messages to send when the duplication event happens. */ private static final int NUM_MESSAGES_TO_DUPLICATE = 3; - /** - * The seed for the random number generator. This is used to make the - * test more deterministic. - */ - private static final long SEED = 0xDEADBEEF; - - private final Random mRandom = new Random(SEED); + /** The counter to track the number of interactions with the test mode manager. */ + private final AtomicLong mCounter = new AtomicLong(0); /** * @return whether the message was handled * @see ContextHubServiceCallback#handleNanoappMessage */ public boolean handleNanoappMessage(Runnable handleMessage, NanoAppMessage message) { + if (!message.isReliable()) { + return false; + } + + long counterValue = mCounter.getAndIncrement(); if (Flags.reliableMessageDuplicateDetectionService() - && message.isReliable() - && mRandom.nextDouble() < MESSAGE_DUPLICATION_PROBABILITY) { - Log.i(TAG, "[TEST MODE] Duplicating message (" + && counterValue % NUMBER_OF_EVENTS == DUPLICATE_MESSAGE_TO_HOST_EVENT) { + Log.i(TAG, "[TEST MODE] Duplicating message to host (" + NUM_MESSAGES_TO_DUPLICATE + " sends) with message sequence number: " + message.getMessageSequenceNumber()); @@ -63,6 +64,14 @@ public class ContextHubTestModeManager { } return true; } + + if (counterValue % NUMBER_OF_EVENTS == DROP_MESSAGE_TO_HOST_EVENT) { + Log.i(TAG, "[TEST MODE] Dropping message to host with " + + "message sequence number: " + + message.getMessageSequenceNumber()); + return true; + } + return false; } @@ -70,14 +79,39 @@ public class ContextHubTestModeManager { * @return whether the message was handled * @see IContextHubWrapper#sendMessageToContextHub */ - public boolean sendMessageToContextHub(NanoAppMessage message) { + public boolean sendMessageToContextHub(Callable<Integer> sendMessage, NanoAppMessage message) { + if (!message.isReliable()) { + return false; + } + + long counterValue = mCounter.getAndIncrement(); + if (counterValue % NUMBER_OF_EVENTS == DUPLICATE_MESSAGE_TO_CONTEXT_HUB_EVENT) { + Log.i(TAG, "[TEST MODE] Duplicating message to the Context Hub (" + + NUM_MESSAGES_TO_DUPLICATE + + " sends) with message sequence number: " + + message.getMessageSequenceNumber()); + for (int i = 0; i < NUM_MESSAGES_TO_DUPLICATE; ++i) { + try { + int result = sendMessage.call(); + if (result != ContextHubTransaction.RESULT_SUCCESS) { + Log.e(TAG, "sendMessage returned an error: " + result); + } + } catch (Exception e) { + Log.e(TAG, "Exception in sendMessageToContextHub: " + + e.getMessage()); + } + } + return true; + } + if (Flags.reliableMessageRetrySupportService() - && message.isReliable() - && mRandom.nextDouble() < MESSAGE_DROP_PROBABILITY) { - Log.i(TAG, "[TEST MODE] Dropping message with message sequence number: " + && counterValue % NUMBER_OF_EVENTS == DROP_MESSAGE_TO_CONTEXT_HUB_EVENT) { + Log.i(TAG, "[TEST MODE] Dropping message to the Context Hub with " + + "message sequence number: " + message.getMessageSequenceNumber()); return true; } + return false; } } diff --git a/services/core/java/com/android/server/location/contexthub/IContextHubWrapper.java b/services/core/java/com/android/server/location/contexthub/IContextHubWrapper.java index 4fc3d8715a88..a8ad41853d34 100644 --- a/services/core/java/com/android/server/location/contexthub/IContextHubWrapper.java +++ b/services/core/java/com/android/server/location/contexthub/IContextHubWrapper.java @@ -53,6 +53,7 @@ import java.util.Map; import java.util.NoSuchElementException; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.Callable; /** * @hide @@ -659,32 +660,40 @@ public abstract class IContextHubWrapper { @ContextHubTransaction.Result public int sendMessageToContextHub(short hostEndpointId, int contextHubId, - NanoAppMessage message) throws RemoteException { + NanoAppMessage message) { android.hardware.contexthub.IContextHub hub = getHub(); if (hub == null) { return ContextHubTransaction.RESULT_FAILED_BAD_PARAMS; } - try { - var msg = ContextHubServiceUtil.createAidlContextHubMessage( - hostEndpointId, message); - - // Only process the message normally if not using test mode manager or if - // the test mode manager call returned false as this indicates it did not - // process the message. - boolean useTestModeManager = Flags.reliableMessageImplementation() - && Flags.reliableMessageTestModeBehavior() - && mIsTestModeEnabled.get(); - if (!useTestModeManager || !mTestModeManager.sendMessageToContextHub(message)) { + Callable<Integer> sendMessage = () -> { + try { + var msg = ContextHubServiceUtil.createAidlContextHubMessage( + hostEndpointId, message); hub.sendMessageToHub(contextHubId, msg); + return ContextHubTransaction.RESULT_SUCCESS; + } catch (RemoteException | ServiceSpecificException e) { + return ContextHubTransaction.RESULT_FAILED_UNKNOWN; + } catch (IllegalArgumentException e) { + return ContextHubTransaction.RESULT_FAILED_BAD_PARAMS; + } + }; + + // Only process the message normally if not using test mode manager or if + // the test mode manager call returned false as this indicates it did not + // process the message. + boolean useTestModeManager = Flags.reliableMessageImplementation() + && Flags.reliableMessageTestModeBehavior() + && mIsTestModeEnabled.get(); + if (!useTestModeManager || !mTestModeManager.sendMessageToContextHub( + sendMessage, message)) { + try { + return sendMessage.call(); + } catch (Exception e) { + return ContextHubTransaction.RESULT_FAILED_UNKNOWN; } - - return ContextHubTransaction.RESULT_SUCCESS; - } catch (RemoteException | ServiceSpecificException e) { - return ContextHubTransaction.RESULT_FAILED_UNKNOWN; - } catch (IllegalArgumentException e) { - return ContextHubTransaction.RESULT_FAILED_BAD_PARAMS; } + return ContextHubTransaction.RESULT_SUCCESS; } @ContextHubTransaction.Result 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 3cc04570643f..a44e55344fe3 100644 --- a/services/core/java/com/android/server/notification/ConditionProviders.java +++ b/services/core/java/com/android/server/notification/ConditionProviders.java @@ -17,7 +17,7 @@ package com.android.server.notification; import static android.service.notification.Condition.STATE_TRUE; -import static android.service.notification.ZenModeConfig.UPDATE_ORIGIN_USER; +import static android.service.notification.ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI; import android.app.INotificationManager; import android.app.NotificationManager; @@ -326,7 +326,7 @@ public class ConditionProviders extends ManagedServices { // 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 == UPDATE_ORIGIN_USER) { + if (r.condition != null && r.condition.source == ORIGIN_USER_IN_SYSTEMUI) { if (r.condition.state == STATE_TRUE && c.state == STATE_TRUE) { r.condition = c; } diff --git a/services/core/java/com/android/server/notification/DefaultDeviceEffectsApplier.java b/services/core/java/com/android/server/notification/DefaultDeviceEffectsApplier.java index d060f8f2d036..c8cb54f8a55e 100644 --- a/services/core/java/com/android/server/notification/DefaultDeviceEffectsApplier.java +++ b/services/core/java/com/android/server/notification/DefaultDeviceEffectsApplier.java @@ -31,7 +31,7 @@ import android.os.PowerManager; import android.service.notification.DeviceEffectsApplier; import android.service.notification.ZenDeviceEffects; import android.service.notification.ZenModeConfig; -import android.service.notification.ZenModeConfig.ConfigChangeOrigin; +import android.service.notification.ZenModeConfig.ConfigOrigin; import android.util.Slog; import com.android.internal.annotations.GuardedBy; @@ -72,7 +72,7 @@ class DefaultDeviceEffectsApplier implements DeviceEffectsApplier { } @Override - public void apply(ZenDeviceEffects effects, @ConfigChangeOrigin int origin) { + public void apply(ZenDeviceEffects effects, @ConfigOrigin int origin) { Binder.withCleanCallingIdentity(() -> { if (mLastAppliedEffects.shouldSuppressAmbientDisplay() != effects.shouldSuppressAmbientDisplay()) { @@ -120,15 +120,16 @@ class DefaultDeviceEffectsApplier implements DeviceEffectsApplier { mLastAppliedEffects = effects; } - private void updateOrScheduleNightMode(boolean useNightMode, @ConfigChangeOrigin int origin) { + private void updateOrScheduleNightMode(boolean useNightMode, @ConfigOrigin int origin) { mPendingNightMode = useNightMode; // Changing the theme can be disruptive for the user (Activities are likely recreated, may // lose some state). Therefore we only apply the change immediately if the rule was // activated manually, or we are initializing, or the screen is currently off/dreaming. - if (origin == ZenModeConfig.UPDATE_ORIGIN_INIT - || origin == ZenModeConfig.UPDATE_ORIGIN_INIT_USER - || origin == ZenModeConfig.UPDATE_ORIGIN_USER + if (origin == ZenModeConfig.ORIGIN_INIT + || origin == ZenModeConfig.ORIGIN_INIT_USER + || origin == ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI + || origin == ZenModeConfig.ORIGIN_USER_IN_APP || !mPowerManager.isInteractive()) { unregisterScreenOffReceiver(); updateNightModeImmediately(useNightMode); 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 4179eddc7bc4..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()); } } }); @@ -5771,16 +5772,20 @@ public class NotificationManagerService extends SystemService { Binder.getCallingUid()); } - @ZenModeConfig.ConfigChangeOrigin + @ZenModeConfig.ConfigOrigin private int computeZenOrigin(boolean fromUser) { // "fromUser" is introduced with MODES_API, so only consider it in that case. - // (Non-MODES_API behavior should also not depend at all on UPDATE_ORIGIN_USER). + // (Non-MODES_API behavior should also not depend at all on ORIGIN_USER_IN_X). if (android.app.Flags.modesApi() && fromUser) { - return ZenModeConfig.UPDATE_ORIGIN_USER; + if (isCallerSystemOrSystemUi()) { + return ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI; + } else { + return ZenModeConfig.ORIGIN_USER_IN_APP; + } } else if (isCallerSystemOrSystemUi()) { - return ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI; + return ZenModeConfig.ORIGIN_SYSTEM; } else { - return ZenModeConfig.UPDATE_ORIGIN_APP; + return ZenModeConfig.ORIGIN_APP; } } @@ -6137,7 +6142,7 @@ public class NotificationManagerService extends SystemService { enforcePolicyAccess(pkg, "setNotificationPolicy"); enforceUserOriginOnlyFromSystem(fromUser, "setNotificationPolicy"); int callingUid = Binder.getCallingUid(); - @ZenModeConfig.ConfigChangeOrigin int origin = computeZenOrigin(fromUser); + @ZenModeConfig.ConfigOrigin int origin = computeZenOrigin(fromUser); boolean shouldApplyAsImplicitRule = android.app.Flags.modesApi() && !canManageGlobalZenPolicy(pkg, callingUid); @@ -8663,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) { @@ -9386,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()); } } @@ -10368,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") @@ -10535,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); } @@ -10554,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/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java index c09504fa36c8..821722b15645 100644 --- a/services/core/java/com/android/server/notification/PreferencesHelper.java +++ b/services/core/java/com/android/server/notification/PreferencesHelper.java @@ -30,7 +30,6 @@ import static android.app.NotificationManager.IMPORTANCE_LOW; import static android.app.NotificationManager.IMPORTANCE_MAX; import static android.app.NotificationManager.IMPORTANCE_NONE; import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED; - import static android.os.UserHandle.USER_SYSTEM; import static android.service.notification.Flags.notificationClassification; @@ -1979,8 +1978,8 @@ public class PreferencesHelper implements RankingConfig { (areChannelsBypassingDnd ? NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND : 0), policy.priorityConversationSenders), - fromSystemOrSystemUi ? ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI - : ZenModeConfig.UPDATE_ORIGIN_APP, + fromSystemOrSystemUi ? ZenModeConfig.ORIGIN_SYSTEM + : ZenModeConfig.ORIGIN_APP, callingUid); } diff --git a/services/core/java/com/android/server/notification/ZenModeConditions.java b/services/core/java/com/android/server/notification/ZenModeConditions.java index 3650536ac7f6..268d835e704c 100644 --- a/services/core/java/com/android/server/notification/ZenModeConditions.java +++ b/services/core/java/com/android/server/notification/ZenModeConditions.java @@ -116,8 +116,8 @@ public class ZenModeConditions implements ConditionProviders.Callback { if (DEBUG) Log.d(TAG, "onServiceAdded " + component); final int callingUid = Binder.getCallingUid(); mHelper.setConfig(mHelper.getConfig(), component, - callingUid == Process.SYSTEM_UID ? ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI - : ZenModeConfig.UPDATE_ORIGIN_APP, + callingUid == Process.SYSTEM_UID ? ZenModeConfig.ORIGIN_SYSTEM + : ZenModeConfig.ORIGIN_APP, "zmc.onServiceAdded:" + component, callingUid); } @@ -128,8 +128,8 @@ public class ZenModeConditions implements ConditionProviders.Callback { if (config == null) return; final int callingUid = Binder.getCallingUid(); mHelper.setAutomaticZenRuleState(id, condition, - callingUid == Process.SYSTEM_UID ? ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI - : ZenModeConfig.UPDATE_ORIGIN_APP, + callingUid == Process.SYSTEM_UID ? ZenModeConfig.ORIGIN_SYSTEM + : ZenModeConfig.ORIGIN_APP, callingUid); } diff --git a/services/core/java/com/android/server/notification/ZenModeEventLogger.java b/services/core/java/com/android/server/notification/ZenModeEventLogger.java index 4a82057ed2a2..b03a54ec0cd3 100644 --- a/services/core/java/com/android/server/notification/ZenModeEventLogger.java +++ b/services/core/java/com/android/server/notification/ZenModeEventLogger.java @@ -34,7 +34,7 @@ import android.os.Process; import android.service.notification.DNDPolicyProto; import android.service.notification.ZenAdapters; import android.service.notification.ZenModeConfig; -import android.service.notification.ZenModeConfig.ConfigChangeOrigin; +import android.service.notification.ZenModeConfig.ConfigOrigin; import android.service.notification.ZenModeConfig.ZenRule; import android.service.notification.ZenModeDiff; import android.service.notification.ZenPolicy; @@ -113,7 +113,7 @@ class ZenModeEventLogger { * @param origin The origin of the Zen change. */ public final void maybeLogZenChange(ZenModeInfo prevInfo, ZenModeInfo newInfo, int callingUid, - @ConfigChangeOrigin int origin) { + @ConfigOrigin int origin) { mChangeState.init(prevInfo, newInfo, callingUid, origin); if (mChangeState.shouldLogChanges()) { maybeReassignCallingUid(); @@ -250,11 +250,11 @@ class ZenModeEventLogger { ZenModeConfig mPrevConfig, mNewConfig; NotificationManager.Policy mPrevPolicy, mNewPolicy; int mCallingUid = Process.INVALID_UID; - @ConfigChangeOrigin - int mOrigin = ZenModeConfig.UPDATE_ORIGIN_UNKNOWN; + @ConfigOrigin + int mOrigin = ZenModeConfig.ORIGIN_UNKNOWN; private void init(ZenModeInfo prevInfo, ZenModeInfo newInfo, int callingUid, - @ConfigChangeOrigin int origin) { + @ConfigOrigin int origin) { // previous & new may be the same -- that would indicate that zen mode hasn't changed. mPrevZenMode = prevInfo.mZenMode; mNewZenMode = newInfo.mZenMode; @@ -484,7 +484,8 @@ class ZenModeEventLogger { */ boolean getIsUserAction() { if (Flags.modesApi()) { - return mOrigin == ZenModeConfig.UPDATE_ORIGIN_USER; + return mOrigin == ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI + || mOrigin == ZenModeConfig.ORIGIN_USER_IN_APP; } // Approach for pre-MODES_API: @@ -549,10 +550,10 @@ class ZenModeEventLogger { } boolean isFromSystemOrSystemUi() { - return mOrigin == ZenModeConfig.UPDATE_ORIGIN_INIT - || mOrigin == ZenModeConfig.UPDATE_ORIGIN_INIT_USER - || mOrigin == ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI - || mOrigin == ZenModeConfig.UPDATE_ORIGIN_RESTORE_BACKUP; + return mOrigin == ZenModeConfig.ORIGIN_INIT + || mOrigin == ZenModeConfig.ORIGIN_INIT_USER + || mOrigin == ZenModeConfig.ORIGIN_SYSTEM + || mOrigin == ZenModeConfig.ORIGIN_RESTORE_BACKUP; } /** diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java index 95d8bb953065..8c280edf03c0 100644 --- a/services/core/java/com/android/server/notification/ZenModeHelper.java +++ b/services/core/java/com/android/server/notification/ZenModeHelper.java @@ -29,13 +29,14 @@ 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.NotificationServiceProto.ROOT_CONFIG; -import static android.service.notification.ZenModeConfig.UPDATE_ORIGIN_APP; -import static android.service.notification.ZenModeConfig.UPDATE_ORIGIN_INIT; -import static android.service.notification.ZenModeConfig.UPDATE_ORIGIN_INIT_USER; -import static android.service.notification.ZenModeConfig.UPDATE_ORIGIN_RESTORE_BACKUP; -import static android.service.notification.ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI; -import static android.service.notification.ZenModeConfig.UPDATE_ORIGIN_UNKNOWN; -import static android.service.notification.ZenModeConfig.UPDATE_ORIGIN_USER; +import static android.service.notification.ZenModeConfig.ORIGIN_APP; +import static android.service.notification.ZenModeConfig.ORIGIN_INIT; +import static android.service.notification.ZenModeConfig.ORIGIN_INIT_USER; +import static android.service.notification.ZenModeConfig.ORIGIN_RESTORE_BACKUP; +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 com.android.internal.util.FrameworkStatsLog.DND_MODE_RULE; import static com.android.internal.util.Preconditions.checkArgument; @@ -95,7 +96,7 @@ import android.service.notification.SystemZenRules; import android.service.notification.ZenAdapters; import android.service.notification.ZenDeviceEffects; import android.service.notification.ZenModeConfig; -import android.service.notification.ZenModeConfig.ConfigChangeOrigin; +import android.service.notification.ZenModeConfig.ConfigOrigin; import android.service.notification.ZenModeConfig.ZenRule; import android.service.notification.ZenModeProto; import android.service.notification.ZenPolicy; @@ -294,7 +295,7 @@ public class ZenModeHelper { // "update" config to itself, which will have no effect in the case where a config // was read in via XML, but will initialize zen mode if nothing was read in and the // config remains the default. - updateConfigAndZenModeLocked(mConfig, UPDATE_ORIGIN_INIT, "init", + updateConfigAndZenModeLocked(mConfig, ORIGIN_INIT, "init", true /*setRingerMode*/, Process.SYSTEM_UID /* callingUid */); } } @@ -328,7 +329,7 @@ public class ZenModeHelper { } mDeviceEffectsApplier = deviceEffectsApplier; } - applyConsolidatedDeviceEffects(UPDATE_ORIGIN_INIT); + applyConsolidatedDeviceEffects(ORIGIN_INIT); } public void onUserSwitched(int user) { @@ -368,7 +369,7 @@ public class ZenModeHelper { config.user = user; } synchronized (mConfigLock) { - setConfigLocked(config, null, UPDATE_ORIGIN_INIT_USER, reason, + setConfigLocked(config, null, ORIGIN_INIT_USER, reason, Process.SYSTEM_UID); } cleanUpZenRules(); @@ -384,7 +385,7 @@ public class ZenModeHelper { final int newZen = NotificationManager.zenModeFromInterruptionFilter(filter, -1); if (newZen != -1) { setManualZenMode(newZen, null, - fromSystemOrSystemUi ? UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI : UPDATE_ORIGIN_APP, + fromSystemOrSystemUi ? ORIGIN_SYSTEM : ORIGIN_APP, /* reason= */ "listener:" + (name != null ? name.flattenToShortString() : null), /* caller= */ name != null ? name.getPackageName() : null, callingUid); @@ -447,8 +448,8 @@ public class ZenModeHelper { } public String addAutomaticZenRule(String pkg, AutomaticZenRule automaticZenRule, - @ConfigChangeOrigin int origin, String reason, int callingUid) { - requirePublicOrigin("addAutomaticZenRule", origin); + @ConfigOrigin int origin, String reason, int callingUid) { + checkManageRuleOrigin("addAutomaticZenRule", origin); if (!ZenModeConfig.SYSTEM_AUTHORITY.equals(pkg)) { PackageItemInfo component = getServiceInfo(automaticZenRule.getOwner()); if (component == null) { @@ -497,7 +498,7 @@ public class ZenModeHelper { @GuardedBy("mConfigLock") private ZenRule maybeRestoreRemovedRule(ZenModeConfig config, ZenRule ruleToAdd, - AutomaticZenRule azrToAdd, @ConfigChangeOrigin int origin) { + AutomaticZenRule azrToAdd, @ConfigOrigin int origin) { if (!Flags.modesApi()) { return ruleToAdd; } @@ -517,7 +518,7 @@ public class ZenModeHelper { config.deletedRules.remove(deletedKey); ruleToRestore.deletionInstant = null; - if (origin != UPDATE_ORIGIN_APP) { + if (origin != ORIGIN_APP) { return ruleToAdd; // Okay to create anew. } @@ -547,8 +548,8 @@ public class ZenModeHelper { } public boolean updateAutomaticZenRule(String ruleId, AutomaticZenRule automaticZenRule, - @ConfigChangeOrigin int origin, String reason, int callingUid) { - requirePublicOrigin("updateAutomaticZenRule", origin); + @ConfigOrigin int origin, String reason, int callingUid) { + checkManageRuleOrigin("updateAutomaticZenRule", origin); if (ruleId == null) { throw new IllegalArgumentException("ruleId cannot be null"); } @@ -621,7 +622,7 @@ public class ZenModeHelper { mContext.getString(R.string.zen_mode_implicit_deactivated), STATE_FALSE); setAutomaticZenRuleStateLocked(newConfig, Collections.singletonList(rule), - deactivated, UPDATE_ORIGIN_APP, callingUid); + deactivated, ORIGIN_APP, callingUid); } } else { // Either create a new rule with a default ZenPolicy, or update an existing rule's @@ -647,7 +648,7 @@ public class ZenModeHelper { mContext.getString(R.string.zen_mode_implicit_activated), STATE_TRUE); - setConfigLocked(newConfig, /* triggeringComponent= */ null, UPDATE_ORIGIN_APP, + setConfigLocked(newConfig, /* triggeringComponent= */ null, ORIGIN_APP, "applyGlobalZenModeAsImplicitZenRule", callingUid); } } @@ -701,7 +702,7 @@ public class ZenModeHelper { /* updateBitmask= */ false, isNew); - setConfigLocked(newConfig, /* triggeringComponent= */ null, UPDATE_ORIGIN_APP, + setConfigLocked(newConfig, /* triggeringComponent= */ null, ORIGIN_APP, "applyGlobalPolicyAsImplicitZenRule", callingUid); } } @@ -788,9 +789,9 @@ public class ZenModeHelper { return ruleId.startsWith(IMPLICIT_RULE_ID_PREFIX); } - boolean removeAutomaticZenRule(String id, @ConfigChangeOrigin int origin, String reason, + boolean removeAutomaticZenRule(String id, @ConfigOrigin int origin, String reason, int callingUid) { - requirePublicOrigin("removeAutomaticZenRule", origin); + checkManageRuleOrigin("removeAutomaticZenRule", origin); ZenModeConfig newConfig; synchronized (mConfigLock) { if (mConfig == null) return false; @@ -821,9 +822,9 @@ public class ZenModeHelper { } } - boolean removeAutomaticZenRules(String packageName, @ConfigChangeOrigin int origin, + boolean removeAutomaticZenRules(String packageName, @ConfigOrigin int origin, String reason, int callingUid) { - requirePublicOrigin("removeAutomaticZenRules", origin); + checkManageRuleOrigin("removeAutomaticZenRules", origin); ZenModeConfig newConfig; synchronized (mConfigLock) { if (mConfig == null) return false; @@ -837,7 +838,7 @@ public class ZenModeHelper { } // If the system is clearing all rules this means DND access is revoked or the package // was uninstalled, so also clear the preserved-deleted rules. - if (origin == UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI) { + if (origin == ORIGIN_SYSTEM) { for (int i = newConfig.deletedRules.size() - 1; i >= 0; i--) { ZenRule rule = newConfig.deletedRules.get(newConfig.deletedRules.keyAt(i)); if (Objects.equals(rule.getPkg(), packageName)) { @@ -850,7 +851,7 @@ public class ZenModeHelper { } private void maybePreserveRemovedRule(ZenModeConfig config, ZenRule ruleToRemove, - @ConfigChangeOrigin int origin) { + @ConfigOrigin int origin) { if (!Flags.modesApi()) { return; } @@ -859,7 +860,7 @@ public class ZenModeHelper { // We don't try to preserve system-owned rules because their conditionIds (used as // deletedRuleKey) are not stable. This is almost moot anyway because an app cannot // delete a system-owned rule. - if (origin == UPDATE_ORIGIN_APP && !ruleToRemove.canBeUpdatedByApp() + if (origin == ORIGIN_APP && !ruleToRemove.canBeUpdatedByApp() && !PACKAGE_ANDROID.equals(ruleToRemove.pkg)) { String deletedKey = ZenModeConfig.deletedRuleKey(ruleToRemove); if (deletedKey != null) { @@ -888,9 +889,9 @@ public class ZenModeHelper { } } - void setAutomaticZenRuleState(String id, Condition condition, @ConfigChangeOrigin int origin, + void setAutomaticZenRuleState(String id, Condition condition, @ConfigOrigin int origin, int callingUid) { - requirePublicOrigin("setAutomaticZenRuleState", origin); + checkSetRuleStateOrigin("setAutomaticZenRuleState(String id)", origin); ZenModeConfig newConfig; synchronized (mConfigLock) { if (mConfig == null) return; @@ -911,8 +912,8 @@ public class ZenModeHelper { } void setAutomaticZenRuleState(Uri ruleDefinition, Condition condition, - @ConfigChangeOrigin int origin, int callingUid) { - requirePublicOrigin("setAutomaticZenRuleState", origin); + @ConfigOrigin int origin, int callingUid) { + checkSetRuleStateOrigin("setAutomaticZenRuleState(Uri ruleDefinition)", origin); ZenModeConfig newConfig; synchronized (mConfigLock) { if (mConfig == null) return; @@ -932,11 +933,13 @@ public class ZenModeHelper { @GuardedBy("mConfigLock") private void setAutomaticZenRuleStateLocked(ZenModeConfig config, List<ZenRule> rules, - Condition condition, @ConfigChangeOrigin int origin, int callingUid) { + Condition condition, @ConfigOrigin int origin, int callingUid) { if (rules == null || rules.isEmpty()) return; - if (Flags.modesApi() && condition.source == SOURCE_USER_ACTION) { - origin = UPDATE_ORIGIN_USER; // Although coming from app, it's actually a user action. + if (!Flags.modesUi()) { + if (Flags.modesApi() && condition.source == SOURCE_USER_ACTION) { + origin = ORIGIN_USER_IN_APP; // Although coming from app, it's actually from user. + } } for (ZenRule rule : rules) { @@ -1062,7 +1065,7 @@ public class ZenModeHelper { } } if (updated) { - setConfigLocked(config, null, UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, + setConfigLocked(config, null, ORIGIN_SYSTEM, "updateZenRulesOnLocaleChange", Process.SYSTEM_UID); } } @@ -1119,11 +1122,11 @@ public class ZenModeHelper { * </ul> * * <p>The rule's {@link ZenRule#condition} is cleared (meaning that an active rule will be - * deactivated) unless the update has origin == {@link ZenModeConfig#UPDATE_ORIGIN_USER}. + * deactivated) unless the update has origin == {@link ZenModeConfig#ORIGIN_USER_IN_SYSTEMUI}. */ @GuardedBy("mConfigLock") private boolean populateZenRule(String pkg, AutomaticZenRule azr, ZenRule rule, - @ConfigChangeOrigin int origin, boolean isNew) { + @ConfigOrigin int origin, boolean isNew) { if (Flags.modesApi()) { boolean modified = false; // These values can always be edited by the app, so we apply changes immediately. @@ -1137,7 +1140,7 @@ public class ZenModeHelper { // Allow updating the CPS backing system rules (e.g. for custom manual -> schedule) if (Flags.modesUi() - && (origin == UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI || origin == UPDATE_ORIGIN_USER) + && (origin == ORIGIN_SYSTEM || origin == ORIGIN_USER_IN_SYSTEMUI) && Objects.equals(rule.pkg, SystemZenRules.PACKAGE_ANDROID) && !Objects.equals(rule.component, azr.getOwner())) { rule.component = azr.getOwner(); @@ -1151,7 +1154,7 @@ public class ZenModeHelper { rule.disabledOrigin = origin; } else if (azr.isEnabled()) { // Enabling or previously enabled. Clear disabler. - rule.disabledOrigin = UPDATE_ORIGIN_UNKNOWN; + rule.disabledOrigin = ORIGIN_UNKNOWN; } } @@ -1166,7 +1169,7 @@ public class ZenModeHelper { Flags.modesApi() && (Flags.modesUi() || isWatch) && !isNew - && origin == UPDATE_ORIGIN_USER + && origin == ORIGIN_USER_IN_SYSTEMUI && rule.enabled == azr.isEnabled() && rule.conditionId != null && rule.condition != null @@ -1232,7 +1235,7 @@ public class ZenModeHelper { } // Updates the bitmasks if the origin of the change is the user. - boolean updateBitmask = (origin == UPDATE_ORIGIN_USER); + boolean updateBitmask = (origin == ORIGIN_USER_IN_SYSTEMUI); if (updateBitmask && !TextUtils.equals(previousName, azr.getName())) { rule.userModifiedFields |= AutomaticZenRule.FIELD_NAME; @@ -1263,7 +1266,7 @@ public class ZenModeHelper { // Updates the bitmask and values for all device effect fields, based on the origin. modified |= updateZenDeviceEffects(rule, azr.getDeviceEffects(), - origin == UPDATE_ORIGIN_APP, updateBitmask); + origin == ORIGIN_APP, updateBitmask); return modified; } else { @@ -1297,8 +1300,8 @@ public class ZenModeHelper { * change. (Note that regardless of origin, fields can always be updated if they're not already * user modified.) */ - private static boolean doesOriginAlwaysUpdateValues(@ConfigChangeOrigin int origin) { - return origin == UPDATE_ORIGIN_USER || origin == UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI; + private static boolean doesOriginAlwaysUpdateValues(@ConfigOrigin int origin) { + return origin == ORIGIN_USER_IN_SYSTEMUI || origin == ORIGIN_SYSTEM; } /** @@ -1539,7 +1542,7 @@ public class ZenModeHelper { : AUTOMATIC_RULE_STATUS_DISABLED); } - void setManualZenMode(int zenMode, Uri conditionId, @ConfigChangeOrigin int origin, + void setManualZenMode(int zenMode, Uri conditionId, @ConfigOrigin int origin, String reason, @Nullable String caller, int callingUid) { setManualZenMode(zenMode, conditionId, origin, reason, caller, true /*setRingerMode*/, callingUid); @@ -1547,7 +1550,7 @@ public class ZenModeHelper { Settings.Secure.SHOW_ZEN_SETTINGS_SUGGESTION, 0); } - private void setManualZenMode(int zenMode, Uri conditionId, @ConfigChangeOrigin int origin, + private void setManualZenMode(int zenMode, Uri conditionId, @ConfigOrigin int origin, String reason, @Nullable String caller, boolean setRingerMode, int callingUid) { ZenModeConfig newConfig; synchronized (mConfigLock) { @@ -1564,8 +1567,8 @@ public class ZenModeHelper { newConfig.manualRule.zenMode = zenMode; newConfig.manualRule.condition = new Condition(newConfig.manualRule.conditionId, "", zenMode == Global.ZEN_MODE_OFF ? STATE_FALSE : STATE_TRUE, - origin == UPDATE_ORIGIN_USER ? SOURCE_USER_ACTION : SOURCE_UNKNOWN); - if (zenMode == Global.ZEN_MODE_OFF && origin != UPDATE_ORIGIN_USER) { + origin == ORIGIN_USER_IN_SYSTEMUI ? SOURCE_USER_ACTION : SOURCE_UNKNOWN); + if (zenMode == Global.ZEN_MODE_OFF && origin != ORIGIN_USER_IN_SYSTEMUI) { // 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()) { @@ -1602,7 +1605,7 @@ public class ZenModeHelper { } public void setManualZenRuleDeviceEffects(ZenDeviceEffects deviceEffects, - @ConfigChangeOrigin int origin, String reason, int callingUid) { + @ConfigOrigin int origin, String reason, int callingUid) { if (!Flags.modesUi()) { return; } @@ -1673,7 +1676,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 } } @@ -1752,7 +1759,7 @@ public class ZenModeHelper { if (DEBUG) Log.d(TAG, reason); synchronized (mConfigLock) { setConfigLocked(config, null, - forRestore ? UPDATE_ORIGIN_RESTORE_BACKUP : UPDATE_ORIGIN_INIT, reason, + forRestore ? ORIGIN_RESTORE_BACKUP : ORIGIN_INIT, reason, Process.SYSTEM_UID); } } @@ -1787,7 +1794,7 @@ public class ZenModeHelper { /** * Sets the global notification policy used for priority only do not disturb */ - public void setNotificationPolicy(Policy policy, @ConfigChangeOrigin int origin, + public void setNotificationPolicy(Policy policy, @ConfigOrigin int origin, int callingUid) { synchronized (mConfigLock) { if (policy == null || mConfig == null) return; @@ -1843,7 +1850,7 @@ public class ZenModeHelper { } if (!newConfig.equals(mConfig)) { - setConfigLocked(newConfig, null, UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, + setConfigLocked(newConfig, null, ORIGIN_SYSTEM, "cleanUpZenRules", Process.SYSTEM_UID); } } @@ -1893,20 +1900,20 @@ public class ZenModeHelper { @GuardedBy("mConfigLock") private boolean setConfigLocked(ZenModeConfig config, ComponentName triggeringComponent, - @ConfigChangeOrigin int origin, String reason, int callingUid) { + @ConfigOrigin int origin, String reason, int callingUid) { return setConfigLocked(config, origin, reason, triggeringComponent, true /*setRingerMode*/, callingUid); } void setConfig(ZenModeConfig config, ComponentName triggeringComponent, - @ConfigChangeOrigin int origin, String reason, int callingUid) { + @ConfigOrigin int origin, String reason, int callingUid) { synchronized (mConfigLock) { setConfigLocked(config, triggeringComponent, origin, reason, callingUid); } } @GuardedBy("mConfigLock") - private boolean setConfigLocked(ZenModeConfig config, @ConfigChangeOrigin int origin, + private boolean setConfigLocked(ZenModeConfig config, @ConfigOrigin int origin, String reason, ComponentName triggeringComponent, boolean setRingerMode, int callingUid) { final long identity = Binder.clearCallingIdentity(); @@ -1954,7 +1961,7 @@ public class ZenModeHelper { * If logging is enabled, will also request logging of the outcome of this change if needed. */ @GuardedBy("mConfigLock") - private void updateConfigAndZenModeLocked(ZenModeConfig config, @ConfigChangeOrigin int origin, + private void updateConfigAndZenModeLocked(ZenModeConfig config, @ConfigOrigin int origin, String reason, boolean setRingerMode, int callingUid) { final boolean logZenModeEvents = mFlagResolver.isEnabled( SystemUiSystemPropertiesFlags.NotificationFlags.LOG_DND_STATE_EVENTS); @@ -1963,7 +1970,7 @@ public class ZenModeHelper { mZenMode, mConfig, mConsolidatedPolicy); if (!config.equals(mConfig)) { // Schedule broadcasts. Cannot be sent during boot, though. - if (Flags.modesApi() && origin != UPDATE_ORIGIN_INIT) { + if (Flags.modesApi() && origin != ORIGIN_INIT) { for (ZenRule rule : config.automaticRules.values()) { ZenRule original = mConfig.automaticRules.get(rule.id); if (original != null) { @@ -2020,7 +2027,7 @@ public class ZenModeHelper { @VisibleForTesting @GuardedBy("mConfigLock") - protected void evaluateZenModeLocked(@ConfigChangeOrigin int origin, String reason, + protected void evaluateZenModeLocked(@ConfigOrigin int origin, String reason, boolean setRingerMode) { if (DEBUG) Log.d(TAG, "evaluateZenMode"); if (mConfig == null) return; @@ -2111,7 +2118,7 @@ public class ZenModeHelper { } @GuardedBy("mConfigLock") - private void updateAndApplyConsolidatedPolicyAndDeviceEffects(@ConfigChangeOrigin int origin, + private void updateAndApplyConsolidatedPolicyAndDeviceEffects(@ConfigOrigin int origin, String reason) { synchronized (mConfigLock) { if (mConfig == null) return; @@ -2163,7 +2170,7 @@ public class ZenModeHelper { } } - private void applyConsolidatedDeviceEffects(@ConfigChangeOrigin int source) { + private void applyConsolidatedDeviceEffects(@ConfigOrigin int source) { if (!Flags.modesApi()) { return; } @@ -2506,7 +2513,7 @@ public class ZenModeHelper { } if (newZen != -1) { - setManualZenMode(newZen, null, UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, + setManualZenMode(newZen, null, ORIGIN_SYSTEM, "ringerModeInternal", /* caller= */ null, /* setRingerMode= */ false, Process.SYSTEM_UID); } @@ -2551,7 +2558,7 @@ public class ZenModeHelper { break; } if (newZen != -1) { - setManualZenMode(newZen, null, UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, + setManualZenMode(newZen, null, ORIGIN_SYSTEM, "ringerModeExternal", caller, false /*setRingerMode*/, Process.SYSTEM_UID); } @@ -2708,15 +2715,33 @@ public class ZenModeHelper { } } - /** Checks that the {@code origin} supplied to a ZenModeHelper "API" method makes sense. */ - private static void requirePublicOrigin(String method, @ConfigChangeOrigin int origin) { + /** + * Checks that the {@code origin} supplied to ZenModeHelper rule-management API methods + * ({@link #addAutomaticZenRule}, {@link #removeAutomaticZenRule}, etc, makes sense. + */ + private static void checkManageRuleOrigin(String method, @ConfigOrigin int origin) { + if (!Flags.modesApi()) { + return; + } + checkArgument(origin == ORIGIN_APP || origin == ORIGIN_SYSTEM + || origin == ORIGIN_USER_IN_SYSTEMUI, + "Expected one of ORIGIN_APP, ORIGIN_SYSTEM, or " + + "ORIGIN_USER_IN_SYSTEMUI for %s, but received '%s'.", + method, origin); + } + + /** + * Checks that the {@code origin} supplied to {@link #setAutomaticZenRuleState} overloads makes + * sense. + */ + private static void checkSetRuleStateOrigin(String method, @ConfigOrigin int origin) { if (!Flags.modesApi()) { return; } - checkArgument(origin == UPDATE_ORIGIN_APP || origin == UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI - || origin == UPDATE_ORIGIN_USER, - "Expected one of UPDATE_ORIGIN_APP, UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, or " - + "UPDATE_ORIGIN_USER for %s, but received '%s'.", + checkArgument(origin == ORIGIN_APP || origin == ORIGIN_USER_IN_APP + || origin == ORIGIN_SYSTEM || origin == ORIGIN_USER_IN_SYSTEMUI, + "Expected one of ORIGIN_APP, ORIGIN_USER_IN_APP, ORIGIN_SYSTEM, or " + + "ORIGIN_USER_IN_SYSTEMUI for %s, but received '%s'.", method, origin); } @@ -2841,7 +2866,7 @@ public class ZenModeHelper { } } - private void postApplyDeviceEffects(@ConfigChangeOrigin int origin) { + private void postApplyDeviceEffects(@ConfigOrigin int origin) { removeMessages(MSG_APPLY_EFFECTS); sendMessage(obtainMessage(MSG_APPLY_EFFECTS, origin, 0)); } @@ -2862,7 +2887,7 @@ public class ZenModeHelper { updateRingerAndAudio(/* shouldApplyToRinger= */ false); break; case MSG_APPLY_EFFECTS: - @ConfigChangeOrigin int origin = msg.arg1; + @ConfigOrigin int origin = msg.arg1; applyConsolidatedDeviceEffects(origin); break; } 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/AppDataHelper.java b/services/core/java/com/android/server/pm/AppDataHelper.java index 8b72138898ad..29f8243cfe60 100644 --- a/services/core/java/com/android/server/pm/AppDataHelper.java +++ b/services/core/java/com/android/server/pm/AppDataHelper.java @@ -127,7 +127,7 @@ public class AppDataHelper { } // TODO(b/211761016): should we still create the profile dirs? - if (ps.getPkg() != null && !shouldHaveAppStorage(ps.getPkg())) { + if (!shouldHaveAppStorage(ps)) { Slog.w(TAG, "Skipping preparing app data for " + ps.getPackageName()); return; } @@ -172,19 +172,19 @@ public class AppDataHelper { } private void prepareAppDataAndMigrate(@NonNull Installer.Batch batch, - @NonNull AndroidPackage pkg, @UserIdInt int userId, + @NonNull PackageStateInternal psi, @UserIdInt int userId, @StorageManager.StorageFlags int flags, boolean maybeMigrateAppData) { - if (pkg == null) { + if (psi == null || psi.getPkg() == null) { Slog.wtf(TAG, "Package was null!", new Throwable()); return; } - if (!shouldHaveAppStorage(pkg)) { - Slog.w(TAG, "Skipping preparing app data for " + pkg.getPackageName()); + if (!shouldHaveAppStorage(psi)) { + Slog.w(TAG, "Skipping preparing app data for " + psi.getPackageName()); return; } final PackageSetting ps; synchronized (mPm.mLock) { - ps = mPm.mSettings.getPackageLPr(pkg.getPackageName()); + ps = mPm.mSettings.getPackageLPr(psi.getPackageName()); } prepareAppData(batch, ps, Process.INVALID_UID, userId, flags).thenRun(() -> { // Note: this code block is executed with the Installer lock @@ -449,7 +449,7 @@ public class AppDataHelper { } if (ps.getUserStateOrDefault(userId).isInstalled()) { - prepareAppDataAndMigrate(batch, ps.getPkg(), userId, flags, migrateAppData); + prepareAppDataAndMigrate(batch, ps, userId, flags, migrateAppData); preparedCount++; } } @@ -484,8 +484,7 @@ public class AppDataHelper { + " or was deleted without DELETE_KEEP_DATA", PackageManagerException.INTERNAL_ERROR_STORAGE_INVALID_NOT_INSTALLED_FOR_USER); } - if (packageState.getPkg() != null - && !shouldHaveAppStorage(packageState.getPkg())) { + if (!shouldHaveAppStorage(packageState)) { throw PackageManagerException.ofInternalError( "Package " + packageName + " shouldn't have storage", PackageManagerException.INTERNAL_ERROR_STORAGE_INVALID_SHOULD_NOT_HAVE_STORAGE); @@ -535,8 +534,7 @@ public class AppDataHelper { if (packageStateInternal != null && packageStateInternal.getUserStateOrDefault( UserHandle.USER_SYSTEM).isInstalled()) { - AndroidPackage pkg = packageStateInternal.getPkg(); - prepareAppDataAndMigrate(batch, pkg, + prepareAppDataAndMigrate(batch, packageStateInternal, UserHandle.USER_SYSTEM, storageFlags, true /* maybeMigrateAppData */); count++; } @@ -637,12 +635,16 @@ public class AppDataHelper { } /** - * Returns {@code true} if app's internal storage should be created for this {@code pkg}. + * Returns {@code true} if app's internal storage should be created for this {@code ps}. */ - private boolean shouldHaveAppStorage(AndroidPackage pkg) { + private boolean shouldHaveAppStorage(PackageStateInternal ps) { + if (ps.getPkg() == null) { + // Keeps the legacy behavior + return true; + } PackageManager.Property noAppDataProp = - pkg.getProperties().get(PackageManager.PROPERTY_NO_APP_DATA_STORAGE); - return (noAppDataProp == null || !noAppDataProp.getBoolean()) && pkg.getUid() >= 0; + ps.getPkg().getProperties().get(PackageManager.PROPERTY_NO_APP_DATA_STORAGE); + return (noAppDataProp == null || !noAppDataProp.getBoolean()) && ps.getAppId() >= 0; } /** diff --git a/services/core/java/com/android/server/pm/DeletePackageHelper.java b/services/core/java/com/android/server/pm/DeletePackageHelper.java index b56e119d0a19..8398ffc75f0d 100644 --- a/services/core/java/com/android/server/pm/DeletePackageHelper.java +++ b/services/core/java/com/android/server/pm/DeletePackageHelper.java @@ -690,7 +690,13 @@ final class DeletePackageHelper { public void deletePackageVersionedInternal(VersionedPackage versionedPackage, final IPackageDeleteObserver2 observer, final int userId, final int deleteFlags, final boolean allowSilentUninstall) { - final int callingUid = Binder.getCallingUid(); + deletePackageVersionedInternal(versionedPackage, observer, userId, deleteFlags, + Binder.getCallingUid(), allowSilentUninstall); + } + + public void deletePackageVersionedInternal(VersionedPackage versionedPackage, + final IPackageDeleteObserver2 observer, final int userId, final int deleteFlags, + final int callingUid, final boolean allowSilentUninstall) { mPm.mContext.enforceCallingOrSelfPermission( android.Manifest.permission.DELETE_PACKAGES, null); final Computer snapshot = mPm.snapshotComputer(); @@ -720,16 +726,22 @@ final class DeletePackageHelper { final String internalPackageName = snapshot.resolveInternalPackageName(packageName, versionCode); - final int uid = Binder.getCallingUid(); if (!isOrphaned(snapshot, internalPackageName) && !allowSilentUninstall && !isCallerAllowedToSilentlyUninstall( - snapshot, uid, internalPackageName, userId)) { + snapshot, callingUid, internalPackageName, userId)) { mPm.mHandler.post(() -> { try { final Intent intent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE); intent.setData(Uri.fromParts(PACKAGE_SCHEME, packageName, null)); - intent.putExtra(PackageInstaller.EXTRA_CALLBACK, observer.asBinder()); + intent.putExtra(PackageInstaller.EXTRA_CALLBACK, + new PackageManager.UninstallCompleteCallback(observer.asBinder())); + if ((deleteFlags & PackageManager.DELETE_ARCHIVE) != 0) { + // Delete flags are passed to the uninstaller activity so it can be + // preserved in the follow-up uninstall operation after the user + // confirmation + intent.putExtra(PackageInstaller.EXTRA_DELETE_FLAGS, deleteFlags); + } observer.onUserActionRequired(intent); } catch (RemoteException re) { } @@ -738,7 +750,7 @@ final class DeletePackageHelper { } final boolean deleteAllUsers = (deleteFlags & PackageManager.DELETE_ALL_USERS) != 0; final int[] users = deleteAllUsers ? mUserManagerInternal.getUserIds() : new int[]{userId}; - if (UserHandle.getUserId(uid) != userId || (deleteAllUsers && users.length > 1)) { + if (UserHandle.getUserId(callingUid) != userId || (deleteAllUsers && users.length > 1)) { mPm.mContext.enforceCallingOrSelfPermission( android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, "deletePackage for user " + userId); diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index a0d5ea875abf..98e3e24c36b9 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -1801,26 +1801,37 @@ final class InstallPackageHelper { oldPackageState.getRestrictUpdateHash()); } - if (oldPackage != null) { - // APK should not change its sharedUserId declarations - final var oldSharedUid = oldPackage.getSharedUserId() != null - ? oldPackage.getSharedUserId() : "<nothing>"; - final var newSharedUid = parsedPackage.getSharedUserId() != null - ? parsedPackage.getSharedUserId() : "<nothing>"; - if (!oldSharedUid.equals(newSharedUid)) { - throw new PrepareFailure(INSTALL_FAILED_UID_CHANGED, - "Package " + parsedPackage.getPackageName() - + " shared user changed from " - + oldSharedUid + " to " + newSharedUid); + // APK should not change its sharedUserId declarations + final String oldSharedUid; + if (mPm.mSettings.getSharedUserSettingLPr(oldPackageState) != null) { + oldSharedUid = mPm.mSettings.getSharedUserSettingLPr(oldPackageState).name; + } else { + oldSharedUid = "<nothing>"; + } + String newSharedUid = parsedPackage.getSharedUserId() != null + ? parsedPackage.getSharedUserId() : "<nothing>"; + // If the previously installed app version doesn't have sharedUserSetting, + // check that the new apk either doesn't have sharedUserId or it is leaving one. + // If it contains sharedUserId but it is also leaving it, it's ok to proceed. + if (oldSharedUid.equals("<nothing>")) { + if (parsedPackage.isLeavingSharedUser()) { + newSharedUid = "<nothing>"; } + } - // APK should not re-join shared UID - if (oldPackage.isLeavingSharedUser() - && !parsedPackage.isLeavingSharedUser()) { - throw new PrepareFailure(INSTALL_FAILED_UID_CHANGED, - "Package " + parsedPackage.getPackageName() - + " attempting to rejoin " + newSharedUid); - } + if (!oldSharedUid.equals(newSharedUid)) { + throw new PrepareFailure(INSTALL_FAILED_UID_CHANGED, + "Package " + parsedPackage.getPackageName() + + " shared user changed from " + + oldSharedUid + " to " + newSharedUid); + } + + // APK should not re-join shared UID + if (oldPackageState.isLeavingSharedUser() + && !parsedPackage.isLeavingSharedUser()) { + throw new PrepareFailure(INSTALL_FAILED_UID_CHANGED, + "Package " + parsedPackage.getPackageName() + + " attempting to rejoin " + newSharedUid); } // In case of rollback, remember per-user/profile install state diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java index 7156795ba771..be6fa14952c8 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerService.java +++ b/services/core/java/com/android/server/pm/PackageInstallerService.java @@ -1438,7 +1438,8 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements if (mContext.checkPermission(Manifest.permission.DELETE_PACKAGES, callingPid, callingUid) == PackageManager.PERMISSION_GRANTED) { // Sweet, call straight through! - mPm.deletePackageVersioned(versionedPackage, adapter.getBinder(), userId, flags); + mPm.deletePackageVersioned(versionedPackage, adapter.getBinder(), userId, flags, + callingUid); } else if (canSilentlyInstallPackage) { // Allow the device owner and affiliated profile owner to silently delete packages // Need to clear the calling identity to get DELETE_PACKAGES permission diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 33ca8a8e0f9e..20859da4dd56 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -3323,9 +3323,17 @@ public class PackageManagerService implements PackageSender, TestUtilityService } public void deletePackageVersioned(VersionedPackage versionedPackage, + final IPackageDeleteObserver2 observer, final int userId, final int deleteFlags, + final int callingUid) { + mDeletePackageHelper.deletePackageVersionedInternal( + versionedPackage, observer, userId, deleteFlags, callingUid, + /* allowSilentUninstall= */ false); + } + + public void deletePackageVersioned(VersionedPackage versionedPackage, final IPackageDeleteObserver2 observer, final int userId, final int deleteFlags) { mDeletePackageHelper.deletePackageVersionedInternal( - versionedPackage, observer, userId, deleteFlags, false); + versionedPackage, observer, userId, deleteFlags, /* allowSilentUninstall= */ false); } boolean isCallerVerifier(@NonNull Computer snapshot, int callingUid) { 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/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index c902fb26495a..57d7d79b2392 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -29,6 +29,7 @@ import static android.os.UserManager.DEV_CREATE_OVERRIDE_PROPERTY; import static android.os.UserManager.DISALLOW_USER_SWITCH; import static android.os.UserManager.SYSTEM_USER_MODE_EMULATION_PROPERTY; import static android.os.UserManager.USER_OPERATION_ERROR_UNKNOWN; +import static android.os.UserManager.USER_OPERATION_ERROR_USER_RESTRICTED; import static android.os.UserManager.USER_TYPE_PROFILE_PRIVATE; import static com.android.internal.app.SetScreenLockDialogActivity.EXTRA_ORIGIN_USER_ID; @@ -2104,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) { @@ -2132,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) { @@ -4064,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 { @@ -5442,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); } @@ -8039,8 +8075,13 @@ public class UserManagerService extends IUserManager.Stub { String errorMessage = (message != null ? (message + ": ") : "") + restriction + " is enabled."; Slog.w(LOG_TAG, errorMessage); - throw new UserManager.CheckedUserOperationException(errorMessage, + if (android.multiuser.Flags.showDifferentCreationErrorForUnsupportedDevices()) { + throw new UserManager.CheckedUserOperationException(errorMessage, + USER_OPERATION_ERROR_USER_RESTRICTED); + } else { + throw new UserManager.CheckedUserOperationException(errorMessage, USER_OPERATION_ERROR_UNKNOWN); + } } } 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/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/HapticFeedbackVibrationProvider.java b/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java index 3f9da82e3d2e..eccbffb53529 100644 --- a/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java +++ b/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java @@ -22,7 +22,6 @@ import android.os.VibrationAttributes; import android.os.VibrationEffect; import android.os.Vibrator; import android.os.VibratorInfo; -import android.os.vibrator.Flags; import android.util.Slog; import android.util.SparseArray; import android.view.HapticFeedbackConstants; @@ -348,7 +347,7 @@ public final class HapticFeedbackVibrationProvider { predefinedEffectId = VibrationEffect.EFFECT_CLICK; predefinedEffectFallback = true; } - if (Flags.keyboardCategoryEnabled() && mKeyboardVibrationFixedAmplitude > 0) { + if (mKeyboardVibrationFixedAmplitude > 0) { if (mVibratorInfo.isPrimitiveSupported(primitiveId)) { return VibrationEffect.startComposition() .addPrimitive(primitiveId, mKeyboardVibrationFixedAmplitude) @@ -361,10 +360,6 @@ public final class HapticFeedbackVibrationProvider { private VibrationAttributes createKeyboardVibrationAttributes( @HapticFeedbackConstants.PrivateFlags int privFlags) { - // Use touch attribute when the keyboard category is disable. - if (!Flags.keyboardCategoryEnabled()) { - return TOUCH_VIBRATION_ATTRIBUTES; - } // Use touch attribute when the haptic is not apply to IME. if ((privFlags & HapticFeedbackConstants.PRIVATE_FLAG_APPLY_INPUT_METHOD_SETTINGS) == 0) { return TOUCH_VIBRATION_ATTRIBUTES; 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 fb92d609f1cf..f2f5eda7c05a 100644 --- a/services/core/java/com/android/server/vibrator/VibrationSettings.java +++ b/services/core/java/com/android/server/vibrator/VibrationSettings.java @@ -55,7 +55,6 @@ import android.os.VibrationAttributes; import android.os.VibrationEffect; import android.os.Vibrator; import android.os.Vibrator.VibrationIntensity; -import android.os.vibrator.Flags; import android.os.vibrator.VibrationConfig; import android.provider.Settings; import android.util.IndentingPrintWriter; @@ -533,8 +532,7 @@ final class VibrationSettings { return false; } - if (Flags.keyboardCategoryEnabled() - && mVibrationConfig.isKeyboardVibrationSettingsSupported()) { + if (mVibrationConfig.isKeyboardVibrationSettingsSupported()) { int category = callerInfo.attrs.getCategory(); if (usage == USAGE_TOUCH && category == CATEGORY_KEYBOARD) { // Keyboard touch has a different user setting. 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/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index 2f6e7deb5535..18aa9a026675 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -1621,8 +1621,10 @@ class ActivityStarter { currentTop, currentTop.mDisplayContent, false /* deferResume */); } - if (!avoidMoveToFront() && mDoResume && mRootWindowContainer - .hasVisibleWindowAboveButDoesNotOwnNotificationShade(started.launchedFromUid)) { + if (!avoidMoveToFront() && mDoResume + && !mService.getUserManagerInternal().isVisibleBackgroundFullUser(started.mUserId) + && mRootWindowContainer.hasVisibleWindowAboveButDoesNotOwnNotificationShade( + started.launchedFromUid)) { // If the UID launching the activity has a visible window on top of the notification // shade and it's launching an activity that's going to be at the front, we should move // the shade out of the way so the user can see it. We want to avoid the case where the diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index 9f3bbd17bd40..e4cae58e0b81 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -283,6 +283,7 @@ import com.android.server.am.PendingIntentRecord; import com.android.server.am.UserState; import com.android.server.firewall.IntentFirewall; import com.android.server.grammaticalinflection.GrammaticalInflectionManagerInternal; +import com.android.server.pm.UserManagerInternal; import com.android.server.pm.UserManagerService; import com.android.server.policy.PermissionPolicyInternal; import com.android.server.sdksandbox.SdkSandboxManagerLocal; @@ -382,6 +383,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { private PermissionPolicyInternal mPermissionPolicyInternal; private StatusBarManagerInternal mStatusBarManagerInternal; private WallpaperManagerInternal mWallpaperManagerInternal; + private UserManagerInternal mUserManagerInternal; @VisibleForTesting final ActivityTaskManagerInternal mInternal; private PowerManagerInternal mPowerManagerInternal; @@ -5464,6 +5466,13 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { return mWallpaperManagerInternal; } + UserManagerInternal getUserManagerInternal() { + if (mUserManagerInternal == null) { + mUserManagerInternal = LocalServices.getService(UserManagerInternal.class); + } + return mUserManagerInternal; + } + AppWarnings getAppWarningsLocked() { return mAppWarnings; } diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java index 509a060c096d..8ef2693ec327 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java +++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java @@ -2846,6 +2846,11 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { } finally { SaferIntentUtils.DISABLE_ENFORCE_INTENTS_TO_MATCH_INTENT_FILTERS.set(false); synchronized (mService.mGlobalLock) { + // Remove the empty task in case the activity was failed to be launched on the + // task that was restored from Recents. + if (!task.hasChild() && task.shouldRemoveSelfOnLastChildRemoval()) { + task.removeIfPossible("start-from-recents"); + } mService.continueWindowLayout(); } } diff --git a/services/core/java/com/android/server/wm/AppCompatReachabilityOverrides.java b/services/core/java/com/android/server/wm/AppCompatReachabilityOverrides.java index b9bdc325cf98..caff96ba4a9f 100644 --- a/services/core/java/com/android/server/wm/AppCompatReachabilityOverrides.java +++ b/services/core/java/com/android/server/wm/AppCompatReachabilityOverrides.java @@ -35,7 +35,6 @@ import android.annotation.NonNull; import android.content.res.Configuration; import android.graphics.Rect; -import com.android.internal.annotations.VisibleForTesting; import com.android.window.flags.Flags; /** @@ -112,12 +111,10 @@ class AppCompatReachabilityOverrides { : mAppCompatConfiguration.getLetterboxVerticalPositionMultiplier(tabletopMode); } - @VisibleForTesting boolean isHorizontalReachabilityEnabled() { return isHorizontalReachabilityEnabled(mActivityRecord.getParent().getConfiguration()); } - @VisibleForTesting boolean isVerticalReachabilityEnabled() { return isVerticalReachabilityEnabled(mActivityRecord.getParent().getConfiguration()); } diff --git a/services/core/java/com/android/server/wm/AppCompatReachabilityPolicy.java b/services/core/java/com/android/server/wm/AppCompatReachabilityPolicy.java index 90bfddb2095f..c3bf116e227d 100644 --- a/services/core/java/com/android/server/wm/AppCompatReachabilityPolicy.java +++ b/services/core/java/com/android/server/wm/AppCompatReachabilityPolicy.java @@ -31,6 +31,8 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.graphics.Rect; +import com.android.internal.annotations.VisibleForTesting; + import java.util.function.Supplier; /** @@ -43,7 +45,8 @@ class AppCompatReachabilityPolicy { @NonNull private final AppCompatConfiguration mAppCompatConfiguration; @Nullable - private Supplier<Rect> mLetterboxInnerBoundsSupplier; + @VisibleForTesting + Supplier<Rect> mLetterboxInnerBoundsSupplier; AppCompatReachabilityPolicy(@NonNull ActivityRecord activityRecord, @NonNull AppCompatConfiguration appCompatConfiguration) { diff --git a/services/core/java/com/android/server/wm/AppCompatUtils.java b/services/core/java/com/android/server/wm/AppCompatUtils.java index a5db9044ecee..0244d27f4363 100644 --- a/services/core/java/com/android/server/wm/AppCompatUtils.java +++ b/services/core/java/com/android/server/wm/AppCompatUtils.java @@ -115,8 +115,8 @@ class AppCompatUtils { static void fillAppCompatTaskInfo(@NonNull Task task, @NonNull TaskInfo info, @Nullable ActivityRecord top) { final AppCompatTaskInfo appCompatTaskInfo = info.appCompatTaskInfo; - appCompatTaskInfo.cameraCompatTaskInfo.freeformCameraCompatMode = - CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_NONE; + clearAppCompatTaskInfo(appCompatTaskInfo); + if (top == null) { return; } @@ -125,24 +125,28 @@ class AppCompatUtils { final boolean isTopActivityResumed = top.getOrganizedTask() == task && top.isState(RESUMED); final boolean isTopActivityVisible = top.getOrganizedTask() == task && top.isVisible(); // Whether the direct top activity is in size compat mode. - appCompatTaskInfo.topActivityInSizeCompat = isTopActivityVisible && top.inSizeCompatMode(); - if (appCompatTaskInfo.topActivityInSizeCompat + appCompatTaskInfo.setTopActivityInSizeCompat( + isTopActivityVisible && top.inSizeCompatMode()); + if (appCompatTaskInfo.isTopActivityInSizeCompat() && top.mWmService.mAppCompatConfiguration.isTranslucentLetterboxingEnabled()) { // We hide the restart button in case of transparent activities. - appCompatTaskInfo.topActivityInSizeCompat = top.fillsParent(); + appCompatTaskInfo.setTopActivityInSizeCompat(top.fillsParent()); } // Whether the direct top activity is eligible for letterbox education. - appCompatTaskInfo.topActivityEligibleForLetterboxEducation = isTopActivityResumed - && top.isEligibleForLetterboxEducation(); - appCompatTaskInfo.isLetterboxEducationEnabled = top.mLetterboxUiController - .isLetterboxEducationEnabled(); + appCompatTaskInfo.setEligibleForLetterboxEducation( + isTopActivityResumed && top.isEligibleForLetterboxEducation()); + appCompatTaskInfo.setLetterboxEducationEnabled(top.mLetterboxUiController + .isLetterboxEducationEnabled()); + + final AppCompatAspectRatioOverrides aspectRatioOverrides = + top.mAppCompatController.getAppCompatAspectRatioOverrides(); + appCompatTaskInfo.setUserFullscreenOverrideEnabled( + aspectRatioOverrides.shouldApplyUserFullscreenOverride()); + appCompatTaskInfo.setSystemFullscreenOverrideEnabled( + aspectRatioOverrides.isSystemOverrideToFullscreenEnabled()); - appCompatTaskInfo.isUserFullscreenOverrideEnabled = top.mAppCompatController - .getAppCompatAspectRatioOverrides().shouldApplyUserFullscreenOverride(); - appCompatTaskInfo.isSystemFullscreenOverrideEnabled = top.mAppCompatController - .getAppCompatAspectRatioOverrides().isSystemOverrideToFullscreenEnabled(); + appCompatTaskInfo.setIsFromLetterboxDoubleTap(reachabilityOverrides.isFromDoubleTap()); - appCompatTaskInfo.isFromLetterboxDoubleTap = reachabilityOverrides.isFromDoubleTap(); final Rect bounds = top.getBounds(); final Rect appBounds = getAppBounds(top); appCompatTaskInfo.topActivityLetterboxWidth = bounds.width(); @@ -152,16 +156,16 @@ class AppCompatUtils { // We need to consider if letterboxed or pillarboxed. // TODO(b/336807329) Encapsulate reachability logic - appCompatTaskInfo.isLetterboxDoubleTapEnabled = reachabilityOverrides - .isLetterboxDoubleTapEducationEnabled(); - if (appCompatTaskInfo.isLetterboxDoubleTapEnabled) { + appCompatTaskInfo.setLetterboxDoubleTapEnabled(reachabilityOverrides + .isLetterboxDoubleTapEducationEnabled()); + if (appCompatTaskInfo.isLetterboxDoubleTapEnabled()) { if (appCompatTaskInfo.isTopActivityPillarboxed()) { if (reachabilityOverrides.allowHorizontalReachabilityForThinLetterbox()) { // Pillarboxed. appCompatTaskInfo.topActivityLetterboxHorizontalPosition = reachabilityOverrides.getLetterboxPositionForHorizontalReachability(); } else { - appCompatTaskInfo.isLetterboxDoubleTapEnabled = false; + appCompatTaskInfo.setLetterboxDoubleTapEnabled(false); } } else { if (reachabilityOverrides.allowVerticalReachabilityForThinLetterbox()) { @@ -169,15 +173,15 @@ class AppCompatUtils { appCompatTaskInfo.topActivityLetterboxVerticalPosition = reachabilityOverrides.getLetterboxPositionForVerticalReachability(); } else { - appCompatTaskInfo.isLetterboxDoubleTapEnabled = false; + appCompatTaskInfo.setLetterboxDoubleTapEnabled(false); } } } - appCompatTaskInfo.topActivityEligibleForUserAspectRatioButton = - !info.isTopActivityTransparent && !appCompatTaskInfo.topActivityInSizeCompat - && top.mAppCompatController.getAppCompatAspectRatioOverrides() - .shouldEnableUserAspectRatioSettings(); - appCompatTaskInfo.topActivityBoundsLetterboxed = top.areBoundsLetterboxed(); + final boolean eligibleForAspectRatioButton = + !info.isTopActivityTransparent && !appCompatTaskInfo.isTopActivityInSizeCompat() + && aspectRatioOverrides.shouldEnableUserAspectRatioSettings(); + appCompatTaskInfo.setEligibleForUserAspectRatioButton(eligibleForAspectRatioButton); + appCompatTaskInfo.setTopActivityLetterboxed(top.areBoundsLetterboxed()); appCompatTaskInfo.cameraCompatTaskInfo.freeformCameraCompatMode = top.mAppCompatController .getAppCompatCameraOverrides().getFreeformCameraCompatMode(); } @@ -207,4 +211,16 @@ class AppCompatUtils { } return "UNKNOWN_REASON"; } + + private static void clearAppCompatTaskInfo(@NonNull AppCompatTaskInfo info) { + info.topActivityLetterboxVerticalPosition = TaskInfo.PROPERTY_VALUE_UNSET; + info.topActivityLetterboxHorizontalPosition = TaskInfo.PROPERTY_VALUE_UNSET; + info.topActivityLetterboxWidth = TaskInfo.PROPERTY_VALUE_UNSET; + info.topActivityLetterboxHeight = TaskInfo.PROPERTY_VALUE_UNSET; + info.topActivityLetterboxAppHeight = TaskInfo.PROPERTY_VALUE_UNSET; + info.topActivityLetterboxAppWidth = TaskInfo.PROPERTY_VALUE_UNSET; + info.cameraCompatTaskInfo.freeformCameraCompatMode = + CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_NONE; + info.clearTopActivityFlags(); + } } 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 3e55e2d9b25c..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(); @@ -238,7 +245,7 @@ public final class DesktopModeBoundsCalculator { taskInfo.appCompatTaskInfo.topActivityLetterboxAppWidth; final int appLetterboxHeight = taskInfo.appCompatTaskInfo.topActivityLetterboxAppHeight; - if (appCompatTaskInfo.topActivityBoundsLetterboxed) { + if (appCompatTaskInfo.isTopActivityLetterboxed()) { desiredAspectRatio = (float) Math.max(appLetterboxWidth, appLetterboxHeight) / Math.min(appLetterboxWidth, appLetterboxHeight); } else { 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/ImeTargetChangeListener.java b/services/core/java/com/android/server/wm/ImeTargetChangeListener.java deleted file mode 100644 index e94f17c37051..000000000000 --- a/services/core/java/com/android/server/wm/ImeTargetChangeListener.java +++ /dev/null @@ -1,63 +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.server.wm; - -import android.annotation.NonNull; -import android.os.IBinder; -import android.view.WindowManager; - -/** - * Callback the IME targeting window visibility change state for - * {@link com.android.server.inputmethod.InputMethodManagerService} to manage the IME surface - * visibility and z-ordering. - */ -public interface ImeTargetChangeListener { - /** - * Called when a non-IME-focusable overlay window being the IME layering target (e.g. a - * window with {@link android.view.WindowManager.LayoutParams#FLAG_NOT_FOCUSABLE} and - * {@link android.view.WindowManager.LayoutParams#FLAG_ALT_FOCUSABLE_IM} flags) - * has changed its window visibility. - * - * @param overlayWindowToken the window token of the overlay window. - * @param windowType the window type of the overlay window. - * @param visible the visibility of the overlay window, {@code true} means visible - * and {@code false} otherwise. - * @param removed Whether the IME target overlay window has being removed. - * @param displayId display ID where the overlay window exists. - */ - default void onImeTargetOverlayVisibilityChanged(@NonNull IBinder overlayWindowToken, - @WindowManager.LayoutParams.WindowType int windowType, - boolean visible, boolean removed, int displayId) { - } - - /** - * Called when the visibility of IME input target window has changed. - * - * @param imeInputTarget the window token of the IME input target window. - * @param visible the new window visibility made by {@code imeInputTarget}. visible is - * {@code true} when switching to the new visible IME input target - * window and started input, or the same input target relayout to - * visible from invisible. In contrast, visible is {@code false} when - * closing the input target, or the same input target relayout to - * invisible from visible. - * @param removed Whether the IME input target window has being removed. - * @param displayId display ID where the overlay window exists. - */ - default void onImeInputTargetVisibilityChanged(@NonNull IBinder imeInputTarget, boolean visible, - boolean removed, int displayId) { - } -} diff --git a/services/core/java/com/android/server/wm/KeyguardController.java b/services/core/java/com/android/server/wm/KeyguardController.java index 7a0fd3e34fdd..e18ca8552e72 100644 --- a/services/core/java/com/android/server/wm/KeyguardController.java +++ b/services/core/java/com/android/server/wm/KeyguardController.java @@ -39,6 +39,7 @@ import static android.view.WindowManagerPolicyConstants.KEYGUARD_GOING_AWAY_FLAG import static android.view.WindowManagerPolicyConstants.KEYGUARD_GOING_AWAY_FLAG_TO_SHADE; import static android.view.WindowManagerPolicyConstants.KEYGUARD_GOING_AWAY_FLAG_WITH_WALLPAPER; +import static com.android.window.flags.Flags.reduceKeyguardTransitions; import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER; import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM; import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME; @@ -77,6 +78,8 @@ class KeyguardController { private static final int DEFER_WAKE_TRANSITION_TIMEOUT_MS = 5000; + private static final int GOING_AWAY_TIMEOUT_MS = 10500; + private final ActivityTaskSupervisor mTaskSupervisor; private WindowManagerService mWindowManager; @@ -232,6 +235,7 @@ class KeyguardController { dc.mWallpaperController.adjustWallpaperWindows(); dc.executeAppTransition(); } + scheduleGoingAwayTimeout(displayId); } // Update the sleep token first such that ensureActivitiesVisible has correct sleep token @@ -286,6 +290,8 @@ class KeyguardController { mRootWindowContainer.ensureActivitiesVisible(); mRootWindowContainer.addStartingWindowsForVisibleActivities(); mWindowManager.executeAppTransition(); + + scheduleGoingAwayTimeout(displayId); } finally { mService.continueWindowLayout(); Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); @@ -417,31 +423,42 @@ class KeyguardController { final TransitionController tc = mRootWindowContainer.mTransitionController; final KeyguardDisplayState state = getDisplayState(displayId); + final DisplayContent dc = mRootWindowContainer.getDisplayContent(displayId); - final boolean occluded = state.mOccluded; - final boolean performTransition = isKeyguardLocked(displayId); - final boolean executeTransition = performTransition && !tc.isCollecting(); + final boolean locked = isKeyguardLocked(displayId); + final boolean executeTransition = !tc.isShellTransitionsEnabled() + || (locked && !tc.isCollecting() && !reduceKeyguardTransitions()); + + final int transitType, transitFlags, notFlags; + if (state.mOccluded) { + transitType = TRANSIT_KEYGUARD_OCCLUDE; + transitFlags = TRANSIT_FLAG_KEYGUARD_OCCLUDING; + notFlags = TRANSIT_FLAG_KEYGUARD_UNOCCLUDING; + } else { + transitType = TRANSIT_KEYGUARD_UNOCCLUDE; + transitFlags = TRANSIT_FLAG_KEYGUARD_UNOCCLUDING; + notFlags = TRANSIT_FLAG_KEYGUARD_OCCLUDING; + } - mWindowManager.mPolicy.onKeyguardOccludedChangedLw(occluded); + mWindowManager.mPolicy.onKeyguardOccludedChangedLw(state.mOccluded); mService.deferWindowLayout(); try { - if (isKeyguardLocked(displayId)) { - final int type = occluded ? TRANSIT_KEYGUARD_OCCLUDE : TRANSIT_KEYGUARD_UNOCCLUDE; - final int flag = occluded ? TRANSIT_FLAG_KEYGUARD_OCCLUDING - : TRANSIT_FLAG_KEYGUARD_UNOCCLUDING; + if (locked) { if (tc.isShellTransitionsEnabled()) { - final Task trigger = (occluded && topActivity != null) + final Task trigger = (state.mOccluded && topActivity != null) ? topActivity.getRootTask() : null; - Transition transition = tc.requestTransitionIfNeeded(type, flag, trigger, - mRootWindowContainer.getDefaultDisplay()); + tc.requestTransitionIfNeeded(transitType, transitFlags, trigger, dc); + final Transition transition = tc.getCollectingTransition(); + if ((transition.getFlags() & notFlags) != 0 && reduceKeyguardTransitions()) { + transition.removeFlag(notFlags); + } else { + transition.addFlag(transitFlags); + } if (trigger != null) { - if (transition == null) { - transition = tc.getCollectingTransition(); - } transition.collect(trigger); } } else { - mRootWindowContainer.getDefaultDisplay().prepareAppTransition(type, flag); + dc.prepareAppTransition(transitType, transitFlags); } } else { if (tc.inTransition()) { @@ -451,8 +468,8 @@ class KeyguardController { } } updateKeyguardSleepToken(displayId); - if (performTransition && executeTransition) { - mWindowManager.executeAppTransition(); + if (executeTransition) { + dc.executeAppTransition(); } } finally { mService.continueWindowLayout(); @@ -590,6 +607,34 @@ class KeyguardController { } } + /** + * Called when the default display's mKeyguardGoingAway has been left as {@code true} for too + * long. Send an explicit message to the KeyguardService asking it to wrap up. + */ + private final Runnable mGoingAwayTimeout = () -> { + synchronized (mWindowManager.mGlobalLock) { + KeyguardDisplayState state = getDisplayState(DEFAULT_DISPLAY); + if (!state.mKeyguardGoingAway) { + return; + } + state.mKeyguardGoingAway = false; + state.writeEventLog("goingAwayTimeout"); + mWindowManager.mPolicy.startKeyguardExitAnimation(0); + } + }; + + private void scheduleGoingAwayTimeout(int displayId) { + if (displayId != DEFAULT_DISPLAY) { + return; + } + if (getDisplayState(displayId).mKeyguardGoingAway) { + if (!mWindowManager.mH.hasCallbacks(mGoingAwayTimeout)) { + mWindowManager.mH.postDelayed(mGoingAwayTimeout, GOING_AWAY_TIMEOUT_MS); + } + } else { + mWindowManager.mH.removeCallbacks(mGoingAwayTimeout); + } + } /** Represents Keyguard state per individual display. */ private static class KeyguardDisplayState { @@ -709,6 +754,7 @@ class KeyguardController { if (!lastKeyguardGoingAway && mKeyguardGoingAway) { writeEventLog("dismissIfInsecure"); controller.handleDismissInsecureKeyguard(display); + controller.scheduleGoingAwayTimeout(mDisplayId); hasChange = true; } else if (lastOccluded != mOccluded) { controller.handleOccludedChanged(mDisplayId, mTopOccludesActivity); diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java index 38df1b0e0511..4740fc45c6ba 100644 --- a/services/core/java/com/android/server/wm/LetterboxUiController.java +++ b/services/core/java/com/android/server/wm/LetterboxUiController.java @@ -492,6 +492,9 @@ final class LetterboxUiController { return; } + pw.println(prefix + "isTransparentPolicyRunning=" + + mActivityRecord.mAppCompatController.getTransparentPolicy().isRunning()); + boolean areBoundsLetterboxed = mainWin.areAppWindowBoundsLetterboxed(); pw.println(prefix + "areBoundsLetterboxed=" + areBoundsLetterboxed); if (!areBoundsLetterboxed) { diff --git a/services/core/java/com/android/server/wm/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/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/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java index a574845814e8..82d39a39d188 100644 --- a/services/core/java/com/android/server/wm/WindowManagerInternal.java +++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java @@ -990,16 +990,6 @@ public abstract class WindowManagerInternal { } /** - * Sets by the {@link com.android.server.inputmethod.InputMethodManagerService} to monitor - * the visibility change of the IME targeted windows. - * - * @see ImeTargetChangeListener#onImeTargetOverlayVisibilityChanged - * @see ImeTargetChangeListener#onImeInputTargetVisibilityChanged - */ - public abstract void setInputMethodTargetChangeListener( - @NonNull ImeTargetChangeListener listener); - - /** * Moves the {@link WindowToken} {@code binder} to the display specified by {@code displayId}. */ public abstract void moveWindowTokenToDisplay(IBinder binder, int displayId); diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index cf92f1bbb9cf..5749272376ac 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -789,7 +789,6 @@ public class WindowManagerService extends IWindowManager.Stub boolean mHardKeyboardAvailable; WindowManagerInternal.OnHardKeyboardStatusChangeListener mHardKeyboardStatusChangeListener; WindowManagerInternal.OnImeRequestedChangedListener mOnImeRequestedChangedListener; - @Nullable ImeTargetChangeListener mImeTargetChangeListener; SettingsObserver mSettingsObserver; final EmbeddedWindowController mEmbeddedWindowController; @@ -3517,29 +3516,28 @@ public class WindowManagerService extends IWindowManager.Stub void dispatchImeTargetOverlayVisibilityChanged(@NonNull IBinder token, @WindowManager.LayoutParams.WindowType int windowType, boolean visible, boolean removed, int displayId) { - if (mImeTargetChangeListener != null) { - if (DEBUG_INPUT_METHOD) { - Slog.d(TAG, "onImeTargetOverlayVisibilityChanged, win=" + mWindowMap.get(token) - + ", type=" + ViewDebug.intToString(WindowManager.LayoutParams.class, - "type", windowType) + "visible=" + visible + ", removed=" + removed - + ", displayId=" + displayId); - } - mH.post(() -> mImeTargetChangeListener.onImeTargetOverlayVisibilityChanged(token, - windowType, visible, removed, displayId)); + if (DEBUG_INPUT_METHOD) { + Slog.d(TAG, "onImeTargetOverlayVisibilityChanged, win=" + mWindowMap.get(token) + + ", type=" + ViewDebug.intToString(WindowManager.LayoutParams.class, + "type", windowType) + "visible=" + visible + ", removed=" + removed + + ", displayId=" + displayId); } + // Ignoring the starting window since it's ok to cover the IME target + // window in temporary without affecting the IME visibility. + final boolean hasOverlay = visible && !removed && windowType != TYPE_APPLICATION_STARTING; + mH.post(() -> InputMethodManagerInternal.get().setHasVisibleImeLayeringOverlay(hasOverlay, + displayId)); } void dispatchImeInputTargetVisibilityChanged(@NonNull IBinder token, boolean visible, boolean removed, int displayId) { - if (mImeTargetChangeListener != null) { - if (DEBUG_INPUT_METHOD) { - Slog.d(TAG, "onImeInputTargetVisibilityChanged, win=" + mWindowMap.get(token) - + "visible=" + visible + ", removed=" + removed - + ", displayId" + displayId); - } - mH.post(() -> mImeTargetChangeListener.onImeInputTargetVisibilityChanged(token, - visible, removed, displayId)); + if (DEBUG_INPUT_METHOD) { + Slog.d(TAG, "onImeInputTargetVisibilityChanged, win=" + mWindowMap.get(token) + + "visible=" + visible + ", removed=" + removed + ", displayId=" + displayId); } + final boolean visibleAndNotRemoved = visible && !removed; + mH.post(() -> InputMethodManagerInternal.get().onImeInputTargetVisibilityChanged(token, + visibleAndNotRemoved, displayId)); } @Override @@ -8675,13 +8673,6 @@ public class WindowManagerService extends IWindowManager.Stub } @Override - public void setInputMethodTargetChangeListener(@NonNull ImeTargetChangeListener listener) { - synchronized (mGlobalLock) { - mImeTargetChangeListener = listener; - } - } - - @Override public void setOrientationRequestPolicy(boolean respected, int[] fromOrientations, int[] toOrientations) { synchronized (mGlobalLock) { diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index 0093e9d0788b..58c48ad3e9ac 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -22,6 +22,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.app.WindowConfiguration.WINDOW_CONFIG_BOUNDS; import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.WindowManager.TRANSIT_CLOSE_PREPARE_BACK_NAVIGATION; import static android.window.TaskFragmentOperation.OP_TYPE_CLEAR_ADJACENT_TASK_FRAGMENTS; import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE; import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_TASK_FRAGMENT; @@ -59,6 +60,7 @@ import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REMOVE_TASK; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REPARENT; +import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_RESTORE_BACK_NAVIGATION; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_RESTORE_TRANSIENT_ORDER; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_ADJACENT_ROOTS; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_ALWAYS_ON_TOP; @@ -436,6 +438,11 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub // the same transition instead of relying on this possible racing condition. return; } + if (transition.mType == TRANSIT_CLOSE_PREPARE_BACK_NAVIGATION + && mService.mBackNavigationController.restoreBackNavigationSetTransitionReady( + transition)) { + return; + } transition.setAllReady(); } @@ -1386,6 +1393,12 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub task.setTrimmableFromRecents(hop.isTrimmableFromRecents()); break; } + case HIERARCHY_OP_TYPE_RESTORE_BACK_NAVIGATION: { + if (mService.mBackNavigationController.restoreBackNavigation()) { + effects |= TRANSACT_EFFECTS_LIFECYCLE; + } + break; + } } return effects; } diff --git a/services/core/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/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/InputMethodManagerServiceTestBase.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java index 9a25104e1e42..3af5db9841ea 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java @@ -53,6 +53,7 @@ import android.os.ServiceManager; import android.util.ArraySet; import android.view.InputChannel; import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.ImeTracker; import android.window.ImeOnBackInvokedDispatcher; import androidx.test.platform.app.InstrumentationRegistry; @@ -260,6 +261,11 @@ public class InputMethodManagerServiceTestBase { unusedUserId -> mMockInputMethodBindingController); spyOn(mInputMethodManagerService); + synchronized (ImfLock.class) { + doReturn(true).when(mInputMethodManagerService).setImeVisibilityOnFocusedWindowClient( + anyBoolean(), any(UserData.class), any(ImeTracker.Token.class)); + } + // Start a InputMethodManagerService.Lifecycle to publish and manage the lifecycle of // InputMethodManagerService, which is closer to the real situation. InputMethodManagerService.Lifecycle lifecycle = @@ -347,6 +353,14 @@ public class InputMethodManagerServiceTestBase { anyInt() /* flags */, any() /* resultReceiver */); } + protected void verifySetImeVisibility(boolean setVisible, boolean invoked) { + synchronized (ImfLock.class) { + verify(mInputMethodManagerService, + times(invoked ? 1 : 0)).setImeVisibilityOnFocusedWindowClient(eq(setVisible), + any(UserData.class), any(ImeTracker.Token.class)); + } + } + protected void createSessionForClient(IInputMethodClient client) { synchronized (ImfLock.class) { ClientState cs = mInputMethodManagerService.getClientStateLocked(client); diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java index c5b5668c67bc..4d956b2df273 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java @@ -125,27 +125,52 @@ public class InputMethodManagerServiceWindowGainedFocusTest case SOFT_INPUT_STATE_UNSPECIFIED: boolean showSoftInput = (mSoftInputAdjustment == SOFT_INPUT_ADJUST_RESIZE) || mIsLargeScreen; - verifyShowSoftInput( - showSoftInput /* setVisible */, showSoftInput /* showSoftInput */); - // Soft input was hidden by default, so it doesn't need to call - // {@code IMS#hideSoftInput()}. - verifyHideSoftInput(!showSoftInput /* setNotVisible */, false /* hideSoftInput */); + if (android.view.inputmethod.Flags.refactorInsetsController()) { + verifySetImeVisibility(true /* setVisible */, showSoftInput /* invoked */); + // A hide can only be triggered if there is no editorFocused, which this test + // always sets. + verifySetImeVisibility(false /* setVisible */, false /* invoked */); + } else { + verifyShowSoftInput(showSoftInput /* setVisible */, + showSoftInput /* showSoftInput */); + // Soft input was hidden by default, so it doesn't need to call + // {@code IMS#hideSoftInput()}. + verifyHideSoftInput(!showSoftInput /* setNotVisible */, + false /* hideSoftInput */); + } break; case SOFT_INPUT_STATE_VISIBLE: case SOFT_INPUT_STATE_ALWAYS_VISIBLE: - verifyShowSoftInput(true /* setVisible */, true /* showSoftInput */); - verifyHideSoftInput(false /* setNotVisible */, false /* hideSoftInput */); + if (android.view.inputmethod.Flags.refactorInsetsController()) { + verifySetImeVisibility(true /* setVisible */, true /* invoked */); + verifySetImeVisibility(false /* setVisible */, false /* invoked */); + } else { + verifyShowSoftInput(true /* setVisible */, true /* showSoftInput */); + verifyHideSoftInput(false /* setNotVisible */, false /* hideSoftInput */); + } break; - case SOFT_INPUT_STATE_UNCHANGED: // Do nothing - verifyShowSoftInput(false /* setVisible */, false /* showSoftInput */); - verifyHideSoftInput(false /* setNotVisible */, false /* hideSoftInput */); + case SOFT_INPUT_STATE_UNCHANGED: + if (android.view.inputmethod.Flags.refactorInsetsController()) { + verifySetImeVisibility(true /* setVisible */, false /* invoked */); + verifySetImeVisibility(false /* setVisible */, false /* invoked */); + } else { + verifyShowSoftInput(false /* setVisible */, false /* showSoftInput */); + verifyHideSoftInput(false /* setNotVisible */, false /* hideSoftInput */); + } break; case SOFT_INPUT_STATE_HIDDEN: case SOFT_INPUT_STATE_ALWAYS_HIDDEN: - verifyShowSoftInput(false /* setVisible */, false /* showSoftInput */); - // Soft input was hidden by default, so it doesn't need to call - // {@code IMS#hideSoftInput()}. - verifyHideSoftInput(true /* setNotVisible */, false /* hideSoftInput */); + if (android.view.inputmethod.Flags.refactorInsetsController()) { + verifySetImeVisibility(true /* setVisible */, false /* invoked */); + // In this case, we don't have to manipulate the requested visible types of + // the WindowState, as they're already in the correct state + verifySetImeVisibility(false /* setVisible */, false /* invoked */); + } else { + verifyShowSoftInput(false /* setVisible */, false /* showSoftInput */); + // Soft input was hidden by default, so it doesn't need to call + // {@code IMS#hideSoftInput()}. + verifyHideSoftInput(true /* setNotVisible */, false /* hideSoftInput */); + } break; default: throw new IllegalStateException( @@ -167,26 +192,52 @@ public class InputMethodManagerServiceWindowGainedFocusTest case SOFT_INPUT_STATE_UNSPECIFIED: boolean hideSoftInput = (mSoftInputAdjustment != SOFT_INPUT_ADJUST_RESIZE) && !mIsLargeScreen; - verifyShowSoftInput(false /* setVisible */, false /* showSoftInput */); - // Soft input was hidden by default, so it doesn't need to call - // {@code IMS#hideSoftInput()}. - verifyHideSoftInput(hideSoftInput /* setNotVisible */, false /* hideSoftInput */); + if (android.view.inputmethod.Flags.refactorInsetsController()) { + // A show can only be triggered in forward navigation + verifySetImeVisibility(false /* setVisible */, false /* invoked */); + // A hide can only be triggered if there is no editorFocused, which this test + // always sets. + verifySetImeVisibility(false /* setVisible */, false /* invoked */); + } else { + verifyShowSoftInput(false /* setVisible */, false /* showSoftInput */); + // Soft input was hidden by default, so it doesn't need to call + // {@code IMS#hideSoftInput()}. + verifyHideSoftInput(hideSoftInput /* setNotVisible */, + false /* hideSoftInput */); + } break; case SOFT_INPUT_STATE_VISIBLE: case SOFT_INPUT_STATE_HIDDEN: case SOFT_INPUT_STATE_UNCHANGED: // Do nothing - verifyShowSoftInput(false /* setVisible */, false /* showSoftInput */); - verifyHideSoftInput(false /* setNotVisible */, false /* hideSoftInput */); + if (android.view.inputmethod.Flags.refactorInsetsController()) { + verifySetImeVisibility(true /* setVisible */, false /* invoked */); + verifySetImeVisibility(false /* setVisible */, false /* invoked */); + } else { + verifyShowSoftInput(false /* setVisible */, false /* showSoftInput */); + verifyHideSoftInput(false /* setNotVisible */, false /* hideSoftInput */); + } break; case SOFT_INPUT_STATE_ALWAYS_VISIBLE: - verifyShowSoftInput(true /* setVisible */, true /* showSoftInput */); - verifyHideSoftInput(false /* setNotVisible */, false /* hideSoftInput */); + if (android.view.inputmethod.Flags.refactorInsetsController()) { + verifySetImeVisibility(true /* setVisible */, true /* invoked */); + verifySetImeVisibility(false /* setVisible */, false /* invoked */); + } else { + verifyShowSoftInput(true /* setVisible */, true /* showSoftInput */); + verifyHideSoftInput(false /* setNotVisible */, false /* hideSoftInput */); + } break; case SOFT_INPUT_STATE_ALWAYS_HIDDEN: - verifyShowSoftInput(false /* setVisible */, false /* showSoftInput */); - // Soft input was hidden by default, so it doesn't need to call - // {@code IMS#hideSoftInput()}. - verifyHideSoftInput(true /* setNotVisible */, false /* hideSoftInput */); + if (android.view.inputmethod.Flags.refactorInsetsController()) { + verifySetImeVisibility(true /* setVisible */, false /* invoked */); + // In this case, we don't have to manipulate the requested visible types of + // the WindowState, as they're already in the correct state + verifySetImeVisibility(false /* setVisible */, false /* invoked */); + } else { + verifyShowSoftInput(false /* setVisible */, false /* showSoftInput */); + // Soft input was hidden by default, so it doesn't need to call + // {@code IMS#hideSoftInput()}. + verifyHideSoftInput(true /* setNotVisible */, false /* hideSoftInput */); + } break; default: throw new IllegalStateException( 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/servicestests/src/com/android/server/accessibility/MouseKeysInterceptorTest.kt b/services/tests/servicestests/src/com/android/server/accessibility/MouseKeysInterceptorTest.kt index 0def51691efa..8753b251ac98 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/MouseKeysInterceptorTest.kt +++ b/services/tests/servicestests/src/com/android/server/accessibility/MouseKeysInterceptorTest.kt @@ -45,7 +45,6 @@ import org.mockito.ArgumentCaptor import org.mockito.Mock import org.mockito.Mockito import org.mockito.MockitoAnnotations -import java.util.concurrent.TimeUnit import java.util.LinkedList import java.util.Queue import android.util.ArraySet @@ -117,9 +116,6 @@ class MouseKeysInterceptorTest { Mockito.`when`(mockAms.traceManager).thenReturn(mockTraceManager) mouseKeysInterceptor = MouseKeysInterceptor(mockAms, testLooper.looper, DISPLAY_ID) - // VirtualMouse is created on a separate thread. - // Wait for VirtualMouse to be created before running tests - TimeUnit.MILLISECONDS.sleep(20L) mouseKeysInterceptor.next = nextInterceptor } diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java index f98bbf9cce4c..2b93ccb436c5 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java @@ -2120,6 +2120,34 @@ public class HdmiCecLocalDeviceTvTest { assertThat(mNativeWrapper.getResultMessages()).doesNotContain(giveOsdName); } + @Test + public void onOneTouchPlay_wakeUp_addCecDevice() { + assertThat(mHdmiControlService.getHdmiCecNetwork().getDeviceInfoList(false)) + .isEmpty(); + mHdmiCecLocalDeviceTv.mService.getHdmiCecConfig().setIntValue( + HdmiControlManager.CEC_SETTING_NAME_TV_WAKE_ON_ONE_TOUCH_PLAY, + HdmiControlManager.TV_WAKE_ON_ONE_TOUCH_PLAY_ENABLED); + mPowerManager.setInteractive(false); + mTestLooper.dispatchAll(); + + HdmiCecMessage textViewOn = HdmiCecMessageBuilder.buildTextViewOn(ADDR_PLAYBACK_1, + mTvLogicalAddress); + HdmiCecMessage activeSource = HdmiCecMessageBuilder.buildActiveSource(ADDR_PLAYBACK_1, + 0x1000); + assertThat(mHdmiCecLocalDeviceTv.dispatchMessage(textViewOn)).isEqualTo(Constants.HANDLED); + assertThat(mHdmiCecLocalDeviceTv.dispatchMessage(activeSource)).isEqualTo( + Constants.HANDLED); + mTestLooper.dispatchAll(); + assertThat(mPowerManager.isInteractive()).isTrue(); + + // FakePowerManagerWrapper#wakeUp() doesn't broadcast Intent.ACTION_SCREEN_ON so we have to + // manually call this method. + mHdmiControlService.onWakeUp(WAKE_UP_SCREEN_ON); + mTestLooper.dispatchAll(); + assertThat(mHdmiControlService.getHdmiCecNetwork().getDeviceInfoList(false)) + .hasSize(1); + } + protected static class MockTvDevice extends HdmiCecLocalDeviceTv { MockTvDevice(HdmiControlService service) { super(service); 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/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java b/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java index 4bea95f61c1f..7933f7ab06cd 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java @@ -18,8 +18,8 @@ package com.android.server.notification; import static android.app.UiModeManager.MODE_ATTENTION_THEME_OVERLAY_NIGHT; import static android.app.UiModeManager.MODE_ATTENTION_THEME_OVERLAY_OFF; -import static android.service.notification.ZenModeConfig.UPDATE_ORIGIN_APP; -import static android.service.notification.ZenModeConfig.UPDATE_ORIGIN_USER; +import static android.service.notification.ZenModeConfig.ORIGIN_APP; +import static android.service.notification.ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI; import static com.google.common.truth.Truth.assertThat; @@ -48,7 +48,6 @@ import android.hardware.display.ColorDisplayManager; import android.os.PowerManager; import android.platform.test.flag.junit.SetFlagsRule; import android.service.notification.ZenDeviceEffects; -import android.service.notification.ZenModeConfig; import android.testing.TestableContext; import androidx.test.InstrumentationRegistry; @@ -78,27 +77,6 @@ public class DefaultDeviceEffectsApplierTest { @Mock UiModeManager mUiModeManager; @Mock WallpaperManager mWallpaperManager; - private enum ChangeOrigin { - ORIGIN_UNKNOWN(ZenModeConfig.UPDATE_ORIGIN_UNKNOWN), - ORIGIN_INIT(ZenModeConfig.UPDATE_ORIGIN_INIT), - ORIGIN_INIT_USER(ZenModeConfig.UPDATE_ORIGIN_INIT_USER), - ORIGIN_USER(ZenModeConfig.UPDATE_ORIGIN_USER), - ORIGIN_APP(ZenModeConfig.UPDATE_ORIGIN_APP), - ORIGIN_SYSTEM_OR_SYSTEMUI(ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI), - ORIGIN_RESTORE_BACKUP(ZenModeConfig.UPDATE_ORIGIN_RESTORE_BACKUP); - - private final int mValue; - - ChangeOrigin(@ZenModeConfig.ConfigChangeOrigin int value) { - mValue = value; - } - - @ZenModeConfig.ConfigChangeOrigin - public int value() { - return mValue; - } - } - @Before public void setUp() { MockitoAnnotations.initMocks(this); @@ -123,7 +101,7 @@ public class DefaultDeviceEffectsApplierTest { .setShouldDisplayGrayscale(true) .setShouldUseNightMode(true) .build(); - mApplier.apply(effects, UPDATE_ORIGIN_USER); + mApplier.apply(effects, ORIGIN_USER_IN_SYSTEMUI); verify(mPowerManager).suppressAmbientDisplay(anyString(), eq(true)); verify(mColorDisplayManager).setSaturationLevel(eq(0)); @@ -141,14 +119,14 @@ public class DefaultDeviceEffectsApplierTest { .setShouldDisplayGrayscale(true) .setShouldUseNightMode(true) .build(); - mApplier.apply(previousEffects, UPDATE_ORIGIN_USER); + mApplier.apply(previousEffects, ORIGIN_USER_IN_SYSTEMUI); verify(mPowerManager).suppressAmbientDisplay(anyString(), eq(true)); verify(mColorDisplayManager).setSaturationLevel(eq(0)); verify(mWallpaperManager).setWallpaperDimAmount(eq(0.6f)); verify(mUiModeManager).setAttentionModeThemeOverlay(eq(MODE_ATTENTION_THEME_OVERLAY_NIGHT)); ZenDeviceEffects noEffects = new ZenDeviceEffects.Builder().build(); - mApplier.apply(noEffects, UPDATE_ORIGIN_USER); + mApplier.apply(noEffects, ORIGIN_USER_IN_SYSTEMUI); verify(mPowerManager).suppressAmbientDisplay(anyString(), eq(false)); verify(mColorDisplayManager).setSaturationLevel(eq(100)); @@ -163,11 +141,11 @@ public class DefaultDeviceEffectsApplierTest { ZenDeviceEffects previousEffects = new ZenDeviceEffects.Builder() .setShouldSuppressAmbientDisplay(true) .build(); - mApplier.apply(previousEffects, UPDATE_ORIGIN_USER); + mApplier.apply(previousEffects, ORIGIN_USER_IN_SYSTEMUI); verify(mPowerManager).suppressAmbientDisplay(anyString(), eq(true)); ZenDeviceEffects noEffects = new ZenDeviceEffects.Builder().build(); - mApplier.apply(noEffects, UPDATE_ORIGIN_USER); + mApplier.apply(noEffects, ORIGIN_USER_IN_SYSTEMUI); verify(mPowerManager).suppressAmbientDisplay(anyString(), eq(false)); verifyZeroInteractions(mColorDisplayManager, mWallpaperManager, mUiModeManager); @@ -186,7 +164,7 @@ public class DefaultDeviceEffectsApplierTest { .setShouldDisplayGrayscale(true) .setShouldUseNightMode(true) .build(); - mApplier.apply(effects, UPDATE_ORIGIN_USER); + mApplier.apply(effects, ORIGIN_USER_IN_SYSTEMUI); verify(mPowerManager).suppressAmbientDisplay(anyString(), eq(true)); // (And no crash from missing services). @@ -207,7 +185,7 @@ public class DefaultDeviceEffectsApplierTest { .setShouldDisplayGrayscale(true) .setShouldUseNightMode(true) .build(); - mApplier.apply(effects, UPDATE_ORIGIN_USER); + mApplier.apply(effects, ORIGIN_USER_IN_SYSTEMUI); verifyNoMoreInteractions(mWallpaperManager); } @@ -220,7 +198,7 @@ public class DefaultDeviceEffectsApplierTest { .setShouldDimWallpaper(true) .setShouldDisplayGrayscale(true) .build(); - mApplier.apply(effects, UPDATE_ORIGIN_USER); + mApplier.apply(effects, ORIGIN_USER_IN_SYSTEMUI); verify(mColorDisplayManager).setSaturationLevel(eq(0)); verify(mWallpaperManager).setWallpaperDimAmount(eq(0.6f)); @@ -234,12 +212,12 @@ public class DefaultDeviceEffectsApplierTest { mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); mApplier.apply(new ZenDeviceEffects.Builder().setShouldDimWallpaper(true).build(), - UPDATE_ORIGIN_USER); + ORIGIN_USER_IN_SYSTEMUI); verify(mWallpaperManager).setWallpaperDimAmount(eq(0.6f)); // Apply a second effect and remove the first one. mApplier.apply(new ZenDeviceEffects.Builder().setShouldDisplayGrayscale(true).build(), - UPDATE_ORIGIN_USER); + ORIGIN_USER_IN_SYSTEMUI); // Wallpaper dimming was undone, Grayscale was applied, nothing else was touched. verify(mWallpaperManager).setWallpaperDimAmount(eq(0.0f)); @@ -259,7 +237,7 @@ public class DefaultDeviceEffectsApplierTest { when(mPowerManager.isInteractive()).thenReturn(true); mApplier.apply(new ZenDeviceEffects.Builder().setShouldUseNightMode(true).build(), - UPDATE_ORIGIN_APP); + ORIGIN_APP); // Effect was not yet applied, but a broadcast receiver was registered. verifyZeroInteractions(mUiModeManager); @@ -278,7 +256,7 @@ public class DefaultDeviceEffectsApplierTest { @Test public void apply_nightModeWithScreenOff_appliedImmediately( - @TestParameter ChangeOrigin origin) { + @TestParameter ZenChangeOrigin origin) { mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); when(mPowerManager.isInteractive()).thenReturn(false); @@ -292,9 +270,10 @@ public class DefaultDeviceEffectsApplierTest { } @Test - @TestParameters({"{origin: ORIGIN_USER}", "{origin: ORIGIN_INIT}", - "{origin: ORIGIN_INIT_USER}"}) - public void apply_nightModeWithScreenOn_appliedImmediatelyBasedOnOrigin(ChangeOrigin origin) { + @TestParameters({"{origin: ORIGIN_USER_IN_SYSTEMUI}", "{origin: ORIGIN_USER_IN_APP}", + "{origin: ORIGIN_INIT}", "{origin: ORIGIN_INIT_USER}"}) + public void apply_nightModeWithScreenOn_appliedImmediatelyBasedOnOrigin( + ZenChangeOrigin origin) { mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); when(mPowerManager.isInteractive()).thenReturn(true); @@ -309,8 +288,9 @@ public class DefaultDeviceEffectsApplierTest { @Test @TestParameters({"{origin: ORIGIN_APP}", "{origin: ORIGIN_RESTORE_BACKUP}", - "{origin: ORIGIN_SYSTEM_OR_SYSTEMUI}", "{origin: ORIGIN_UNKNOWN}"}) - public void apply_nightModeWithScreenOn_willBeAppliedLaterBasedOnOrigin(ChangeOrigin origin) { + "{origin: ORIGIN_SYSTEM}", "{origin: ORIGIN_UNKNOWN}"}) + public void apply_nightModeWithScreenOn_willBeAppliedLaterBasedOnOrigin( + ZenChangeOrigin origin) { mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); when(mPowerManager.isInteractive()).thenReturn(true); @@ -342,7 +322,7 @@ public class DefaultDeviceEffectsApplierTest { .setShouldDisplayGrayscale(true) .setShouldUseNightMode(true) .build(); - mApplier.apply(effects, UPDATE_ORIGIN_USER); + mApplier.apply(effects, ORIGIN_USER_IN_SYSTEMUI); // No crashes } 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 1b999e49d21b..5a8de58c14ae 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -10064,7 +10064,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // verify that zen mode helper gets passed in a package name of "android" verify(mockZenModeHelper).addAutomaticZenRule(eq("android"), eq(rule), - eq(ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI), anyString(), anyInt()); + eq(ZenModeConfig.ORIGIN_SYSTEM), anyString(), anyInt()); } @Test @@ -10086,7 +10086,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // verify that zen mode helper gets passed in a package name of "android" verify(mockZenModeHelper).addAutomaticZenRule(eq("android"), eq(rule), - eq(ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI), anyString(), anyInt()); + eq(ZenModeConfig.ORIGIN_SYSTEM), anyString(), anyInt()); } @Test @@ -10106,7 +10106,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // verify that zen mode helper gets passed in the package name from the arg, not the owner verify(mockZenModeHelper).addAutomaticZenRule( - eq("another.package"), eq(rule), eq(ZenModeConfig.UPDATE_ORIGIN_APP), + eq("another.package"), eq(rule), eq(ZenModeConfig.ORIGIN_APP), anyString(), anyInt()); // doesn't count as a system/systemui call } @@ -10221,7 +10221,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mBinderService.addAutomaticZenRule(SOME_ZEN_RULE, "pkg", /* fromUser= */ true); verify(zenModeHelper).addAutomaticZenRule(eq("pkg"), eq(SOME_ZEN_RULE), - eq(ZenModeConfig.UPDATE_ORIGIN_USER), anyString(), anyInt()); + eq(ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI), anyString(), anyInt()); } @Test @@ -10233,7 +10233,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mBinderService.addAutomaticZenRule(SOME_ZEN_RULE, "pkg", /* fromUser= */ false); verify(zenModeHelper).addAutomaticZenRule(eq("pkg"), eq(SOME_ZEN_RULE), - eq(ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI), anyString(), anyInt()); + eq(ZenModeConfig.ORIGIN_SYSTEM), anyString(), anyInt()); } @Test @@ -10245,7 +10245,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mBinderService.addAutomaticZenRule(SOME_ZEN_RULE, "pkg", /* fromUser= */ false); verify(zenModeHelper).addAutomaticZenRule(eq("pkg"), eq(SOME_ZEN_RULE), - eq(ZenModeConfig.UPDATE_ORIGIN_APP), anyString(), anyInt()); + eq(ZenModeConfig.ORIGIN_APP), anyString(), anyInt()); } @Test @@ -10267,7 +10267,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mBinderService.updateAutomaticZenRule("id", SOME_ZEN_RULE, /* fromUser= */ true); verify(zenModeHelper).updateAutomaticZenRule(eq("id"), eq(SOME_ZEN_RULE), - eq(ZenModeConfig.UPDATE_ORIGIN_USER), anyString(), anyInt()); + eq(ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI), anyString(), anyInt()); } @Test @@ -10289,7 +10289,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mBinderService.removeAutomaticZenRule("id", /* fromUser= */ true); verify(zenModeHelper).removeAutomaticZenRule(eq("id"), - eq(ZenModeConfig.UPDATE_ORIGIN_USER), anyString(), anyInt()); + eq(ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI), anyString(), anyInt()); } @Test @@ -10304,7 +10304,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test @EnableFlags(android.app.Flags.FLAG_MODES_API) - public void setAutomaticZenRuleState_conditionFromUser_mappedToOriginUser() throws Exception { + public void setAutomaticZenRuleState_fromAppWithConditionFromUser_originUserInApp() + throws Exception { ZenModeHelper zenModeHelper = setUpMockZenTest(); mService.setCallerIsNormalPackage(); @@ -10313,12 +10314,12 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mBinderService.setAutomaticZenRuleState("id", withSourceUser); verify(zenModeHelper).setAutomaticZenRuleState(eq("id"), eq(withSourceUser), - eq(ZenModeConfig.UPDATE_ORIGIN_USER), anyInt()); + eq(ZenModeConfig.ORIGIN_USER_IN_APP), anyInt()); } @Test @EnableFlags(android.app.Flags.FLAG_MODES_API) - public void setAutomaticZenRuleState_fromAppWithConditionNotFromUser_mappedToOriginApp() + public void setAutomaticZenRuleState_fromAppWithConditionNotFromUser_originApp() throws Exception { ZenModeHelper zenModeHelper = setUpMockZenTest(); mService.setCallerIsNormalPackage(); @@ -10328,12 +10329,26 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mBinderService.setAutomaticZenRuleState("id", withSourceContext); verify(zenModeHelper).setAutomaticZenRuleState(eq("id"), eq(withSourceContext), - eq(ZenModeConfig.UPDATE_ORIGIN_APP), anyInt()); + eq(ZenModeConfig.ORIGIN_APP), anyInt()); } @Test @EnableFlags(android.app.Flags.FLAG_MODES_API) - public void setAutomaticZenRuleState_fromSystemWithConditionNotFromUser_mappedToOriginSystem() + public void setAutomaticZenRuleState_fromSystemWithConditionFromUser_originUserInSystemUi() + throws Exception { + ZenModeHelper zenModeHelper = setUpMockZenTest(); + mService.isSystemUid = true; + + Condition withSourceContext = new Condition(Uri.parse("uri"), "summary", STATE_TRUE, + SOURCE_USER_ACTION); + mBinderService.setAutomaticZenRuleState("id", withSourceContext); + + verify(zenModeHelper).setAutomaticZenRuleState(eq("id"), eq(withSourceContext), + eq(ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI), anyInt()); + } + @Test + @EnableFlags(android.app.Flags.FLAG_MODES_API) + public void setAutomaticZenRuleState_fromSystemWithConditionNotFromUser_originSystem() throws Exception { ZenModeHelper zenModeHelper = setUpMockZenTest(); mService.isSystemUid = true; @@ -10343,7 +10358,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mBinderService.setAutomaticZenRuleState("id", withSourceContext); verify(zenModeHelper).setAutomaticZenRuleState(eq("id"), eq(withSourceContext), - eq(ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI), anyInt()); + eq(ZenModeConfig.ORIGIN_SYSTEM), anyInt()); } /** Prepares for a zen-related test that uses a mocked {@link ZenModeHelper}. */ @@ -13049,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); @@ -15508,7 +15617,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mBinderService.setInterruptionFilter("package", INTERRUPTION_FILTER_PRIORITY, false); verify(zenModeHelper).setManualZenMode(eq(ZEN_MODE_IMPORTANT_INTERRUPTIONS), eq(null), - eq(ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI), anyString(), eq("package"), + eq(ZenModeConfig.ORIGIN_SYSTEM), anyString(), eq("package"), anyInt()); } @@ -15554,7 +15663,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { if (canSetGlobalPolicy) { verify(zenModeHelper).setManualZenMode(eq(ZEN_MODE_IMPORTANT_INTERRUPTIONS), eq(null), - eq(ZenModeConfig.UPDATE_ORIGIN_APP), anyString(), eq("package"), anyInt()); + eq(ZenModeConfig.ORIGIN_APP), anyString(), eq("package"), anyInt()); } else { verify(zenModeHelper).applyGlobalZenModeAsImplicitZenRule(anyString(), anyInt(), eq(ZEN_MODE_IMPORTANT_INTERRUPTIONS)); @@ -15594,7 +15703,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { INTERRUPTION_FILTER_PRIORITY); verify(mService.mZenModeHelper).setManualZenMode(eq(ZEN_MODE_IMPORTANT_INTERRUPTIONS), - eq(null), eq(ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI), anyString(), + eq(null), eq(ZenModeConfig.ORIGIN_SYSTEM), anyString(), eq("pkg"), eq(mUid)); } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenChangeOrigin.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenChangeOrigin.java new file mode 100644 index 000000000000..572878857286 --- /dev/null +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenChangeOrigin.java @@ -0,0 +1,43 @@ +/* + * 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.notification; + +import android.service.notification.ZenModeConfig; + +/** Enum version of {@link ZenModeConfig.ConfigOrigin}, for test parameterization. */ +public enum ZenChangeOrigin { + ORIGIN_UNKNOWN(ZenModeConfig.ORIGIN_UNKNOWN), + ORIGIN_INIT(ZenModeConfig.ORIGIN_INIT), + ORIGIN_INIT_USER(ZenModeConfig.ORIGIN_INIT_USER), + ORIGIN_APP(ZenModeConfig.ORIGIN_APP), + ORIGIN_USER_IN_APP(ZenModeConfig.ORIGIN_USER_IN_APP), + ORIGIN_SYSTEM(ZenModeConfig.ORIGIN_SYSTEM), + ORIGIN_USER_IN_SYSTEMUI(ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI), + ORIGIN_RESTORE_BACKUP(ZenModeConfig.ORIGIN_RESTORE_BACKUP); + + private final int mValue; + + ZenChangeOrigin(@ZenModeConfig.ConfigOrigin int value) { + mValue = value; + } + + /** Gets the {@link ZenModeConfig.ConfigOrigin} int value corresponding to the enum. */ + @ZenModeConfig.ConfigOrigin + public int value() { + return mValue; + } +} diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenEnumTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenEnumTest.java index 8add2f957a07..cdceb1f0d86b 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenEnumTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenEnumTest.java @@ -20,12 +20,14 @@ import static com.google.common.truth.Truth.assertThat; import android.app.AutomaticZenRule; import android.provider.Settings; +import android.service.notification.ZenModeConfig; import android.service.notification.ZenPolicy; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; import com.android.os.dnd.ActiveRuleType; +import com.android.os.dnd.ChangeOrigin; import com.android.os.dnd.ChannelPolicy; import com.android.os.dnd.ConversationType; import com.android.os.dnd.PeopleType; @@ -78,6 +80,11 @@ public class ZenEnumTest { testEnum(ZenPolicy.class, "PEOPLE_TYPE", PeopleType.class, "PEOPLE"); } + @Test + public void testEnum_changeOrigin() { + testEnum(ZenModeConfig.class, "ORIGIN", ChangeOrigin.class, "ORIGIN"); + } + /** * Verifies that any constants (i.e. {@code public static final int} fields) named {@code * <apiPrefix>_SOMETHING} in {@code apiClass} are present and have the same numerical value 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 3c3c2f34490a..60c4ac777906 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,8 @@ 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.ZenPolicy.CONVERSATION_SENDERS_IMPORTANT; import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_NONE; import static android.service.notification.ZenPolicy.PEOPLE_TYPE_ANYONE; @@ -283,8 +285,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()); } @@ -535,7 +537,7 @@ public class ZenModeConfigTest extends UiServiceTestCase { rule.triggerDescription = TRIGGER_DESC; rule.deletionInstant = Instant.ofEpochMilli(1701790147000L); if (Flags.modesUi()) { - rule.disabledOrigin = ZenModeConfig.UPDATE_ORIGIN_USER; + rule.disabledOrigin = ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI; } Parcel parcel = Parcel.obtain(); @@ -650,7 +652,7 @@ public class ZenModeConfigTest extends UiServiceTestCase { rule.triggerDescription = TRIGGER_DESC; rule.deletionInstant = Instant.ofEpochMilli(1701790147000L); if (Flags.modesUi()) { - rule.disabledOrigin = ZenModeConfig.UPDATE_ORIGIN_APP; + rule.disabledOrigin = ZenModeConfig.ORIGIN_APP; } ByteArrayOutputStream baos = new ByteArrayOutputStream(); @@ -991,6 +993,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()) { @@ -1238,4 +1292,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/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java index d7bae457c70a..776a840466c8 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java @@ -46,6 +46,7 @@ import static android.app.NotificationManager.Policy.PRIORITY_SENDERS_STARRED; import static android.app.NotificationManager.Policy.STATE_PRIORITY_CHANNELS_BLOCKED; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_BADGE; import static android.content.pm.PackageManager.PERMISSION_GRANTED; +import static android.os.Process.SYSTEM_UID; 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; @@ -54,12 +55,12 @@ 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; import static android.service.notification.Condition.STATE_TRUE; -import static android.service.notification.ZenModeConfig.UPDATE_ORIGIN_APP; -import static android.service.notification.ZenModeConfig.UPDATE_ORIGIN_INIT; -import static android.service.notification.ZenModeConfig.UPDATE_ORIGIN_INIT_USER; -import static android.service.notification.ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI; -import static android.service.notification.ZenModeConfig.UPDATE_ORIGIN_UNKNOWN; -import static android.service.notification.ZenModeConfig.UPDATE_ORIGIN_USER; +import static android.service.notification.ZenModeConfig.ORIGIN_APP; +import static android.service.notification.ZenModeConfig.ORIGIN_INIT; +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.ZenPolicy.PEOPLE_TYPE_CONTACTS; import static android.service.notification.ZenPolicy.PEOPLE_TYPE_NONE; import static android.service.notification.ZenPolicy.PEOPLE_TYPE_STARRED; @@ -131,7 +132,6 @@ import android.media.AudioSystem; import android.media.VolumePolicy; import android.net.Uri; import android.os.Parcel; -import android.os.Process; import android.os.SimpleClock; import android.os.UserHandle; import android.platform.test.annotations.DisableFlags; @@ -186,9 +186,6 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.xmlpull.v1.XmlPullParserException; -import platform.test.runner.parameterized.ParameterizedAndroidJunit4; -import platform.test.runner.parameterized.Parameters; - import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; @@ -208,6 +205,9 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import platform.test.runner.parameterized.ParameterizedAndroidJunit4; +import platform.test.runner.parameterized.Parameters; + @SmallTest @SuppressLint("GuardedBy") // It's ok for this test to access guarded methods from the service. @RunWith(ParameterizedAndroidJunit4.class) @@ -338,27 +338,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeEventLogger.reset(); } - private enum ChangeOrigin { - ORIGIN_UNKNOWN(ZenModeConfig.UPDATE_ORIGIN_UNKNOWN), - ORIGIN_INIT(ZenModeConfig.UPDATE_ORIGIN_INIT), - ORIGIN_INIT_USER(ZenModeConfig.UPDATE_ORIGIN_INIT_USER), - ORIGIN_USER(ZenModeConfig.UPDATE_ORIGIN_USER), - ORIGIN_APP(ZenModeConfig.UPDATE_ORIGIN_APP), - ORIGIN_SYSTEM_OR_SYSTEMUI(ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI), - ORIGIN_RESTORE_BACKUP(ZenModeConfig.UPDATE_ORIGIN_RESTORE_BACKUP); - - private final int mValue; - - ChangeOrigin(@ZenModeConfig.ConfigChangeOrigin int value) { - mValue = value; - } - - @ZenModeConfig.ConfigChangeOrigin - public int value() { - return mValue; - } - } - private XmlResourceParser getDefaultConfigParser() throws IOException, XmlPullParserException { String xml = "<zen version=\"10\">\n" + "<allow alarms=\"true\" media=\"true\" system=\"false\" calls=\"true\" " @@ -395,8 +374,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.writeXml(serializer, false, version, UserHandle.USER_ALL); serializer.endDocument(); serializer.flush(); - mZenModeHelper.setConfig(new ZenModeConfig(), null, UPDATE_ORIGIN_INIT, "writing xml", - Process.SYSTEM_UID); + mZenModeHelper.setConfig(new ZenModeConfig(), null, ORIGIN_INIT, "writing xml", + SYSTEM_UID); return baos; } @@ -411,8 +390,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { serializer.flush(); ZenModeConfig newConfig = new ZenModeConfig(); newConfig.user = userId; - mZenModeHelper.setConfig(newConfig, null, UPDATE_ORIGIN_INIT, "writing xml", - Process.SYSTEM_UID); + mZenModeHelper.setConfig(newConfig, null, ORIGIN_INIT, "writing xml", + SYSTEM_UID); return baos; } @@ -705,13 +684,13 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setInterruptionFilter(INTERRUPTION_FILTER_NONE) .build(); String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - azr, UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "reason", Process.SYSTEM_UID); + azr, ZenModeConfig.ORIGIN_SYSTEM, "reason", SYSTEM_UID); // Enable rule mZenModeHelper.setAutomaticZenRuleState(ruleId, new Condition(azr.getConditionId(), "", STATE_TRUE), - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, - Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, + SYSTEM_UID); // Confirm that the consolidated policy doesn't allow anything NotificationManager.Policy policy = mZenModeHelper.getConsolidatedNotificationPolicy(); @@ -739,13 +718,13 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS) .build(); String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - azr, UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "reason", Process.SYSTEM_UID); + azr, ZenModeConfig.ORIGIN_SYSTEM, "reason", SYSTEM_UID); // Enable rule mZenModeHelper.setAutomaticZenRuleState(ruleId, new Condition(azr.getConditionId(), "", STATE_TRUE), - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, - Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, + SYSTEM_UID); // Confirm that the consolidated policy allows only alarms and media and nothing else NotificationManager.Policy policy = mZenModeHelper.getConsolidatedNotificationPolicy(); @@ -819,7 +798,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_NORMAL); // Set zen to priority-only with all notification sounds muted (so ringer will be muted) Policy totalSilence = new Policy(0, 0, 0); - mZenModeHelper.setNotificationPolicy(totalSilence, UPDATE_ORIGIN_APP, 1); + mZenModeHelper.setNotificationPolicy(totalSilence, ORIGIN_APP, 1); mZenModeHelper.mZenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS; // 2. verify ringer is unchanged @@ -857,7 +836,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // in priority only mode: // ringtone, notification and system streams are affected by ringer mode mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, Uri.EMPTY, - UPDATE_ORIGIN_APP, "test", "caller", 1); + ORIGIN_APP, "test", "caller", 1); ZenModeHelper.RingerModeDelegate ringerModeDelegateRingerMuted = mZenModeHelper.new RingerModeDelegate(); @@ -873,9 +852,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { // even when ringer is muted (since all ringer sounds cannot bypass DND), // system stream is still affected by ringer mode - mZenModeHelper.setNotificationPolicy(new Policy(0, 0, 0), UPDATE_ORIGIN_APP, 1); + mZenModeHelper.setNotificationPolicy(new Policy(0, 0, 0), ORIGIN_APP, 1); mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, Uri.EMPTY, - UPDATE_ORIGIN_APP, "test", "caller", 1); + ORIGIN_APP, "test", "caller", 1); ZenModeHelper.RingerModeDelegate ringerModeDelegateRingerNotMuted = mZenModeHelper.new RingerModeDelegate(); @@ -983,7 +962,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_SILENT); for (int i = 0; i < 3; i++) { mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, Uri.EMPTY, - UPDATE_ORIGIN_APP, "test", "caller", 1); + ORIGIN_APP, "test", "caller", 1); } verify(mAudioManager, never()).setRingerModeInternal(AudioManager.RINGER_MODE_NORMAL, mZenModeHelper.TAG); @@ -1008,7 +987,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { for (int i = 0; i < 3; i++) { // if zen doesn't change, zen should not reapply itself to the ringer mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, Uri.EMPTY, - UPDATE_ORIGIN_APP, "test", "caller", 1); + ORIGIN_APP, "test", "caller", 1); } verify(mAudioManager, never()).setRingerModeInternal(AudioManager.RINGER_MODE_NORMAL, mZenModeHelper.TAG); @@ -1033,7 +1012,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { for (int i = 0; i < 3; i++) { // if zen doesn't change, zen should not reapply itself to the ringer mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, Uri.EMPTY, - UPDATE_ORIGIN_APP, "test", "caller", 1); + ORIGIN_APP, "test", "caller", 1); } verify(mAudioManager, never()).setRingerModeInternal(AudioManager.RINGER_MODE_NORMAL, mZenModeHelper.TAG); @@ -1048,7 +1027,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { reset(mAudioManager); // Turn manual zen mode on - mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, UPDATE_ORIGIN_APP, + mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, + ORIGIN_APP, null, "test", CUSTOM_PKG_UID); // audio manager shouldn't do anything until the handler processes its messages @@ -1076,14 +1056,14 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.setManualZenMode( ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, - UPDATE_ORIGIN_APP, + ORIGIN_APP, null, "test", CUSTOM_PKG_UID); mZenModeHelper.setManualZenMode( ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, - UPDATE_ORIGIN_APP, + ORIGIN_APP, null, "test", CUSTOM_PKG_UID); @@ -1108,15 +1088,15 @@ public class ZenModeHelperTest extends UiServiceTestCase { | PRIORITY_CATEGORY_MESSAGES | PRIORITY_CATEGORY_REPEAT_CALLERS | PRIORITY_CATEGORY_CONVERSATIONS, PRIORITY_SENDERS_STARRED, PRIORITY_SENDERS_STARRED, 0, CONVERSATION_SENDERS_ANYONE), - UPDATE_ORIGIN_UNKNOWN, + ORIGIN_UNKNOWN, 1); mZenModeHelper.setManualZenRuleDeviceEffects(new ZenDeviceEffects.Builder() .setShouldDimWallpaper(true) .setShouldDisplayGrayscale(true) .setShouldUseNightMode(true) - .build(), UPDATE_ORIGIN_UNKNOWN, "test", 1); + .build(), ORIGIN_UNKNOWN, "test", 1); mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, Uri.EMPTY, - UPDATE_ORIGIN_UNKNOWN, "test", "me", 1); + ORIGIN_UNKNOWN, "test", "me", 1); ZenModeConfig actual = mZenModeHelper.mConfig.copy(); @@ -1130,13 +1110,13 @@ public class ZenModeHelperTest extends UiServiceTestCase { | PRIORITY_CATEGORY_CONVERSATIONS, PRIORITY_SENDERS_STARRED, PRIORITY_SENDERS_STARRED, SUPPRESSED_EFFECT_BADGE, CONVERSATION_SENDERS_ANYONE), - UPDATE_ORIGIN_UNKNOWN, 1); + ORIGIN_UNKNOWN, 1); mZenModeHelper.setManualZenRuleDeviceEffects(new ZenDeviceEffects.Builder() .setShouldDimWallpaper(true) .setShouldDisplayGrayscale(true) - .build(), UPDATE_ORIGIN_UNKNOWN, "test", 1); + .build(), ORIGIN_UNKNOWN, "test", 1); mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, Uri.EMPTY, - UPDATE_ORIGIN_UNKNOWN, "test", "me", 1); + ORIGIN_UNKNOWN, "test", "me", 1); ZenModeConfig expected = mZenModeHelper.mConfig.copy(); if (Flags.modesUi()) { @@ -1157,7 +1137,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test public void testProto() throws InvalidProtocolBufferException { mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, - Flags.modesApi() ? UPDATE_ORIGIN_USER : UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, + Flags.modesApi() ? ORIGIN_USER_IN_SYSTEMUI : ZenModeConfig.ORIGIN_SYSTEM, null, "test", CUSTOM_PKG_UID); mZenModeHelper.mConfig.automaticRules = new ArrayMap<>(); // no automatic rules @@ -1381,7 +1361,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { List<StatsEvent> events = new LinkedList<>(); mZenModeHelper.pullRules(events); - mZenModeHelper.removeAutomaticZenRule(CUSTOM_RULE_ID, UPDATE_ORIGIN_APP, "test", + mZenModeHelper.removeAutomaticZenRule(CUSTOM_RULE_ID, ORIGIN_APP, "test", CUSTOM_PKG_UID); assertTrue(-1 == mZenModeHelper.mRulesUidCache.getOrDefault(CUSTOM_PKG_NAME + "|" + 0, -1)); @@ -1410,7 +1390,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { public void testProtoWithManualRule() throws Exception { setupZenConfig(); mZenModeHelper.mConfig.automaticRules = getCustomAutomaticRules(); - mZenModeHelper.setManualZenMode(INTERRUPTION_FILTER_PRIORITY, Uri.EMPTY, UPDATE_ORIGIN_APP, + mZenModeHelper.setManualZenMode(INTERRUPTION_FILTER_PRIORITY, Uri.EMPTY, + ORIGIN_APP, "test", "me", 1); List<StatsEvent> events = new LinkedList<>(); @@ -1437,14 +1418,14 @@ public class ZenModeHelperTest extends UiServiceTestCase { Policy policy = new Policy(PRIORITY_CATEGORY_MEDIA | PRIORITY_CATEGORY_ALARMS, 0, 0); config10.applyNotificationPolicy(policy); config10.user = 10; - mZenModeHelper.setConfig(config10, null, UPDATE_ORIGIN_INIT, "writeXml", - Process.SYSTEM_UID); + mZenModeHelper.setConfig(config10, null, ORIGIN_INIT, "writeXml", + SYSTEM_UID); ZenModeConfig config11 = mZenModeHelper.mConfig.copy(); config11.user = 11; policy = new Policy(0, 0, 0); config11.applyNotificationPolicy(policy); - mZenModeHelper.setConfig(config11, null, UPDATE_ORIGIN_INIT, "writeXml", - Process.SYSTEM_UID); + mZenModeHelper.setConfig(config11, null, ORIGIN_INIT, "writeXml", + SYSTEM_UID); // Backup user 10 and reset values. ByteArrayOutputStream baos = writeXmlAndPurgeForUser(null, 10); @@ -1514,6 +1495,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(); @@ -2307,7 +2312,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); // We need the package name to be something that's not "android" so there aren't any // existing rules under that package. - String id = mZenModeHelper.addAutomaticZenRule("pkgname", zenRule, UPDATE_ORIGIN_APP, + String id = mZenModeHelper.addAutomaticZenRule("pkgname", zenRule, ORIGIN_APP, "test", CUSTOM_PKG_UID); assertNotNull(id); } @@ -2318,7 +2323,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), new ZenPolicy.Builder().build(), NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id = mZenModeHelper.addAutomaticZenRule("pkgname", zenRule, UPDATE_ORIGIN_APP, + String id = mZenModeHelper.addAutomaticZenRule("pkgname", zenRule, ORIGIN_APP, "test", CUSTOM_PKG_UID); fail("allowed too many rules to be created"); } catch (IllegalArgumentException e) { @@ -2339,7 +2344,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig.toScheduleConditionId(si), new ZenPolicy.Builder().build(), NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id = mZenModeHelper.addAutomaticZenRule("pkgname", zenRule, UPDATE_ORIGIN_APP, + String id = mZenModeHelper.addAutomaticZenRule("pkgname", zenRule, ORIGIN_APP, "test", CUSTOM_PKG_UID); assertNotNull(id); } @@ -2350,7 +2355,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), new ZenPolicy.Builder().build(), NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id = mZenModeHelper.addAutomaticZenRule("pkgname", zenRule, UPDATE_ORIGIN_APP, + String id = mZenModeHelper.addAutomaticZenRule("pkgname", zenRule, ORIGIN_APP, "test", CUSTOM_PKG_UID); fail("allowed too many rules to be created"); } catch (IllegalArgumentException e) { @@ -2371,7 +2376,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig.toScheduleConditionId(si), new ZenPolicy.Builder().build(), NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id = mZenModeHelper.addAutomaticZenRule("pkgname", zenRule, UPDATE_ORIGIN_APP, + String id = mZenModeHelper.addAutomaticZenRule("pkgname", zenRule, ORIGIN_APP, "test", CUSTOM_PKG_UID); assertNotNull(id); } @@ -2382,7 +2387,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), new ZenPolicy.Builder().build(), NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id = mZenModeHelper.addAutomaticZenRule("pkgname", zenRule, UPDATE_ORIGIN_APP, + String id = mZenModeHelper.addAutomaticZenRule("pkgname", zenRule, ORIGIN_APP, "test", CUSTOM_PKG_UID); fail("allowed too many rules to be created"); } catch (IllegalArgumentException e) { @@ -2399,7 +2404,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { new ZenPolicy.Builder().build(), NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); String id = mZenModeHelper.addAutomaticZenRule("android", zenRule, - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, "test", SYSTEM_UID); assertTrue(id != null); ZenModeConfig.ZenRule ruleInConfig = mZenModeHelper.mConfig.automaticRules.get(id); @@ -2420,7 +2425,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); String id = mZenModeHelper.addAutomaticZenRule("android", zenRule, - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, "test", SYSTEM_UID); assertTrue(id != null); ZenModeConfig.ZenRule ruleInConfig = mZenModeHelper.mConfig.automaticRules.get(id); @@ -2446,7 +2451,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); String id1 = mZenModeHelper.addAutomaticZenRule("android", zenRule1, - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, "test", SYSTEM_UID); // Zen rule with partially-filled policy: should get all of the filled fields set, and the // rest filled with default state @@ -2461,7 +2466,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { .build(), NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); String id2 = mZenModeHelper.addAutomaticZenRule("android", zenRule2, - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, "test", SYSTEM_UID); // rule 1 should exist assertThat(id1).isNotNull(); @@ -2506,11 +2511,11 @@ public class ZenModeHelperTest extends UiServiceTestCase { new ZenPolicy.Builder().build(), NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id = mZenModeHelper.addAutomaticZenRule(null, zenRule, UPDATE_ORIGIN_APP, "test", + String id = mZenModeHelper.addAutomaticZenRule(null, zenRule, ORIGIN_APP, "test", CUSTOM_PKG_UID); mZenModeHelper.setAutomaticZenRuleState(zenRule.getConditionId(), new Condition(zenRule.getConditionId(), "", STATE_TRUE), - UPDATE_ORIGIN_APP, + ORIGIN_APP, CUSTOM_PKG_UID); ZenModeConfig.ZenRule ruleInConfig = mZenModeHelper.mConfig.automaticRules.get(id); @@ -2526,7 +2531,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { new ZenPolicy.Builder().build(), NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id = mZenModeHelper.addAutomaticZenRule(null, zenRule, UPDATE_ORIGIN_APP, "test", + String id = mZenModeHelper.addAutomaticZenRule(null, zenRule, ORIGIN_APP, "test", CUSTOM_PKG_UID); AutomaticZenRule zenRule2 = new AutomaticZenRule("NEW", @@ -2536,7 +2541,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { new ZenPolicy.Builder().build(), NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - mZenModeHelper.updateAutomaticZenRule(id, zenRule2, UPDATE_ORIGIN_APP, "", CUSTOM_PKG_UID); + mZenModeHelper.updateAutomaticZenRule(id, zenRule2, ORIGIN_APP, "", CUSTOM_PKG_UID); ZenModeConfig.ZenRule ruleInConfig = mZenModeHelper.mConfig.automaticRules.get(id); assertEquals("NEW", ruleInConfig.name); @@ -2551,7 +2556,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { new ZenPolicy.Builder().build(), NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id = mZenModeHelper.addAutomaticZenRule(null, zenRule, UPDATE_ORIGIN_APP, "test", + String id = mZenModeHelper.addAutomaticZenRule(null, zenRule, ORIGIN_APP, "test", CUSTOM_PKG_UID); assertTrue(id != null); @@ -2559,7 +2564,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertTrue(ruleInConfig != null); assertEquals(zenRule.getName(), ruleInConfig.name); - mZenModeHelper.removeAutomaticZenRule(id, UPDATE_ORIGIN_APP, "test", CUSTOM_PKG_UID); + mZenModeHelper.removeAutomaticZenRule(id, ORIGIN_APP, "test", CUSTOM_PKG_UID); assertNull(mZenModeHelper.mConfig.automaticRules.get(id)); } @@ -2571,7 +2576,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), new ZenPolicy.Builder().build(), NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id = mZenModeHelper.addAutomaticZenRule(null, zenRule, UPDATE_ORIGIN_APP, "test", + String id = mZenModeHelper.addAutomaticZenRule(null, zenRule, ORIGIN_APP, "test", CUSTOM_PKG_UID); assertTrue(id != null); @@ -2579,7 +2584,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertTrue(ruleInConfig != null); assertEquals(zenRule.getName(), ruleInConfig.name); - mZenModeHelper.removeAutomaticZenRules(mContext.getPackageName(), UPDATE_ORIGIN_APP, "test", + mZenModeHelper.removeAutomaticZenRules(mContext.getPackageName(), ORIGIN_APP, "test", CUSTOM_PKG_UID); assertNull(mZenModeHelper.mConfig.automaticRules.get(id)); } @@ -2596,17 +2601,17 @@ public class ZenModeHelperTest extends UiServiceTestCase { sharedUri, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); String id = mZenModeHelper.addAutomaticZenRule(mPkg, zenRule, - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, "test", SYSTEM_UID); AutomaticZenRule zenRule2 = new AutomaticZenRule("name2", new ComponentName(mPkg, "ScheduleConditionProvider"), sharedUri, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); String id2 = mZenModeHelper.addAutomaticZenRule(mPkg, zenRule2, - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, "test", SYSTEM_UID); Condition condition = new Condition(sharedUri, "", STATE_TRUE); mZenModeHelper.setAutomaticZenRuleState(sharedUri, condition, - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, SYSTEM_UID); for (ZenModeConfig.ZenRule rule : mZenModeHelper.mConfig.automaticRules.values()) { if (rule.id.equals(id)) { @@ -2621,7 +2626,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { condition = new Condition(sharedUri, "", STATE_FALSE); mZenModeHelper.setAutomaticZenRuleState(sharedUri, condition, - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, SYSTEM_UID); for (ZenModeConfig.ZenRule rule : mZenModeHelper.mConfig.automaticRules.values()) { if (rule.id.equals(id)) { @@ -2656,7 +2661,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setOwner(OWNER) .setDeviceEffects(zde) .build(), - UPDATE_ORIGIN_APP, "reasons", 0); + ORIGIN_APP, "reasons", 0); AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(ruleId); assertThat(savedRule.getDeviceEffects()).isEqualTo( @@ -2689,7 +2694,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setOwner(OWNER) .setDeviceEffects(zde) .build(), - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "reasons", 0); + ZenModeConfig.ORIGIN_SYSTEM, "reasons", 0); AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(ruleId); assertThat(savedRule.getDeviceEffects()).isEqualTo(zde); @@ -2716,7 +2721,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setOwner(OWNER) .setDeviceEffects(zde) .build(), - UPDATE_ORIGIN_USER, + ORIGIN_USER_IN_SYSTEMUI, "reasons", 0); AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(ruleId); @@ -2736,7 +2741,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setOwner(OWNER) .setDeviceEffects(original) .build(), - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "reasons", 0); + ZenModeConfig.ORIGIN_SYSTEM, "reasons", 0); ZenDeviceEffects updateFromApp = new ZenDeviceEffects.Builder() .setShouldUseNightMode(true) // Good @@ -2748,7 +2753,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setOwner(OWNER) .setDeviceEffects(updateFromApp) .build(), - UPDATE_ORIGIN_APP, "reasons", 0); + ORIGIN_APP, "reasons", 0); AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(ruleId); assertThat(savedRule.getDeviceEffects()).isEqualTo( @@ -2770,7 +2775,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setOwner(OWNER) .setDeviceEffects(original) .build(), - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "reasons", 0); + ZenModeConfig.ORIGIN_SYSTEM, "reasons", 0); ZenDeviceEffects updateFromSystem = new ZenDeviceEffects.Builder() .setShouldUseNightMode(true) // Good @@ -2780,7 +2785,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { new AutomaticZenRule.Builder("Rule", CONDITION_ID) .setDeviceEffects(updateFromSystem) .build(), - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "reasons", 0); + ZenModeConfig.ORIGIN_SYSTEM, "reasons", 0); AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(ruleId); assertThat(savedRule.getDeviceEffects()).isEqualTo(updateFromSystem); @@ -2797,7 +2802,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setOwner(OWNER) .setDeviceEffects(original) .build(), - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "reasons", 0); + ZenModeConfig.ORIGIN_SYSTEM, "reasons", 0); ZenDeviceEffects updateFromUser = new ZenDeviceEffects.Builder() .setShouldUseNightMode(true) @@ -2810,7 +2815,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { new AutomaticZenRule.Builder("Rule", CONDITION_ID) .setDeviceEffects(updateFromUser) .build(), - UPDATE_ORIGIN_USER, "reasons", 0); + ORIGIN_USER_IN_SYSTEMUI, "reasons", 0); AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(ruleId); @@ -2829,13 +2834,13 @@ public class ZenModeHelperTest extends UiServiceTestCase { .allowCalls(ZenPolicy.PEOPLE_TYPE_NONE) // default is stars .build()) .build(), - UPDATE_ORIGIN_APP, "reasons", 0); + ORIGIN_APP, "reasons", 0); mZenModeHelper.updateAutomaticZenRule(ruleId, new AutomaticZenRule.Builder("Rule", CONDITION_ID) // no zen policy .build(), - UPDATE_ORIGIN_APP, "reasons", 0); + ORIGIN_APP, "reasons", 0); AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(ruleId); assertThat(savedRule.getZenPolicy().getPriorityCategoryCalls()) @@ -2857,7 +2862,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { .allowReminders(true) .build()) .build(), - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "reasons", 0); + ZenModeConfig.ORIGIN_SYSTEM, "reasons", 0); mZenModeHelper.updateAutomaticZenRule(ruleId, new AutomaticZenRule.Builder("Rule", CONDITION_ID) @@ -2865,7 +2870,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { .allowCalls(ZenPolicy.PEOPLE_TYPE_CONTACTS) .build()) .build(), - UPDATE_ORIGIN_APP, "reasons", 0); + ORIGIN_APP, "reasons", 0); AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(ruleId); assertThat(savedRule.getZenPolicy().getPriorityCategoryCalls()) @@ -2893,7 +2898,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { AutomaticZenRule bedtime = new AutomaticZenRule.Builder("Bedtime Mode (TM)", CONDITION_ID) .setType(TYPE_BEDTIME) .build(); - String bedtimeRuleId = mZenModeHelper.addAutomaticZenRule("pkg", bedtime, UPDATE_ORIGIN_APP, + String bedtimeRuleId = mZenModeHelper.addAutomaticZenRule("pkg", bedtime, + ORIGIN_APP, "reason", CUSTOM_PKG_UID); assertThat(mZenModeHelper.mConfig.automaticRules.keySet()).containsExactly(bedtimeRuleId); @@ -2913,7 +2919,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { AutomaticZenRule bedtime = new AutomaticZenRule.Builder("Bedtime Mode (TM)", CONDITION_ID) .setType(TYPE_BEDTIME) .build(); - String bedtimeRuleId = mZenModeHelper.addAutomaticZenRule("pkg", bedtime, UPDATE_ORIGIN_APP, + String bedtimeRuleId = mZenModeHelper.addAutomaticZenRule("pkg", bedtime, + ORIGIN_APP, "reason", CUSTOM_PKG_UID); assertThat(mZenModeHelper.mConfig.automaticRules.keySet()).containsExactly( @@ -2934,7 +2941,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { AutomaticZenRule bedtime = new AutomaticZenRule.Builder("Bedtime Mode (TM)", CONDITION_ID) .setType(TYPE_BEDTIME) .build(); - String bedtimeRuleId = mZenModeHelper.addAutomaticZenRule("pkg", bedtime, UPDATE_ORIGIN_APP, + String bedtimeRuleId = mZenModeHelper.addAutomaticZenRule("pkg", bedtime, + ORIGIN_APP, "reason", CUSTOM_PKG_UID); assertThat(mZenModeHelper.mConfig.automaticRules.keySet()).containsExactly( @@ -2948,15 +2956,15 @@ public class ZenModeHelperTest extends UiServiceTestCase { // note that caller=null because that's how it comes in from NMS.setZenMode mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "", null, Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, "", null, SYSTEM_UID); // confirm that setting zen mode via setManualZenMode changed the zen mode correctly assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, mZenModeHelper.mZenMode); assertEquals(true, mZenModeHelper.mConfig.manualRule.allowManualInvocation); // and also that it works to turn it back off again - mZenModeHelper.setManualZenMode(Global.ZEN_MODE_OFF, null, UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, - "", null, Process.SYSTEM_UID); + mZenModeHelper.setManualZenMode(Global.ZEN_MODE_OFF, null, ZenModeConfig.ORIGIN_SYSTEM, + "", null, SYSTEM_UID); assertEquals(Global.ZEN_MODE_OFF, mZenModeHelper.mZenMode); } @@ -2965,28 +2973,29 @@ public class ZenModeHelperTest extends UiServiceTestCase { @EnableFlags(FLAG_MODES_API) @DisableFlags(FLAG_MODES_UI) public void setManualZenMode_off_snoozesActiveRules() { - for (ChangeOrigin origin : ChangeOrigin.values()) { + for (ZenChangeOrigin origin : ZenChangeOrigin.values()) { // Start with an active rule and an inactive rule. mZenModeHelper.mConfig.automaticRules.clear(); AutomaticZenRule activeRule = new AutomaticZenRule.Builder("Test", CONDITION_ID) .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) .build(); String activeRuleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - activeRule, UPDATE_ORIGIN_APP, "add it", CUSTOM_PKG_UID); - mZenModeHelper.setAutomaticZenRuleState(activeRuleId, CONDITION_TRUE, UPDATE_ORIGIN_APP, + activeRule, ORIGIN_APP, "add it", CUSTOM_PKG_UID); + mZenModeHelper.setAutomaticZenRuleState(activeRuleId, CONDITION_TRUE, + ORIGIN_APP, CUSTOM_PKG_UID); AutomaticZenRule inactiveRule = new AutomaticZenRule.Builder("Test", CONDITION_ID) .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) .build(); String inactiveRuleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - inactiveRule, UPDATE_ORIGIN_APP, "add it", CUSTOM_PKG_UID); + inactiveRule, ORIGIN_APP, "add it", CUSTOM_PKG_UID); assertWithMessage("Failure for origin " + origin.name()) .that(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_IMPORTANT_INTERRUPTIONS); // User turns DND off. mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null, origin.value(), - "snoozing", "systemui", Process.SYSTEM_UID); + "snoozing", "systemui", SYSTEM_UID); assertWithMessage("Failure for origin " + origin.name()) .that(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF); assertWithMessage("Failure for origin " + origin.name()) @@ -3000,30 +3009,31 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) - public void setManualZenMode_off_doesNotSnoozeRulesIfFromUser() { - for (ChangeOrigin origin : ChangeOrigin.values()) { + public void setManualZenMode_off_doesNotSnoozeRulesIfFromUserInSystemUi() { + for (ZenChangeOrigin origin : ZenChangeOrigin.values()) { // Start with an active rule and an inactive rule mZenModeHelper.mConfig.automaticRules.clear(); AutomaticZenRule activeRule = new AutomaticZenRule.Builder("Test", CONDITION_ID) .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) .build(); String activeRuleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - activeRule, UPDATE_ORIGIN_APP, "add it", CUSTOM_PKG_UID); - mZenModeHelper.setAutomaticZenRuleState(activeRuleId, CONDITION_TRUE, UPDATE_ORIGIN_APP, + activeRule, ORIGIN_APP, "add it", CUSTOM_PKG_UID); + mZenModeHelper.setAutomaticZenRuleState(activeRuleId, CONDITION_TRUE, + ORIGIN_APP, CUSTOM_PKG_UID); AutomaticZenRule inactiveRule = new AutomaticZenRule.Builder("Test", CONDITION_ID) .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) .build(); String inactiveRuleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - inactiveRule, UPDATE_ORIGIN_APP, "add it", CUSTOM_PKG_UID); + inactiveRule, ORIGIN_APP, "add it", CUSTOM_PKG_UID); assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_IMPORTANT_INTERRUPTIONS); // User turns DND off. mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null, origin.value(), - "snoozing", "systemui", Process.SYSTEM_UID); + "snoozing", "systemui", SYSTEM_UID); ZenModeConfig config = mZenModeHelper.mConfig; - if (origin == ChangeOrigin.ORIGIN_USER) { + if (origin == ZenChangeOrigin.ORIGIN_USER_IN_SYSTEMUI) { // Other rule was unaffected. assertWithMessage("Failure for origin " + origin.name()).that( mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_IMPORTANT_INTERRUPTIONS); @@ -3048,14 +3058,14 @@ public class ZenModeHelperTest extends UiServiceTestCase { // note that caller=null because that's how it comes in from NMS.setZenMode mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "", null, Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, "", null, SYSTEM_UID); // confirm that setting zen mode via setManualZenMode changed the zen mode correctly assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, mZenModeHelper.mZenMode); // and also that it works to turn it back off again - mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null, UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "", - null, Process.SYSTEM_UID); + mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null, ZenModeConfig.ORIGIN_SYSTEM, "", + null, SYSTEM_UID); assertEquals(ZEN_MODE_OFF, mZenModeHelper.mZenMode); } @@ -3069,12 +3079,12 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Turn zen mode on (to important_interruptions) // Need to additionally call the looper in order to finish the post-apply-config process mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, - Flags.modesApi() ? UPDATE_ORIGIN_USER : UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "", null, - Process.SYSTEM_UID); + Flags.modesApi() ? ORIGIN_USER_IN_SYSTEMUI : ZenModeConfig.ORIGIN_SYSTEM, "", + null, SYSTEM_UID); // Now turn zen mode off, but via a different package UID -- this should get registered as // "not an action by the user" because some other app is changing zen mode - mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null, UPDATE_ORIGIN_APP, "", null, + mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null, ORIGIN_APP, "", null, CUSTOM_PKG_UID); // In total, this should be 2 loggable changes @@ -3100,11 +3110,11 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertThat(mZenModeEventLogger.getFromSystemOrSystemUi(0)).isEqualTo( !(Flags.modesUi() || Flags.modesApi())); assertTrue(mZenModeEventLogger.getIsUserAction(0)); - assertEquals(Process.SYSTEM_UID, mZenModeEventLogger.getPackageUid(0)); + assertEquals(SYSTEM_UID, mZenModeEventLogger.getPackageUid(0)); checkDndProtoMatchesSetupZenConfig(mZenModeEventLogger.getPolicyProto(0)); // change origin should be populated only under modes_ui assertThat(mZenModeEventLogger.getChangeOrigin(0)).isEqualTo( - (Flags.modesApi() && Flags.modesUi()) ? UPDATE_ORIGIN_USER : 0); + (Flags.modesApi() && Flags.modesUi()) ? ORIGIN_USER_IN_SYSTEMUI : 0); // and from turning zen mode off: // - event ID: DND_TURNED_OFF @@ -3128,7 +3138,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { checkDndProtoMatchesSetupZenConfig(mZenModeEventLogger.getPolicyProto(1)); } assertThat(mZenModeEventLogger.getChangeOrigin(1)).isEqualTo( - Flags.modesUi() ? UPDATE_ORIGIN_APP : 0); + Flags.modesUi() ? ORIGIN_APP : 0); } @Test @@ -3145,7 +3155,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { null, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule, - UPDATE_ORIGIN_APP, "test", CUSTOM_PKG_UID); + ORIGIN_APP, "test", CUSTOM_PKG_UID); // Event 1: Mimic the rule coming on automatically by setting the Condition to STATE_TRUE // Note that pre-modes_ui, this event serves as a test that automatic changes to an app's @@ -3153,14 +3163,14 @@ public class ZenModeHelperTest extends UiServiceTestCase { // modes_ui is true, we opt to trust the provided change origin. mZenModeHelper.setAutomaticZenRuleState(id, new Condition(zenRule.getConditionId(), "", STATE_TRUE), - Flags.modesUi() ? UPDATE_ORIGIN_APP : UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, + Flags.modesUi() ? ORIGIN_APP : ZenModeConfig.ORIGIN_SYSTEM, CUSTOM_PKG_UID); // Event 2: "User" turns off the automatic rule (sets it to not enabled) zenRule.setEnabled(false); mZenModeHelper.updateAutomaticZenRule(id, zenRule, - Flags.modesApi() ? UPDATE_ORIGIN_USER : UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "", - Process.SYSTEM_UID); + Flags.modesApi() ? ORIGIN_USER_IN_SYSTEMUI : ZenModeConfig.ORIGIN_SYSTEM, "", + SYSTEM_UID); AutomaticZenRule systemRule = new AutomaticZenRule("systemRule", null, @@ -3169,18 +3179,18 @@ public class ZenModeHelperTest extends UiServiceTestCase { null, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); String systemId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), systemRule, - Flags.modesApi() ? UPDATE_ORIGIN_USER : UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", - Process.SYSTEM_UID); + Flags.modesApi() ? ORIGIN_USER_IN_SYSTEMUI : ZenModeConfig.ORIGIN_SYSTEM, "test", + SYSTEM_UID); // Event 3: turn on the system rule mZenModeHelper.setAutomaticZenRuleState(systemId, new Condition(zenRule.getConditionId(), "", STATE_TRUE), - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, SYSTEM_UID); // Event 4: "User" deletes the rule mZenModeHelper.removeAutomaticZenRule(systemId, - Flags.modesApi() ? UPDATE_ORIGIN_USER : UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "", - Process.SYSTEM_UID); + Flags.modesApi() ? ORIGIN_USER_IN_SYSTEMUI : ZenModeConfig.ORIGIN_SYSTEM, "", + SYSTEM_UID); // In total, this represents 4 events assertEquals(4, mZenModeEventLogger.numLoggedChanges()); @@ -3203,7 +3213,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertEquals(CUSTOM_PKG_UID, mZenModeEventLogger.getPackageUid(0)); checkDndProtoMatchesDefaultZenConfig(mZenModeEventLogger.getPolicyProto(0)); assertThat(mZenModeEventLogger.getChangeOrigin(0)).isEqualTo( - Flags.modesUi() ? UPDATE_ORIGIN_APP : 0); + Flags.modesUi() ? ORIGIN_APP : 0); // When the automatic rule is disabled, this should turn off zen mode and also count as a // user action. We don't care what the consolidated policy is when DND turns off. @@ -3217,14 +3227,14 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertEquals(0, mZenModeEventLogger.getNumRulesActive(1)); assertTrue(mZenModeEventLogger.getIsUserAction(1)); assertThat(mZenModeEventLogger.getPackageUid(1)).isEqualTo( - Flags.modesUi() ? CUSTOM_PKG_UID : Process.SYSTEM_UID); + Flags.modesUi() ? CUSTOM_PKG_UID : SYSTEM_UID); if (Flags.modesApi()) { assertThat(mZenModeEventLogger.getPolicyProto(1)).isNull(); } else { checkDndProtoMatchesSetupZenConfig(mZenModeEventLogger.getPolicyProto(1)); } assertThat(mZenModeEventLogger.getChangeOrigin(1)).isEqualTo( - Flags.modesUi() ? UPDATE_ORIGIN_USER : 0); + Flags.modesUi() ? ORIGIN_USER_IN_SYSTEMUI : 0); // When the system rule is enabled, this counts as an automatic action that comes from the // system and turns on DND @@ -3233,9 +3243,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertEquals(DNDProtoEnums.AUTOMATIC_RULE, mZenModeEventLogger.getChangedRuleType(2)); assertEquals(1, mZenModeEventLogger.getNumRulesActive(2)); assertFalse(mZenModeEventLogger.getIsUserAction(2)); - assertEquals(Process.SYSTEM_UID, mZenModeEventLogger.getPackageUid(2)); + assertEquals(SYSTEM_UID, mZenModeEventLogger.getPackageUid(2)); assertThat(mZenModeEventLogger.getChangeOrigin(2)).isEqualTo( - Flags.modesUi() ? UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI : 0); + Flags.modesUi() ? ZenModeConfig.ORIGIN_SYSTEM : 0); // When the system rule is deleted, we consider this a user action that turns DND off // (again) @@ -3244,9 +3254,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertEquals(DNDProtoEnums.AUTOMATIC_RULE, mZenModeEventLogger.getChangedRuleType(3)); assertEquals(0, mZenModeEventLogger.getNumRulesActive(3)); assertTrue(mZenModeEventLogger.getIsUserAction(3)); - assertEquals(Process.SYSTEM_UID, mZenModeEventLogger.getPackageUid(3)); + assertEquals(SYSTEM_UID, mZenModeEventLogger.getPackageUid(3)); assertThat(mZenModeEventLogger.getChangeOrigin(3)).isEqualTo( - Flags.modesUi() ? UPDATE_ORIGIN_USER : 0); + Flags.modesUi() ? ORIGIN_USER_IN_SYSTEMUI : 0); } @Test @@ -3264,28 +3274,28 @@ public class ZenModeHelperTest extends UiServiceTestCase { null, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule, - UPDATE_ORIGIN_APP, "test", CUSTOM_PKG_UID); + ORIGIN_APP, "test", CUSTOM_PKG_UID); // Event 1: Mimic the rule coming on manually when the user turns it on in the app // ("Turn on bedtime now" because user goes to bed earlier). mZenModeHelper.setAutomaticZenRuleState(id, new Condition(zenRule.getConditionId(), "", STATE_TRUE, SOURCE_USER_ACTION), - UPDATE_ORIGIN_USER, CUSTOM_PKG_UID); + ORIGIN_USER_IN_APP, CUSTOM_PKG_UID); // Event 2: App deactivates the rule automatically (it's 8 AM, bedtime schedule ends) mZenModeHelper.setAutomaticZenRuleState(id, new Condition(zenRule.getConditionId(), "", STATE_FALSE, SOURCE_SCHEDULE), - UPDATE_ORIGIN_APP, CUSTOM_PKG_UID); + ORIGIN_APP, CUSTOM_PKG_UID); // Event 3: App activates the rule automatically (it's now 11 PM, bedtime schedule starts) mZenModeHelper.setAutomaticZenRuleState(id, new Condition(zenRule.getConditionId(), "", STATE_TRUE, SOURCE_SCHEDULE), - UPDATE_ORIGIN_APP, CUSTOM_PKG_UID); + ORIGIN_APP, CUSTOM_PKG_UID); // Event 4: User deactivates the rule manually (they get up before 8 AM on the next day) mZenModeHelper.setAutomaticZenRuleState(id, new Condition(zenRule.getConditionId(), "", STATE_FALSE, SOURCE_USER_ACTION), - UPDATE_ORIGIN_USER, CUSTOM_PKG_UID); + ORIGIN_USER_IN_APP, CUSTOM_PKG_UID); // In total, this represents 4 events assertEquals(4, mZenModeEventLogger.numLoggedChanges()); @@ -3301,7 +3311,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertTrue(mZenModeEventLogger.getIsUserAction(0)); assertEquals(CUSTOM_PKG_UID, mZenModeEventLogger.getPackageUid(0)); assertThat(mZenModeEventLogger.getChangeOrigin(0)).isEqualTo( - Flags.modesUi() ? UPDATE_ORIGIN_USER : 0); + Flags.modesUi() ? ORIGIN_USER_IN_APP : 0); // Automatic rule turned off automatically by app: // - event ID: DND_TURNED_OFF @@ -3314,7 +3324,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertFalse(mZenModeEventLogger.getIsUserAction(1)); assertEquals(CUSTOM_PKG_UID, mZenModeEventLogger.getPackageUid(1)); assertThat(mZenModeEventLogger.getChangeOrigin(1)).isEqualTo( - Flags.modesUi() ? UPDATE_ORIGIN_APP : 0); + Flags.modesUi() ? ORIGIN_APP : 0); // Automatic rule turned on automatically by app: // - event ID: DND_TURNED_ON @@ -3328,7 +3338,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertFalse(mZenModeEventLogger.getIsUserAction(2)); assertEquals(CUSTOM_PKG_UID, mZenModeEventLogger.getPackageUid(2)); assertThat(mZenModeEventLogger.getChangeOrigin(2)).isEqualTo( - Flags.modesUi() ? UPDATE_ORIGIN_APP : 0); + Flags.modesUi() ? ORIGIN_APP : 0); // Automatic rule turned off automatically by the user: // - event ID: DND_TURNED_ON @@ -3341,7 +3351,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertTrue(mZenModeEventLogger.getIsUserAction(3)); assertEquals(CUSTOM_PKG_UID, mZenModeEventLogger.getPackageUid(3)); assertThat(mZenModeEventLogger.getChangeOrigin(3)).isEqualTo( - Flags.modesUi() ? UPDATE_ORIGIN_USER : 0); + Flags.modesUi() ? ORIGIN_USER_IN_APP : 0); } @Test @@ -3352,21 +3362,21 @@ public class ZenModeHelperTest extends UiServiceTestCase { // First just turn zen mode on mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, - UPDATE_ORIGIN_USER, "", null, Process.SYSTEM_UID); + ORIGIN_USER_IN_SYSTEMUI, "", null, SYSTEM_UID); // Now change the policy slightly; want to confirm that this'll be reflected in the logs ZenModeConfig newConfig = mZenModeHelper.mConfig.copy(); mZenModeHelper.setNotificationPolicy(new Policy(PRIORITY_CATEGORY_ALARMS, 0, 0), - UPDATE_ORIGIN_USER, Process.SYSTEM_UID); + ORIGIN_USER_IN_SYSTEMUI, SYSTEM_UID); // Turn zen mode off; we want to make sure policy changes do not get logged when zen mode // is off. - mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null, UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "", - null, Process.SYSTEM_UID); + mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null, ZenModeConfig.ORIGIN_SYSTEM, "", + null, SYSTEM_UID); // Change the policy again mZenModeHelper.setNotificationPolicy(new Policy(PRIORITY_CATEGORY_REPEAT_CALLERS, 0, 0), - UPDATE_ORIGIN_USER, Process.SYSTEM_UID); + ORIGIN_USER_IN_SYSTEMUI, SYSTEM_UID); // Total events: we only expect ones for turning on, changing policy, and turning off assertEquals(3, mZenModeEventLogger.numLoggedChanges()); @@ -3386,7 +3396,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeEventLogger.getEventId(1)); assertEquals(DNDProtoEnums.UNKNOWN_RULE, mZenModeEventLogger.getChangedRuleType(1)); assertTrue(mZenModeEventLogger.getIsUserAction(1)); - assertEquals(Process.SYSTEM_UID, mZenModeEventLogger.getPackageUid(1)); + assertEquals(SYSTEM_UID, mZenModeEventLogger.getPackageUid(1)); DNDPolicyProto dndProto = mZenModeEventLogger.getPolicyProto(1); assertEquals(STATE_ALLOW, dndProto.getAlarms().getNumber()); assertEquals(STATE_DISALLOW, dndProto.getRepeatCallers().getNumber()); @@ -3410,7 +3420,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { null, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule, - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, "test", SYSTEM_UID); // Rule 2, same as rule 1 AutomaticZenRule zenRule2 = new AutomaticZenRule("name2", @@ -3420,7 +3430,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { null, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); String id2 = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule2, - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, "test", SYSTEM_UID); // Rule 3, has stricter settings than the default settings ZenModeConfig ruleConfig = mZenModeHelper.mConfig.copy(); @@ -3432,27 +3442,27 @@ public class ZenModeHelperTest extends UiServiceTestCase { ruleConfig.getZenPolicy(), NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); String id3 = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule3, - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, "test", SYSTEM_UID); // First: turn on rule 1 mZenModeHelper.setAutomaticZenRuleState(id, new Condition(zenRule.getConditionId(), "", STATE_TRUE), - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, SYSTEM_UID); // Second: turn on rule 2 mZenModeHelper.setAutomaticZenRuleState(id2, new Condition(zenRule2.getConditionId(), "", STATE_TRUE), - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, SYSTEM_UID); // Third: turn on rule 3 mZenModeHelper.setAutomaticZenRuleState(id3, new Condition(zenRule3.getConditionId(), "", STATE_TRUE), - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, SYSTEM_UID); // Fourth: Turn *off* rule 2 mZenModeHelper.setAutomaticZenRuleState(id2, new Condition(zenRule2.getConditionId(), "", STATE_FALSE), - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, SYSTEM_UID); // This should result in a total of four events assertEquals(4, mZenModeEventLogger.numLoggedChanges()); @@ -3465,7 +3475,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, mZenModeEventLogger.getNewZenMode(0)); assertEquals(1, mZenModeEventLogger.getNumRulesActive(0)); assertFalse(mZenModeEventLogger.getIsUserAction(0)); - assertEquals(Process.SYSTEM_UID, mZenModeEventLogger.getPackageUid(0)); + assertEquals(SYSTEM_UID, mZenModeEventLogger.getPackageUid(0)); checkDndProtoMatchesDefaultZenConfig(mZenModeEventLogger.getPolicyProto(0)); // Event 2: rule 2 turns on. This should not change anything about the policy, so the only @@ -3474,7 +3484,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeEventLogger.getEventId(1)); assertEquals(2, mZenModeEventLogger.getNumRulesActive(1)); assertFalse(mZenModeEventLogger.getIsUserAction(1)); - assertEquals(Process.SYSTEM_UID, mZenModeEventLogger.getPackageUid(1)); + assertEquals(SYSTEM_UID, mZenModeEventLogger.getPackageUid(1)); checkDndProtoMatchesDefaultZenConfig(mZenModeEventLogger.getPolicyProto(1)); // Event 3: rule 3 turns on. This should trigger a policy change, and be classified as such, @@ -3484,7 +3494,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeEventLogger.getEventId(2)); assertEquals(3, mZenModeEventLogger.getNumRulesActive(2)); assertFalse(mZenModeEventLogger.getIsUserAction(2)); - assertEquals(Process.SYSTEM_UID, mZenModeEventLogger.getPackageUid(2)); + assertEquals(SYSTEM_UID, mZenModeEventLogger.getPackageUid(2)); DNDPolicyProto dndProto = mZenModeEventLogger.getPolicyProto(2); assertEquals(STATE_DISALLOW, dndProto.getReminders().getNumber()); assertEquals(STATE_DISALLOW, dndProto.getCalls().getNumber()); @@ -3508,7 +3518,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Artificially turn zen mode "on". Re-evaluating zen mode should cause it to turn back off // given that we don't have any zen rules active. mZenModeHelper.mZenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS; - mZenModeHelper.evaluateZenModeLocked(UPDATE_ORIGIN_UNKNOWN, "test", true); + mZenModeHelper.evaluateZenModeLocked(ORIGIN_UNKNOWN, "test", true); // Check that the change actually took: zen mode should be off now assertEquals(ZEN_MODE_OFF, mZenModeHelper.mZenMode); @@ -3537,7 +3547,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { manualRulePolicy, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule, - UPDATE_ORIGIN_APP, "test", Process.SYSTEM_UID); + ORIGIN_APP, "test", SYSTEM_UID); // Rule 2, same as rule 1 but owned by the system AutomaticZenRule zenRule2 = new AutomaticZenRule("name2", @@ -3547,7 +3557,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { manualRulePolicy, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); String id2 = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule2, - UPDATE_ORIGIN_USER, "test", Process.SYSTEM_UID); + ORIGIN_USER_IN_SYSTEMUI, "test", SYSTEM_UID); // Turn on rule 1; call looks like it's from the system. Because setting a condition is // typically an automatic (non-user-initiated) action, expect the calling UID to be @@ -3555,23 +3565,24 @@ public class ZenModeHelperTest extends UiServiceTestCase { // When modes_ui is true: we expect the change origin to be the source of truth. mZenModeHelper.setAutomaticZenRuleState(id, new Condition(zenRule.getConditionId(), "", STATE_TRUE), - Flags.modesUi() ? UPDATE_ORIGIN_APP : UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, - Process.SYSTEM_UID); + Flags.modesUi() ? ORIGIN_APP : ZenModeConfig.ORIGIN_SYSTEM, + SYSTEM_UID); // Second: turn on rule 2. This is a system-owned rule and the UID should not be modified // (nor even looked up; the mock PackageManager won't handle "android" as input). mZenModeHelper.setAutomaticZenRuleState(id2, new Condition(zenRule2.getConditionId(), "", STATE_TRUE), - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, SYSTEM_UID); // Disable rule 1. Because this looks like a user action, the UID should not be modified // from the system-provided one unless modes_ui is true. zenRule.setEnabled(false); mZenModeHelper.updateAutomaticZenRule(id, zenRule, - UPDATE_ORIGIN_USER, "", Process.SYSTEM_UID); + ORIGIN_USER_IN_SYSTEMUI, "", SYSTEM_UID); // Add a manual rule. Any manual rule changes should not get calling uids reassigned. - mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, UPDATE_ORIGIN_APP, + mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, + ORIGIN_APP, "", null, CUSTOM_PKG_UID); // Change rule 2's condition, but from some other UID. Since it doesn't look like it's from @@ -3579,7 +3590,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Note that this probably shouldn't be able to occur in real scenarios. mZenModeHelper.setAutomaticZenRuleState(id2, new Condition(zenRule2.getConditionId(), "", STATE_FALSE), - UPDATE_ORIGIN_APP, 12345); + ORIGIN_APP, 12345); // That was 5 events total assertEquals(5, mZenModeEventLogger.numLoggedChanges()); @@ -3598,7 +3609,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeEventLogger.getEventId(1)); assertEquals(DNDProtoEnums.AUTOMATIC_RULE, mZenModeEventLogger.getChangedRuleType(1)); assertFalse(mZenModeEventLogger.getIsUserAction(1)); - assertEquals(Process.SYSTEM_UID, mZenModeEventLogger.getPackageUid(1)); + assertEquals(SYSTEM_UID, mZenModeEventLogger.getPackageUid(1)); // Third event: disable rule 1. This looks like a user action so UID should be left alone. // When modes_ui is true, we assign log this user action with the app that owns the rule. @@ -3607,7 +3618,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertEquals(DNDProtoEnums.AUTOMATIC_RULE, mZenModeEventLogger.getChangedRuleType(2)); assertTrue(mZenModeEventLogger.getIsUserAction(2)); assertThat(mZenModeEventLogger.getPackageUid(2)).isEqualTo( - Flags.modesUi() ? CUSTOM_PKG_UID : Process.SYSTEM_UID); + Flags.modesUi() ? CUSTOM_PKG_UID : SYSTEM_UID); // Fourth event: turns on manual mode. Doesn't change effective policy so this is just a // change in active rules. Confirm that the package UID is left unchanged. @@ -3636,29 +3647,30 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Turn on zen mode with a manual rule with an enabler set. This should *not* count // as a user action, and *should* get its UID reassigned. mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "", CUSTOM_PKG_NAME, Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, "", CUSTOM_PKG_NAME, SYSTEM_UID); assertEquals(1, mZenModeEventLogger.numLoggedChanges()); // Now change apps bypassing to true ZenModeConfig newConfig = mZenModeHelper.mConfig.copy(); newConfig.areChannelsBypassingDnd = true; mZenModeHelper.setNotificationPolicy(newConfig.toNotificationPolicy(), - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, SYSTEM_UID); assertEquals(2, mZenModeEventLogger.numLoggedChanges()); // and then back to false, all without changing anything else newConfig.areChannelsBypassingDnd = false; mZenModeHelper.setNotificationPolicy(newConfig.toNotificationPolicy(), - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, SYSTEM_UID); assertEquals(3, mZenModeEventLogger.numLoggedChanges()); // Turn off manual mode, call from a package: don't reset UID even though enabler is set - mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null, UPDATE_ORIGIN_APP, "", + mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null, ORIGIN_APP, "", CUSTOM_PKG_NAME, 12345); assertEquals(4, mZenModeEventLogger.numLoggedChanges()); // And likewise when turning it back on again - mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, UPDATE_ORIGIN_APP, + mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, + ORIGIN_APP, "", CUSTOM_PKG_NAME, 12345); // These are 5 events in total. @@ -3705,7 +3717,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // First just turn zen mode on mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "", null, Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, "", null, SYSTEM_UID); // Now change only the channels part of the policy; want to confirm that this'll be // reflected in the logs @@ -3716,7 +3728,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { STATE_PRIORITY_CHANNELS_BLOCKED, oldPolicy.priorityConversationSenders); mZenModeHelper.setNotificationPolicy(newPolicy, - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, SYSTEM_UID); // Total events: one for turning on, one for changing policy assertThat(mZenModeEventLogger.numLoggedChanges()).isEqualTo(2); @@ -3757,17 +3769,17 @@ public class ZenModeHelperTest extends UiServiceTestCase { null, NotificationManager.INTERRUPTION_FILTER_ALL, true); String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule, - UPDATE_ORIGIN_APP, "test", CUSTOM_PKG_UID); + ORIGIN_APP, "test", CUSTOM_PKG_UID); // Event 1: App activates the rule automatically. mZenModeHelper.setAutomaticZenRuleState(id, new Condition(zenRule.getConditionId(), "", STATE_TRUE, SOURCE_SCHEDULE), - UPDATE_ORIGIN_APP, CUSTOM_PKG_UID); + ORIGIN_APP, CUSTOM_PKG_UID); // Event 2: App deactivates the rule automatically. mZenModeHelper.setAutomaticZenRuleState(id, new Condition(zenRule.getConditionId(), "", STATE_FALSE, SOURCE_SCHEDULE), - UPDATE_ORIGIN_APP, CUSTOM_PKG_UID); + ORIGIN_APP, CUSTOM_PKG_UID); // In total, this represents 2 events. assertEquals(2, mZenModeEventLogger.numLoggedChanges()); @@ -3798,7 +3810,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setInterruptionFilter(INTERRUPTION_FILTER_ALL) .setType(TYPE_BEDTIME) .build(); - String bedtimeRuleId = mZenModeHelper.addAutomaticZenRule(mPkg, bedtime, UPDATE_ORIGIN_APP, + String bedtimeRuleId = mZenModeHelper.addAutomaticZenRule(mPkg, bedtime, + ORIGIN_APP, "reason", CUSTOM_PKG_UID); // Create immersive rule @@ -3806,27 +3819,28 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setType(TYPE_IMMERSIVE) .setZenPolicy(mZenModeHelper.mConfig.getZenPolicy()) // same as the manual rule .build(); - String immersiveId = mZenModeHelper.addAutomaticZenRule(mPkg, immersive, UPDATE_ORIGIN_APP, + String immersiveId = mZenModeHelper.addAutomaticZenRule(mPkg, immersive, + ORIGIN_APP, "reason", CUSTOM_PKG_UID); // Event 1: Activate bedtime rule. This doesn't turn on notification filtering mZenModeHelper.setAutomaticZenRuleState(bedtimeRuleId, new Condition(bedtime.getConditionId(), "", STATE_TRUE, SOURCE_SCHEDULE), - UPDATE_ORIGIN_APP, CUSTOM_PKG_UID); + ORIGIN_APP, CUSTOM_PKG_UID); // Event 2: turn on manual zen mode. Manual rule will have ACTIVE_RULE_TYPE_MANUAL mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "", null, Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, "", null, SYSTEM_UID); // Event 3: Turn immersive on mZenModeHelper.setAutomaticZenRuleState(immersiveId, new Condition(immersive.getConditionId(), "", STATE_TRUE, SOURCE_SCHEDULE), - UPDATE_ORIGIN_APP, CUSTOM_PKG_UID); + ORIGIN_APP, CUSTOM_PKG_UID); // Event 4: Turn off bedtime mode, leaving just manual + immersive mZenModeHelper.setAutomaticZenRuleState(bedtimeRuleId, new Condition(bedtime.getConditionId(), "", STATE_FALSE, SOURCE_SCHEDULE), - UPDATE_ORIGIN_APP, CUSTOM_PKG_UID); + ORIGIN_APP, CUSTOM_PKG_UID); // Total of 4 events assertEquals(4, mZenModeEventLogger.numLoggedChanges()); @@ -3887,12 +3901,12 @@ public class ZenModeHelperTest extends UiServiceTestCase { null, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule, - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, "test", SYSTEM_UID); // enable the rule mZenModeHelper.setAutomaticZenRuleState(id, new Condition(zenRule.getConditionId(), "", STATE_TRUE), - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, SYSTEM_UID); assertEquals(mZenModeHelper.getNotificationPolicy(), mZenModeHelper.getConsolidatedNotificationPolicy()); @@ -3923,12 +3937,12 @@ public class ZenModeHelperTest extends UiServiceTestCase { null, // null policy NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule, - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, "test", SYSTEM_UID); // enable the rule mZenModeHelper.setAutomaticZenRuleState(id, new Condition(zenRule.getConditionId(), "", STATE_TRUE), - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, SYSTEM_UID); // inspect the consolidated policy, which should match the device default settings. assertThat(ZenAdapters.notificationPolicyToZenPolicy(mZenModeHelper.mConsolidatedPolicy)) @@ -3961,12 +3975,12 @@ public class ZenModeHelperTest extends UiServiceTestCase { customPolicy, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule, - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, "test", SYSTEM_UID); // enable the rule; this will update the consolidated policy mZenModeHelper.setAutomaticZenRuleState(id, new Condition(zenRule.getConditionId(), "", STATE_TRUE), - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, SYSTEM_UID); // since this is the only active rule, the consolidated policy should match the custom // policy for every field specified, and take default values (from device default or @@ -4006,12 +4020,12 @@ public class ZenModeHelperTest extends UiServiceTestCase { customPolicy, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule, - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, "test", SYSTEM_UID); // enable the rule; this will update the consolidated policy mZenModeHelper.setAutomaticZenRuleState(id, new Condition(zenRule.getConditionId(), "", STATE_TRUE), - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, SYSTEM_UID); // since this is the only active rule, the consolidated policy should match the custom // policy for every field specified, and take default values (from either device default @@ -4046,12 +4060,12 @@ public class ZenModeHelperTest extends UiServiceTestCase { null, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule, - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, "test", SYSTEM_UID); // enable rule 1 mZenModeHelper.setAutomaticZenRuleState(id, new Condition(zenRule.getConditionId(), "", STATE_TRUE), - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, SYSTEM_UID); // custom policy for rule 2 ZenPolicy customPolicy = new ZenPolicy.Builder() @@ -4070,12 +4084,12 @@ public class ZenModeHelperTest extends UiServiceTestCase { customPolicy, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); String id2 = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule2, - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, "test", SYSTEM_UID); // enable rule 2; this will update the consolidated policy mZenModeHelper.setAutomaticZenRuleState(id2, new Condition(zenRule2.getConditionId(), "", STATE_TRUE), - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, SYSTEM_UID); // now both rules should be on, and the consolidated policy should reflect the most // restrictive option of each of the two @@ -4107,12 +4121,12 @@ public class ZenModeHelperTest extends UiServiceTestCase { null, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule, - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, "test", SYSTEM_UID); // enable rule 1 mZenModeHelper.setAutomaticZenRuleState(id, new Condition(zenRule.getConditionId(), "", STATE_TRUE), - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, SYSTEM_UID); // custom policy for rule 2 ZenPolicy customPolicy = new ZenPolicy.Builder() @@ -4131,12 +4145,12 @@ public class ZenModeHelperTest extends UiServiceTestCase { customPolicy, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); String id2 = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule2, - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, "test", SYSTEM_UID); // enable rule 2; this will update the consolidated policy mZenModeHelper.setAutomaticZenRuleState(id2, new Condition(zenRule2.getConditionId(), "", STATE_TRUE), - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, SYSTEM_UID); // now both rules should be on, and the consolidated policy should reflect the most // restrictive option of each of the two @@ -4173,12 +4187,12 @@ public class ZenModeHelperTest extends UiServiceTestCase { customPolicy, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule, - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, "test", SYSTEM_UID); // enable the rule; this will update the consolidated policy mZenModeHelper.setAutomaticZenRuleState(id, new Condition(zenRule.getConditionId(), "", STATE_TRUE), - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, SYSTEM_UID); // confirm that channels make it through assertTrue(mZenModeHelper.mConsolidatedPolicy.allowPriorityChannels()); @@ -4195,12 +4209,12 @@ public class ZenModeHelperTest extends UiServiceTestCase { strictPolicy, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); String id2 = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule2, - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, "test", SYSTEM_UID); // enable rule 2; this will update the consolidated policy mZenModeHelper.setAutomaticZenRuleState(id2, new Condition(zenRule2.getConditionId(), "", STATE_TRUE), - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, SYSTEM_UID); // rule 2 should override rule 1 assertFalse(mZenModeHelper.mConsolidatedPolicy.allowPriorityChannels()); @@ -4226,10 +4240,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { .build(), NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); String rule1Id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - zenRuleWithPriority, UPDATE_ORIGIN_APP, "test", CUSTOM_PKG_UID); + zenRuleWithPriority, ORIGIN_APP, "test", CUSTOM_PKG_UID); mZenModeHelper.setAutomaticZenRuleState(rule1Id, new Condition(zenRuleWithPriority.getConditionId(), "", STATE_TRUE), - UPDATE_ORIGIN_APP, CUSTOM_PKG_UID); + ORIGIN_APP, CUSTOM_PKG_UID); // Rule 2: ALL, but somehow with a super strict ZenPolicy. AutomaticZenRule zenRuleWithAll = new AutomaticZenRule("All", @@ -4239,10 +4253,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { new ZenPolicy.Builder().disallowAllSounds().build(), NotificationManager.INTERRUPTION_FILTER_ALL, true); String rule2Id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - zenRuleWithAll, UPDATE_ORIGIN_APP, "test", CUSTOM_PKG_UID); + zenRuleWithAll, ORIGIN_APP, "test", CUSTOM_PKG_UID); mZenModeHelper.setAutomaticZenRuleState(rule2Id, new Condition(zenRuleWithPriority.getConditionId(), "", STATE_TRUE), - UPDATE_ORIGIN_APP, CUSTOM_PKG_UID); + ORIGIN_APP, CUSTOM_PKG_UID); // Consolidated Policy should be default + rule1. assertThat(mZenModeHelper.mConsolidatedPolicy.allowAlarms()).isEqualTo( @@ -4321,7 +4335,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { .build(); String ruleId = mZenModeHelper.addAutomaticZenRule(OWNER.getPackageName(), azr, - UPDATE_ORIGIN_APP, "add", CUSTOM_PKG_UID); + ORIGIN_APP, "add", CUSTOM_PKG_UID); ZenModeConfig.ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); @@ -4351,15 +4365,15 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) .build(); String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - azrBase, UPDATE_ORIGIN_APP, "reason", Process.SYSTEM_UID); + azrBase, ORIGIN_APP, "reason", SYSTEM_UID); AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId); // Checks the name can be changed by the app because the user has not modified it. AutomaticZenRule azrUpdate = new AutomaticZenRule.Builder(rule) .setName("NewName") .build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, UPDATE_ORIGIN_APP, "reason", - Process.SYSTEM_UID); + mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, ORIGIN_APP, "reason", + SYSTEM_UID); rule = mZenModeHelper.getAutomaticZenRule(ruleId); assertThat(rule.getName()).isEqualTo("NewName"); @@ -4368,16 +4382,16 @@ public class ZenModeHelperTest extends UiServiceTestCase { azrUpdate = new AutomaticZenRule.Builder(rule) .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS) .build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, UPDATE_ORIGIN_USER, "reason", - Process.SYSTEM_UID); + mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, ORIGIN_USER_IN_SYSTEMUI, "reason", + SYSTEM_UID); // ...but the app can still modify the name, because the name itself hasn't been modified // by the user. azrUpdate = new AutomaticZenRule.Builder(rule) .setName("NewAppName") .build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, UPDATE_ORIGIN_APP, "reason", - Process.SYSTEM_UID); + mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, ORIGIN_APP, "reason", + SYSTEM_UID); rule = mZenModeHelper.getAutomaticZenRule(ruleId); assertThat(rule.getName()).isEqualTo("NewAppName"); @@ -4385,8 +4399,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { azrUpdate = new AutomaticZenRule.Builder(rule) .setName("UserProvidedName") .build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, UPDATE_ORIGIN_USER, "reason", - Process.SYSTEM_UID); + mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, ORIGIN_USER_IN_SYSTEMUI, "reason", + SYSTEM_UID); rule = mZenModeHelper.getAutomaticZenRule(ruleId); assertThat(rule.getName()).isEqualTo("UserProvidedName"); @@ -4394,8 +4408,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { azrUpdate = new AutomaticZenRule.Builder(rule) .setName("NewAppName") .build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, UPDATE_ORIGIN_APP, "reason", - Process.SYSTEM_UID); + mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, ORIGIN_APP, "reason", + SYSTEM_UID); rule = mZenModeHelper.getAutomaticZenRule(ruleId); assertThat(rule.getName()).isEqualTo("UserProvidedName"); } @@ -4411,7 +4425,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { .build(); // Adds the rule using the app, to avoid having any user modified bits set. String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - azrBase, UPDATE_ORIGIN_APP, "reason", Process.SYSTEM_UID); + azrBase, ORIGIN_APP, "reason", SYSTEM_UID); AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId); // Modifies the filter, icon, zen policy, and device effects @@ -4430,8 +4444,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { .build(); // Update the rule with the AZR from origin user. - mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, UPDATE_ORIGIN_USER, "reason", - Process.SYSTEM_UID); + mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, ORIGIN_USER_IN_SYSTEMUI, "reason", + SYSTEM_UID); rule = mZenModeHelper.getAutomaticZenRule(ruleId); // UPDATE_ORIGIN_USER should change the bitmask and change the values. @@ -4468,7 +4482,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { .build(); // Adds the rule using the app, to avoid having any user modified bits set. String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - azrBase, UPDATE_ORIGIN_APP, "reason", Process.SYSTEM_UID); + azrBase, ORIGIN_APP, "reason", SYSTEM_UID); AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId); // Modifies the icon, zen policy and device effects @@ -4487,8 +4501,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { .build(); // Update the rule with the AZR from origin systemUI. - mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, - "reason", Process.SYSTEM_UID); + mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, ZenModeConfig.ORIGIN_SYSTEM, + "reason", SYSTEM_UID); rule = mZenModeHelper.getAutomaticZenRule(ruleId); // UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI should change the value but NOT update the bitmask. @@ -4518,7 +4532,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { .build(); // Adds the rule using the app, to avoid having any user modified bits set. String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - azrBase, UPDATE_ORIGIN_APP, "reason", Process.SYSTEM_UID); + azrBase, ORIGIN_APP, "reason", SYSTEM_UID); AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId); ZenPolicy policy = new ZenPolicy.Builder() @@ -4535,8 +4549,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Since the rule is not already user modified, UPDATE_ORIGIN_APP can modify the rule. // The bitmask is not modified. - mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, UPDATE_ORIGIN_APP, "reason", - Process.SYSTEM_UID); + mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, ORIGIN_APP, "reason", + SYSTEM_UID); ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); assertThat(storedRule.userModifiedFields).isEqualTo(0); @@ -4551,7 +4565,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Creates another rule, this time from user. This will have user modified bits set. String ruleIdUser = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - azrBase, UPDATE_ORIGIN_USER, "reason", Process.SYSTEM_UID); + azrBase, ORIGIN_USER_IN_SYSTEMUI, "reason", SYSTEM_UID); storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleIdUser); int ruleModifiedFields = storedRule.userModifiedFields; int rulePolicyModifiedFields = storedRule.zenPolicyUserModifiedFields; @@ -4559,8 +4573,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Zen rule update coming from the app again. This cannot fully update the rule, because // the rule is already considered user modified. - mZenModeHelper.updateAutomaticZenRule(ruleIdUser, azrUpdate, UPDATE_ORIGIN_APP, - "reason", Process.SYSTEM_UID); + mZenModeHelper.updateAutomaticZenRule(ruleIdUser, azrUpdate, ORIGIN_APP, + "reason", SYSTEM_UID); AutomaticZenRule ruleUser = mZenModeHelper.getAutomaticZenRule(ruleIdUser); // The app can only change the value if the rule is not already user modified, @@ -4591,7 +4605,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { .build()) .build(); String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - azrBase, UPDATE_ORIGIN_APP, "reason", Process.SYSTEM_UID); + azrBase, ORIGIN_APP, "reason", SYSTEM_UID); AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId); // The values are modified but the bitmask is not. @@ -4613,7 +4627,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { .build(); // Adds the rule using the app, to avoid having any user modified bits set. String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - azrBase, UPDATE_ORIGIN_APP, "reason", Process.SYSTEM_UID); + azrBase, ORIGIN_APP, "reason", SYSTEM_UID); AutomaticZenRule azr = new AutomaticZenRule.Builder(azrBase) // Sets Device Effects to null @@ -4622,8 +4636,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Zen rule update coming from app, but since the rule isn't already // user modified, it can be updated. - mZenModeHelper.updateAutomaticZenRule(ruleId, azr, UPDATE_ORIGIN_APP, "reason", - Process.SYSTEM_UID); + mZenModeHelper.updateAutomaticZenRule(ruleId, azr, ORIGIN_APP, "reason", + SYSTEM_UID); AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId); // When AZR's ZenDeviceEffects is null, the updated rule's device effects are kept. @@ -4639,7 +4653,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { .build(); // Adds the rule using the app, to avoid having any user modified bits set. String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - azrBase, UPDATE_ORIGIN_APP, "reason", Process.SYSTEM_UID); + azrBase, ORIGIN_APP, "reason", SYSTEM_UID); AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId); AutomaticZenRule azr = new AutomaticZenRule.Builder(azrBase) @@ -4649,8 +4663,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Zen rule update coming from app, but since the rule isn't already // user modified, it can be updated. - mZenModeHelper.updateAutomaticZenRule(ruleId, azr, UPDATE_ORIGIN_APP, "reason", - Process.SYSTEM_UID); + mZenModeHelper.updateAutomaticZenRule(ruleId, azr, ORIGIN_APP, "reason", + SYSTEM_UID); rule = mZenModeHelper.getAutomaticZenRule(ruleId); // When AZR's ZenPolicy is null, we expect the updated rule's policy to be unchanged @@ -4671,7 +4685,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { .build(); // Adds the rule using the app, to avoid having any user modified bits set. String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - azrBase, UPDATE_ORIGIN_APP, "reason", Process.SYSTEM_UID); + azrBase, ORIGIN_APP, "reason", SYSTEM_UID); AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId); // Create a fully populated ZenPolicy. @@ -4700,8 +4714,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Applies the update to the rule. // Default config defined in getDefaultConfigParser() is used as the original rule. - mZenModeHelper.updateAutomaticZenRule(ruleId, azr, UPDATE_ORIGIN_USER, "reason", - Process.SYSTEM_UID); + mZenModeHelper.updateAutomaticZenRule(ruleId, azr, ORIGIN_USER_IN_SYSTEMUI, "reason", + SYSTEM_UID); rule = mZenModeHelper.getAutomaticZenRule(ruleId); // New ZenPolicy differs from the default config @@ -4732,7 +4746,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { .build(); // Adds the rule using the app, to avoid having any user modified bits set. String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - azrBase, UPDATE_ORIGIN_APP, "reason", Process.SYSTEM_UID); + azrBase, ORIGIN_APP, "reason", SYSTEM_UID); AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId); ZenDeviceEffects deviceEffects = new ZenDeviceEffects.Builder() @@ -4743,8 +4757,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { .build(); // Applies the update to the rule. - mZenModeHelper.updateAutomaticZenRule(ruleId, azr, UPDATE_ORIGIN_USER, "reason", - Process.SYSTEM_UID); + mZenModeHelper.updateAutomaticZenRule(ruleId, azr, ORIGIN_USER_IN_SYSTEMUI, "reason", + SYSTEM_UID); rule = mZenModeHelper.getAutomaticZenRule(ruleId); // New ZenDeviceEffects is used; all fields considered set, since previously were null. @@ -4769,7 +4783,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { null, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); final String createdId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - zenRule, UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID); + zenRule, ZenModeConfig.ORIGIN_SYSTEM, "test", SYSTEM_UID); CountDownLatch latch = new CountDownLatch(1); final int[] actualStatus = new int[1]; @@ -4785,8 +4799,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.addCallback(callback); zenRule.setEnabled(false); - mZenModeHelper.updateAutomaticZenRule(createdId, zenRule, UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, - "", Process.SYSTEM_UID); + mZenModeHelper.updateAutomaticZenRule(createdId, zenRule, ZenModeConfig.ORIGIN_SYSTEM, + "", SYSTEM_UID); assertTrue(latch.await(500, TimeUnit.MILLISECONDS)); assertEquals(AUTOMATIC_RULE_STATUS_DISABLED, actualStatus[0]); @@ -4804,7 +4818,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { null, NotificationManager.INTERRUPTION_FILTER_PRIORITY, false); final String createdId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - zenRule, UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID); + zenRule, ZenModeConfig.ORIGIN_SYSTEM, "test", SYSTEM_UID); CountDownLatch latch = new CountDownLatch(1); final int[] actualStatus = new int[1]; @@ -4820,8 +4834,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.addCallback(callback); zenRule.setEnabled(true); - mZenModeHelper.updateAutomaticZenRule(createdId, zenRule, UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, - "", Process.SYSTEM_UID); + mZenModeHelper.updateAutomaticZenRule(createdId, zenRule, ZenModeConfig.ORIGIN_SYSTEM, + "", SYSTEM_UID); assertTrue(latch.await(500, TimeUnit.MILLISECONDS)); assertEquals(AUTOMATIC_RULE_STATUS_ENABLED, actualStatus[0]); @@ -4840,7 +4854,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { null, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); final String createdId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - zenRule, UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID); + zenRule, ZenModeConfig.ORIGIN_SYSTEM, "test", SYSTEM_UID); CountDownLatch latch = new CountDownLatch(1); final int[] actualStatus = new int[1]; @@ -4857,7 +4871,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.setAutomaticZenRuleState(createdId, new Condition(zenRule.getConditionId(), "", STATE_TRUE), - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, SYSTEM_UID); assertTrue(latch.await(500, TimeUnit.MILLISECONDS)); if (CompatChanges.isChangeEnabled(ZenModeHelper.SEND_ACTIVATION_AZR_STATUSES)) { @@ -4880,7 +4894,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { null, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); final String createdId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - zenRule, UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID); + zenRule, ZenModeConfig.ORIGIN_SYSTEM, "test", SYSTEM_UID); CountDownLatch latch = new CountDownLatch(1); final int[] actualStatus = new int[2]; @@ -4899,10 +4913,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.setAutomaticZenRuleState(createdId, new Condition(zenRule.getConditionId(), "", STATE_TRUE), - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, SYSTEM_UID); - mZenModeHelper.setManualZenMode(Global.ZEN_MODE_OFF, null, UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, - null, "", Process.SYSTEM_UID); + mZenModeHelper.setManualZenMode(Global.ZEN_MODE_OFF, null, ZenModeConfig.ORIGIN_SYSTEM, + null, "", SYSTEM_UID); assertTrue(latch.await(500, TimeUnit.MILLISECONDS)); if (CompatChanges.isChangeEnabled(ZenModeHelper.SEND_ACTIVATION_AZR_STATUSES)) { @@ -4925,7 +4939,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { null, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); final String createdId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - zenRule, UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID); + zenRule, ZenModeConfig.ORIGIN_SYSTEM, "test", SYSTEM_UID); CountDownLatch latch = new CountDownLatch(1); final int[] actualStatus = new int[2]; @@ -4944,11 +4958,11 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.setAutomaticZenRuleState(createdId, new Condition(zenRule.getConditionId(), "", STATE_TRUE), - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, SYSTEM_UID); mZenModeHelper.setAutomaticZenRuleState(createdId, new Condition(zenRule.getConditionId(), "", STATE_FALSE), - UPDATE_ORIGIN_APP, Process.SYSTEM_UID); + ORIGIN_APP, SYSTEM_UID); assertTrue(latch.await(500, TimeUnit.MILLISECONDS)); if (CompatChanges.isChangeEnabled(ZenModeHelper.SEND_ACTIVATION_AZR_STATUSES)) { @@ -4970,21 +4984,21 @@ public class ZenModeHelperTest extends UiServiceTestCase { null, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); final String createdId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - zenRule, UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID); + zenRule, ZenModeConfig.ORIGIN_SYSTEM, "test", SYSTEM_UID); // Event 1: Mimic the rule coming on automatically by setting the Condition to STATE_TRUE mZenModeHelper.setAutomaticZenRuleState(createdId, new Condition(zenRule.getConditionId(), "", STATE_TRUE), - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, SYSTEM_UID); // Event 2: Snooze rule by turning off DND - mZenModeHelper.setManualZenMode(Global.ZEN_MODE_OFF, null, UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, - "", null, Process.SYSTEM_UID); + mZenModeHelper.setManualZenMode(Global.ZEN_MODE_OFF, null, ZenModeConfig.ORIGIN_SYSTEM, + "", null, SYSTEM_UID); // Event 3: "User" turns off the automatic rule (sets it to not enabled) zenRule.setEnabled(false); - mZenModeHelper.updateAutomaticZenRule(createdId, zenRule, UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, - "", Process.SYSTEM_UID); + mZenModeHelper.updateAutomaticZenRule(createdId, zenRule, ZenModeConfig.ORIGIN_SYSTEM, + "", SYSTEM_UID); assertEquals(false, mZenModeHelper.mConfig.automaticRules.get(createdId).snoozing); } @@ -4997,16 +5011,16 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setConfigurationActivity(new ComponentName(mPkg, "cls")) .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) .build(); - String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, rule, UPDATE_ORIGIN_APP, "reason", + String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, rule, ORIGIN_APP, "reason", CUSTOM_PKG_UID); - mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_TRUE, UPDATE_ORIGIN_APP, + mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_TRUE, ORIGIN_APP, CUSTOM_PKG_UID); assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_IMPORTANT_INTERRUPTIONS); AutomaticZenRule updateWithDiff = new AutomaticZenRule.Builder(rule) .setTriggerDescription("Whenever") .build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, updateWithDiff, UPDATE_ORIGIN_APP, "reason", + mZenModeHelper.updateAutomaticZenRule(ruleId, updateWithDiff, ORIGIN_APP, "reason", CUSTOM_PKG_UID); assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF); @@ -5021,14 +5035,14 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setConfigurationActivity(new ComponentName(mPkg, "cls")) .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) .build(); - String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, rule, UPDATE_ORIGIN_APP, "reason", + String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, rule, ORIGIN_APP, "reason", CUSTOM_PKG_UID); - mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_TRUE, UPDATE_ORIGIN_APP, + mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_TRUE, ORIGIN_APP, CUSTOM_PKG_UID); assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_IMPORTANT_INTERRUPTIONS); AutomaticZenRule updateUnchanged = new AutomaticZenRule.Builder(rule).build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, updateUnchanged, UPDATE_ORIGIN_APP, "reason", + mZenModeHelper.updateAutomaticZenRule(ruleId, updateUnchanged, ORIGIN_APP, "reason", CUSTOM_PKG_UID); assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_IMPORTANT_INTERRUPTIONS); @@ -5044,17 +5058,17 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setConfigurationActivity(new ComponentName(mPkg, "cls")) .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) .build(); - String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, rule, UPDATE_ORIGIN_APP, "reason", + String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, rule, ORIGIN_APP, "reason", CUSTOM_PKG_UID); - mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_TRUE, UPDATE_ORIGIN_APP, + mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_TRUE, ORIGIN_APP, CUSTOM_PKG_UID); assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_IMPORTANT_INTERRUPTIONS); AutomaticZenRule updateWithDiff = new AutomaticZenRule.Builder(rule) .setTriggerDescription("Whenever") .build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, updateWithDiff, UPDATE_ORIGIN_USER, "reason", - CUSTOM_PKG_UID); + mZenModeHelper.updateAutomaticZenRule(ruleId, updateWithDiff, ORIGIN_USER_IN_SYSTEMUI, + "reason", SYSTEM_UID); assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_IMPORTANT_INTERRUPTIONS); assertThat(mZenModeHelper.mConfig.automaticRules.get(ruleId).condition).isEqualTo( @@ -5074,15 +5088,15 @@ public class ZenModeHelperTest extends UiServiceTestCase { .build(); String ruleId = mZenModeHelper.addAutomaticZenRule( - mPkg, rule, UPDATE_ORIGIN_APP, "reason", CUSTOM_PKG_UID); + mPkg, rule, ORIGIN_APP, "reason", CUSTOM_PKG_UID); mZenModeHelper.setAutomaticZenRuleState( - ruleId, CONDITION_TRUE, UPDATE_ORIGIN_APP, CUSTOM_PKG_UID); + ruleId, CONDITION_TRUE, ORIGIN_APP, CUSTOM_PKG_UID); assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_IMPORTANT_INTERRUPTIONS); AutomaticZenRule updateWithDiff = new AutomaticZenRule.Builder(rule).setTriggerDescription("Whenever").build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, updateWithDiff, UPDATE_ORIGIN_USER, "reason", - CUSTOM_PKG_UID); + mZenModeHelper.updateAutomaticZenRule(ruleId, updateWithDiff, ORIGIN_USER_IN_SYSTEMUI, + "reason", SYSTEM_UID); assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_IMPORTANT_INTERRUPTIONS); assertThat(mZenModeHelper.mConfig.automaticRules.get(ruleId).condition).isEqualTo( @@ -5097,18 +5111,18 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setConfigurationActivity(new ComponentName(mPkg, "cls")) .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) .build(); - String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, rule, UPDATE_ORIGIN_APP, "reason", + String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, rule, ORIGIN_APP, "reason", CUSTOM_PKG_UID); - mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_TRUE, UPDATE_ORIGIN_APP, + mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_TRUE, ORIGIN_APP, CUSTOM_PKG_UID); assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_IMPORTANT_INTERRUPTIONS); mZenModeHelper.updateAutomaticZenRule(ruleId, - new AutomaticZenRule.Builder(rule).setEnabled(false).build(), UPDATE_ORIGIN_USER, - "disable", CUSTOM_PKG_UID); + new AutomaticZenRule.Builder(rule).setEnabled(false).build(), + ORIGIN_USER_IN_SYSTEMUI, "disable", SYSTEM_UID); mZenModeHelper.updateAutomaticZenRule(ruleId, - new AutomaticZenRule.Builder(rule).setEnabled(true).build(), UPDATE_ORIGIN_USER, - "enable", CUSTOM_PKG_UID); + new AutomaticZenRule.Builder(rule).setEnabled(true).build(), + ORIGIN_USER_IN_SYSTEMUI, "enable", SYSTEM_UID); assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF); assertThat(mZenModeHelper.mConfig.automaticRules.get(ruleId).condition).isNull(); @@ -5123,13 +5137,13 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setOwner(new ComponentName("android", "some.old.cps")) .build(); String ruleId = mZenModeHelper.addAutomaticZenRule("android", original, - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "reason", Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, "reason", SYSTEM_UID); AutomaticZenRule update = new AutomaticZenRule.Builder("Rule", Uri.EMPTY) .setOwner(new ComponentName("android", "brand.new.cps")) .build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, update, UPDATE_ORIGIN_USER, "reason", - Process.SYSTEM_UID); + mZenModeHelper.updateAutomaticZenRule(ruleId, update, ORIGIN_USER_IN_SYSTEMUI, "reason", + SYSTEM_UID); AutomaticZenRule result = mZenModeHelper.getAutomaticZenRule(ruleId); assertThat(result).isNotNull(); @@ -5143,13 +5157,13 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setOwner(new ComponentName(mContext.getPackageName(), "old.third.party.cps")) .build(); String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), original, - UPDATE_ORIGIN_APP, "reason", CUSTOM_PKG_UID); + ORIGIN_APP, "reason", CUSTOM_PKG_UID); AutomaticZenRule update = new AutomaticZenRule.Builder("Rule", Uri.EMPTY) .setOwner(new ComponentName(mContext.getPackageName(), "new.third.party.cps")) .build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, update, UPDATE_ORIGIN_USER, "reason", - Process.SYSTEM_UID); + mZenModeHelper.updateAutomaticZenRule(ruleId, update, ORIGIN_USER_IN_SYSTEMUI, "reason", + SYSTEM_UID); AutomaticZenRule result = mZenModeHelper.getAutomaticZenRule(ruleId); assertThat(result).isNotNull(); @@ -5166,24 +5180,24 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setShouldSuppressAmbientDisplay(true) .setShouldDimWallpaper(true) .build()); - mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_TRUE, UPDATE_ORIGIN_APP, + mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_TRUE, ORIGIN_APP, CUSTOM_PKG_UID); mTestableLooper.processAllMessages(); - verify(mDeviceEffectsApplier).apply(any(), eq(UPDATE_ORIGIN_APP)); + verify(mDeviceEffectsApplier).apply(any(), eq(ORIGIN_APP)); // Now delete the (currently active!) rule. For example, assume this is done from settings. - mZenModeHelper.removeAutomaticZenRule(ruleId, UPDATE_ORIGIN_USER, "remove", - Process.SYSTEM_UID); + mZenModeHelper.removeAutomaticZenRule(ruleId, ORIGIN_USER_IN_SYSTEMUI, "remove", + SYSTEM_UID); mTestableLooper.processAllMessages(); - verify(mDeviceEffectsApplier).apply(eq(NO_EFFECTS), eq(UPDATE_ORIGIN_USER)); + verify(mDeviceEffectsApplier).apply(eq(NO_EFFECTS), eq(ORIGIN_USER_IN_SYSTEMUI)); } @Test @EnableFlags(FLAG_MODES_API) public void testDeviceEffects_applied() { mZenModeHelper.setDeviceEffectsApplier(mDeviceEffectsApplier); - verify(mDeviceEffectsApplier).apply(eq(NO_EFFECTS), eq(UPDATE_ORIGIN_INIT)); + verify(mDeviceEffectsApplier).apply(eq(NO_EFFECTS), eq(ORIGIN_INIT)); ZenDeviceEffects effects = new ZenDeviceEffects.Builder() .setShouldSuppressAmbientDisplay(true) @@ -5192,11 +5206,11 @@ public class ZenModeHelperTest extends UiServiceTestCase { String ruleId = addRuleWithEffects(effects); verifyNoMoreInteractions(mDeviceEffectsApplier); - mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_TRUE, UPDATE_ORIGIN_APP, + mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_TRUE, ORIGIN_APP, CUSTOM_PKG_UID); mTestableLooper.processAllMessages(); - verify(mDeviceEffectsApplier).apply(eq(effects), eq(UPDATE_ORIGIN_APP)); + verify(mDeviceEffectsApplier).apply(eq(effects), eq(ORIGIN_APP)); } @Test @@ -5206,30 +5220,30 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenDeviceEffects zde = new ZenDeviceEffects.Builder().setShouldUseNightMode(true).build(); String ruleId = addRuleWithEffects(zde); - mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_TRUE, UPDATE_ORIGIN_APP, + mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_TRUE, ORIGIN_APP, CUSTOM_PKG_UID); mTestableLooper.processAllMessages(); - verify(mDeviceEffectsApplier).apply(eq(zde), eq(UPDATE_ORIGIN_APP)); + verify(mDeviceEffectsApplier).apply(eq(zde), eq(ORIGIN_APP)); - mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_FALSE, UPDATE_ORIGIN_APP, + mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_FALSE, ORIGIN_APP, CUSTOM_PKG_UID); mTestableLooper.processAllMessages(); - verify(mDeviceEffectsApplier).apply(eq(NO_EFFECTS), eq(UPDATE_ORIGIN_APP)); + verify(mDeviceEffectsApplier).apply(eq(NO_EFFECTS), eq(ORIGIN_APP)); } @Test @EnableFlags(FLAG_MODES_API) public void testDeviceEffects_changeToConsolidatedEffects_applied() { mZenModeHelper.setDeviceEffectsApplier(mDeviceEffectsApplier); - verify(mDeviceEffectsApplier).apply(eq(NO_EFFECTS), eq(UPDATE_ORIGIN_INIT)); + verify(mDeviceEffectsApplier).apply(eq(NO_EFFECTS), eq(ORIGIN_INIT)); String ruleId = addRuleWithEffects( new ZenDeviceEffects.Builder() .setShouldDisplayGrayscale(true) .addExtraEffect("ONE") .build()); - mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_TRUE, UPDATE_ORIGIN_APP, + mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_TRUE, ORIGIN_APP, CUSTOM_PKG_UID); mTestableLooper.processAllMessages(); verify(mDeviceEffectsApplier).apply( @@ -5237,7 +5251,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setShouldDisplayGrayscale(true) .addExtraEffect("ONE") .build()), - eq(UPDATE_ORIGIN_APP)); + eq(ORIGIN_APP)); // Now create and activate a second rule that adds more effects. String secondRuleId = addRuleWithEffects( @@ -5245,7 +5259,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setShouldDimWallpaper(true) .addExtraEffect("TWO") .build()); - mZenModeHelper.setAutomaticZenRuleState(secondRuleId, CONDITION_TRUE, UPDATE_ORIGIN_APP, + mZenModeHelper.setAutomaticZenRuleState(secondRuleId, CONDITION_TRUE, ORIGIN_APP, CUSTOM_PKG_UID); mTestableLooper.processAllMessages(); @@ -5255,28 +5269,28 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setShouldDimWallpaper(true) .setExtraEffects(ImmutableSet.of("ONE", "TWO")) .build()), - eq(UPDATE_ORIGIN_APP)); + eq(ORIGIN_APP)); } @Test @EnableFlags(FLAG_MODES_API) public void testDeviceEffects_noChangeToConsolidatedEffects_notApplied() { mZenModeHelper.setDeviceEffectsApplier(mDeviceEffectsApplier); - verify(mDeviceEffectsApplier).apply(eq(NO_EFFECTS), eq(UPDATE_ORIGIN_INIT)); + verify(mDeviceEffectsApplier).apply(eq(NO_EFFECTS), eq(ORIGIN_INIT)); ZenDeviceEffects zde = new ZenDeviceEffects.Builder() .setShouldUseNightMode(true) .addExtraEffect("extra_effect") .build(); String ruleId = addRuleWithEffects(zde); - mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_TRUE, UPDATE_ORIGIN_APP, + mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_TRUE, ORIGIN_APP, CUSTOM_PKG_UID); mTestableLooper.processAllMessages(); - verify(mDeviceEffectsApplier).apply(eq(zde), eq(UPDATE_ORIGIN_APP)); + verify(mDeviceEffectsApplier).apply(eq(zde), eq(ORIGIN_APP)); // Now create and activate a second rule that doesn't add any more effects. String secondRuleId = addRuleWithEffects(zde); - mZenModeHelper.setAutomaticZenRuleState(secondRuleId, CONDITION_TRUE, UPDATE_ORIGIN_APP, + mZenModeHelper.setAutomaticZenRuleState(secondRuleId, CONDITION_TRUE, ORIGIN_APP, CUSTOM_PKG_UID); mTestableLooper.processAllMessages(); @@ -5290,20 +5304,20 @@ public class ZenModeHelperTest extends UiServiceTestCase { String ruleId = addRuleWithEffects(zde); verify(mDeviceEffectsApplier, never()).apply(any(), anyInt()); - mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_TRUE, UPDATE_ORIGIN_APP, + mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_TRUE, ORIGIN_APP, CUSTOM_PKG_UID); mTestableLooper.processAllMessages(); verify(mDeviceEffectsApplier, never()).apply(any(), anyInt()); mZenModeHelper.setDeviceEffectsApplier(mDeviceEffectsApplier); - verify(mDeviceEffectsApplier).apply(eq(zde), eq(UPDATE_ORIGIN_INIT)); + verify(mDeviceEffectsApplier).apply(eq(zde), eq(ORIGIN_INIT)); } @Test @EnableFlags(FLAG_MODES_API) public void testDeviceEffects_onUserSwitch_appliedImmediately() { mZenModeHelper.setDeviceEffectsApplier(mDeviceEffectsApplier); - verify(mDeviceEffectsApplier).apply(eq(NO_EFFECTS), eq(UPDATE_ORIGIN_INIT)); + verify(mDeviceEffectsApplier).apply(eq(NO_EFFECTS), eq(ORIGIN_INIT)); // Initialize default configurations (default rules) for both users. mZenModeHelper.onUserSwitched(1); @@ -5324,7 +5338,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { mTestableLooper.processAllMessages(); verify(mDeviceEffectsApplier).apply(eq(user1Rule.zenDeviceEffects), - eq(UPDATE_ORIGIN_INIT_USER)); + eq(ORIGIN_INIT_USER)); } private String addRuleWithEffects(ZenDeviceEffects effects) { @@ -5332,7 +5346,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setDeviceEffects(effects) .build(); return mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule, - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "reasons", Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, "reasons", SYSTEM_UID); } @Test @@ -5346,7 +5360,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setZenPolicy(new ZenPolicy.Builder().allowRepeatCallers(false).build()) .build(); String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule, - UPDATE_ORIGIN_APP, "add it", CUSTOM_PKG_UID); + ORIGIN_APP, "add it", CUSTOM_PKG_UID); assertThat(mZenModeHelper.getAutomaticZenRule(ruleId).getCreationTime()).isEqualTo(1000); // User customizes it. @@ -5354,12 +5368,12 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS) .setZenPolicy(new ZenPolicy.Builder().allowRepeatCallers(true).build()) .build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdate, UPDATE_ORIGIN_USER, "userUpdate", - Process.SYSTEM_UID); + mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdate, ORIGIN_USER_IN_SYSTEMUI, + "userUpdate", SYSTEM_UID); // App deletes it. mTestClock.advanceByMillis(1000); - mZenModeHelper.removeAutomaticZenRule(ruleId, UPDATE_ORIGIN_APP, "delete it", + mZenModeHelper.removeAutomaticZenRule(ruleId, ORIGIN_APP, "delete it", CUSTOM_PKG_UID); assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(0); assertThat(mZenModeHelper.mConfig.deletedRules).hasSize(1); @@ -5367,7 +5381,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // App adds it again. mTestClock.advanceByMillis(1000); String newRuleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule, - UPDATE_ORIGIN_APP, "add it again", CUSTOM_PKG_UID); + ORIGIN_APP, "add it again", CUSTOM_PKG_UID); // Verify that the rule was restored: // - id and creation time is the same as the original one. @@ -5401,12 +5415,12 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setZenPolicy(new ZenPolicy.Builder().allowRepeatCallers(false).build()) .build(); String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule, - UPDATE_ORIGIN_APP, "add it", CUSTOM_PKG_UID); + ORIGIN_APP, "add it", CUSTOM_PKG_UID); assertThat(mZenModeHelper.getAutomaticZenRule(ruleId).getCreationTime()).isEqualTo(1000); // App deletes it. mTestClock.advanceByMillis(1000); - mZenModeHelper.removeAutomaticZenRule(ruleId, UPDATE_ORIGIN_APP, "delete it", + mZenModeHelper.removeAutomaticZenRule(ruleId, ORIGIN_APP, "delete it", CUSTOM_PKG_UID); assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(0); assertThat(mZenModeHelper.mConfig.deletedRules).hasSize(0); @@ -5414,7 +5428,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // App adds it again. mTestClock.advanceByMillis(1000); String newRuleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule, - UPDATE_ORIGIN_APP, "add it again", CUSTOM_PKG_UID); + ORIGIN_APP, "add it again", CUSTOM_PKG_UID); // Verify that the rule was recreated. This means id and creation time are new. AutomaticZenRule finalRule = mZenModeHelper.getAutomaticZenRule(newRuleId); @@ -5433,7 +5447,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setZenPolicy(new ZenPolicy.Builder().allowRepeatCallers(false).build()) .build(); String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule, - UPDATE_ORIGIN_APP, "add it", CUSTOM_PKG_UID); + ORIGIN_APP, "add it", CUSTOM_PKG_UID); assertThat(mZenModeHelper.getAutomaticZenRule(ruleId).getCreationTime()).isEqualTo(1000); // User customizes it. @@ -5442,12 +5456,12 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS) .setZenPolicy(new ZenPolicy.Builder().allowRepeatCallers(true).build()) .build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdate, UPDATE_ORIGIN_USER, "userUpdate", - Process.SYSTEM_UID); + mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdate, ORIGIN_USER_IN_SYSTEMUI, + "userUpdate", SYSTEM_UID); // App deletes it. mTestClock.advanceByMillis(1000); - mZenModeHelper.removeAutomaticZenRule(ruleId, UPDATE_ORIGIN_APP, "delete it", + mZenModeHelper.removeAutomaticZenRule(ruleId, ORIGIN_APP, "delete it", CUSTOM_PKG_UID); assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(0); assertThat(mZenModeHelper.mConfig.deletedRules).hasSize(1); @@ -5455,7 +5469,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // User creates it again (unusual case, but ok). mTestClock.advanceByMillis(1000); String newRuleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule, - UPDATE_ORIGIN_USER, "add it anew", CUSTOM_PKG_UID); + ORIGIN_USER_IN_SYSTEMUI, "add it anew", SYSTEM_UID); // Verify that the rule was recreated. This means id and creation time are new, and the rule // matches the latest data supplied to addAZR. @@ -5481,7 +5495,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setZenPolicy(new ZenPolicy.Builder().allowRepeatCallers(false).build()) .build(); String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule, - UPDATE_ORIGIN_APP, "add it", CUSTOM_PKG_UID); + ORIGIN_APP, "add it", CUSTOM_PKG_UID); assertThat(mZenModeHelper.getAutomaticZenRule(ruleId).getCreationTime()).isEqualTo(1000); // User customizes it. @@ -5490,20 +5504,20 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS) .setZenPolicy(new ZenPolicy.Builder().allowRepeatCallers(true).build()) .build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdate, UPDATE_ORIGIN_USER, "userUpdate", - Process.SYSTEM_UID); + mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdate, ORIGIN_USER_IN_SYSTEMUI, + "userUpdate", SYSTEM_UID); // User deletes it. mTestClock.advanceByMillis(1000); - mZenModeHelper.removeAutomaticZenRule(ruleId, UPDATE_ORIGIN_USER, "delete it", - CUSTOM_PKG_UID); + mZenModeHelper.removeAutomaticZenRule(ruleId, ORIGIN_USER_IN_SYSTEMUI, "delete it", + SYSTEM_UID); assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(0); assertThat(mZenModeHelper.mConfig.deletedRules).hasSize(0); // App creates it again. mTestClock.advanceByMillis(1000); String newRuleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule, - UPDATE_ORIGIN_APP, "add it again", CUSTOM_PKG_UID); + ORIGIN_APP, "add it again", CUSTOM_PKG_UID); // Verify that the rule was recreated. This means id and creation time are new. AutomaticZenRule finalRule = mZenModeHelper.getAutomaticZenRule(newRuleId); @@ -5520,29 +5534,34 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Start with a bunch of customized rules where conditionUris are not unique. String id1 = mZenModeHelper.addAutomaticZenRule("pkg1", - new AutomaticZenRule.Builder("Test1", Uri.parse("uri1")).build(), UPDATE_ORIGIN_APP, + new AutomaticZenRule.Builder("Test1", Uri.parse("uri1")).build(), + ORIGIN_APP, "add it", CUSTOM_PKG_UID); String id2 = mZenModeHelper.addAutomaticZenRule("pkg1", - new AutomaticZenRule.Builder("Test2", Uri.parse("uri2")).build(), UPDATE_ORIGIN_APP, + new AutomaticZenRule.Builder("Test2", Uri.parse("uri2")).build(), + ORIGIN_APP, "add it", CUSTOM_PKG_UID); String id3 = mZenModeHelper.addAutomaticZenRule("pkg1", - new AutomaticZenRule.Builder("Test3", Uri.parse("uri2")).build(), UPDATE_ORIGIN_APP, + new AutomaticZenRule.Builder("Test3", Uri.parse("uri2")).build(), + ORIGIN_APP, "add it", CUSTOM_PKG_UID); String id4 = mZenModeHelper.addAutomaticZenRule("pkg2", - new AutomaticZenRule.Builder("Test4", Uri.parse("uri1")).build(), UPDATE_ORIGIN_APP, + new AutomaticZenRule.Builder("Test4", Uri.parse("uri1")).build(), + ORIGIN_APP, "add it", CUSTOM_PKG_UID); String id5 = mZenModeHelper.addAutomaticZenRule("pkg2", - new AutomaticZenRule.Builder("Test5", Uri.parse("uri1")).build(), UPDATE_ORIGIN_APP, + new AutomaticZenRule.Builder("Test5", Uri.parse("uri1")).build(), + ORIGIN_APP, "add it", CUSTOM_PKG_UID); for (ZenRule zenRule : mZenModeHelper.mConfig.automaticRules.values()) { zenRule.userModifiedFields = AutomaticZenRule.FIELD_INTERRUPTION_FILTER; } - mZenModeHelper.removeAutomaticZenRule(id1, UPDATE_ORIGIN_APP, "begone", CUSTOM_PKG_UID); - mZenModeHelper.removeAutomaticZenRule(id2, UPDATE_ORIGIN_APP, "begone", CUSTOM_PKG_UID); - mZenModeHelper.removeAutomaticZenRule(id3, UPDATE_ORIGIN_APP, "begone", CUSTOM_PKG_UID); - mZenModeHelper.removeAutomaticZenRule(id4, UPDATE_ORIGIN_APP, "begone", CUSTOM_PKG_UID); - mZenModeHelper.removeAutomaticZenRule(id5, UPDATE_ORIGIN_APP, "begone", CUSTOM_PKG_UID); + mZenModeHelper.removeAutomaticZenRule(id1, ORIGIN_APP, "begone", CUSTOM_PKG_UID); + mZenModeHelper.removeAutomaticZenRule(id2, ORIGIN_APP, "begone", CUSTOM_PKG_UID); + mZenModeHelper.removeAutomaticZenRule(id3, ORIGIN_APP, "begone", CUSTOM_PKG_UID); + mZenModeHelper.removeAutomaticZenRule(id4, ORIGIN_APP, "begone", CUSTOM_PKG_UID); + mZenModeHelper.removeAutomaticZenRule(id5, ORIGIN_APP, "begone", CUSTOM_PKG_UID); assertThat(mZenModeHelper.mConfig.deletedRules.keySet()) .containsExactly("pkg1|uri1", "pkg1|uri2", "pkg2|uri1"); @@ -5557,17 +5576,19 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.mConfig.automaticRules.clear(); mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - new AutomaticZenRule.Builder("Test1", Uri.parse("uri1")).build(), UPDATE_ORIGIN_APP, + new AutomaticZenRule.Builder("Test1", Uri.parse("uri1")).build(), + ORIGIN_APP, "add it", CUSTOM_PKG_UID); mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - new AutomaticZenRule.Builder("Test2", Uri.parse("uri2")).build(), UPDATE_ORIGIN_APP, + new AutomaticZenRule.Builder("Test2", Uri.parse("uri2")).build(), + ORIGIN_APP, "add it", CUSTOM_PKG_UID); for (ZenRule zenRule : mZenModeHelper.mConfig.automaticRules.values()) { zenRule.userModifiedFields = AutomaticZenRule.FIELD_INTERRUPTION_FILTER; } - mZenModeHelper.removeAutomaticZenRules(mContext.getPackageName(), UPDATE_ORIGIN_APP, + mZenModeHelper.removeAutomaticZenRules(mContext.getPackageName(), ORIGIN_APP, "begone", CUSTOM_PKG_UID); assertThat(mZenModeHelper.mConfig.deletedRules).hasSize(2); @@ -5586,7 +5607,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.mConfig.deletedRules.put(ZenModeConfig.deletedRuleKey(pkg2Rule), pkg2Rule); mZenModeHelper.removeAutomaticZenRules("pkg1", - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "goodbye pkg1", Process.SYSTEM_UID); + ZenModeConfig.ORIGIN_SYSTEM, "goodbye pkg1", SYSTEM_UID); // Preserved rules from pkg1 are gone; those from pkg2 are still there. assertThat(mZenModeHelper.mConfig.deletedRules.values().stream().map(r -> r.pkg) @@ -5603,23 +5624,23 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) .build(); String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule, - UPDATE_ORIGIN_APP, "add it", CUSTOM_PKG_UID); + ORIGIN_APP, "add it", CUSTOM_PKG_UID); // User customizes it. AutomaticZenRule userUpdate = new AutomaticZenRule.Builder(rule) .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS) .build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdate, UPDATE_ORIGIN_USER, "userUpdate", - Process.SYSTEM_UID); + mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdate, ORIGIN_USER_IN_SYSTEMUI, + "userUpdate", SYSTEM_UID); assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF); // App activates it. - mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_TRUE, UPDATE_ORIGIN_APP, + mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_TRUE, ORIGIN_APP, CUSTOM_PKG_UID); assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_ALARMS); // App deletes it. - mZenModeHelper.removeAutomaticZenRule(ruleId, UPDATE_ORIGIN_APP, "delete it", + mZenModeHelper.removeAutomaticZenRule(ruleId, ORIGIN_APP, "delete it", CUSTOM_PKG_UID); assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(0); assertThat(mZenModeHelper.mConfig.deletedRules).hasSize(1); @@ -5627,7 +5648,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // App adds it again. String newRuleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule, - UPDATE_ORIGIN_APP, "add it again", CUSTOM_PKG_UID); + ORIGIN_APP, "add it again", CUSTOM_PKG_UID); // The rule is restored... assertThat(newRuleId).isEqualTo(ruleId); @@ -5652,35 +5673,35 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) .build(); String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule, - UPDATE_ORIGIN_APP, "add it", CUSTOM_PKG_UID); + ORIGIN_APP, "add it", CUSTOM_PKG_UID); // User customizes it. AutomaticZenRule userUpdate = new AutomaticZenRule.Builder(rule) .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS) .build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdate, UPDATE_ORIGIN_USER, "userUpdate", - Process.SYSTEM_UID); + mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdate, ORIGIN_USER_IN_SYSTEMUI, + "userUpdate", SYSTEM_UID); assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF); // App activates it. - mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_TRUE, UPDATE_ORIGIN_APP, + mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_TRUE, ORIGIN_APP, CUSTOM_PKG_UID); assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_ALARMS); // User snoozes it. - mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null, UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, - "snoozing", "systemui", Process.SYSTEM_UID); + mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null, ZenModeConfig.ORIGIN_SYSTEM, + "snoozing", "systemui", SYSTEM_UID); assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF); // App deletes it. - mZenModeHelper.removeAutomaticZenRule(ruleId, UPDATE_ORIGIN_APP, "delete it", + mZenModeHelper.removeAutomaticZenRule(ruleId, ORIGIN_APP, "delete it", CUSTOM_PKG_UID); assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(0); assertThat(mZenModeHelper.mConfig.deletedRules).hasSize(1); // App adds it again. String newRuleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule, - UPDATE_ORIGIN_APP, "add it again", CUSTOM_PKG_UID); + ORIGIN_APP, "add it again", CUSTOM_PKG_UID); // The rule is restored... assertThat(newRuleId).isEqualTo(ruleId); @@ -5762,20 +5783,20 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setConfigurationActivity( new ComponentName(mContext.getPackageName(), "Blah")) .build(), - UPDATE_ORIGIN_APP, "reasons", CUSTOM_PKG_UID); + ORIGIN_APP, "reasons", CUSTOM_PKG_UID); // Null condition -> STATE_FALSE assertThat(mZenModeHelper.getAutomaticZenRuleState(id)).isEqualTo(Condition.STATE_FALSE); - mZenModeHelper.setAutomaticZenRuleState(id, CONDITION_TRUE, UPDATE_ORIGIN_APP, + mZenModeHelper.setAutomaticZenRuleState(id, CONDITION_TRUE, ORIGIN_APP, CUSTOM_PKG_UID); assertThat(mZenModeHelper.getAutomaticZenRuleState(id)).isEqualTo(Condition.STATE_TRUE); - mZenModeHelper.setAutomaticZenRuleState(id, CONDITION_FALSE, UPDATE_ORIGIN_APP, + mZenModeHelper.setAutomaticZenRuleState(id, CONDITION_FALSE, ORIGIN_APP, CUSTOM_PKG_UID); assertThat(mZenModeHelper.getAutomaticZenRuleState(id)).isEqualTo(Condition.STATE_FALSE); - mZenModeHelper.removeAutomaticZenRule(id, UPDATE_ORIGIN_APP, "", CUSTOM_PKG_UID); + mZenModeHelper.removeAutomaticZenRule(id, ORIGIN_APP, "", CUSTOM_PKG_UID); assertThat(mZenModeHelper.getAutomaticZenRuleState(id)).isEqualTo(Condition.STATE_UNKNOWN); } @@ -5788,7 +5809,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { systemRule.condition = new Condition(systemRule.conditionId, "on", Condition.STATE_TRUE); ZenModeConfig config = mZenModeHelper.mConfig.copy(); config.automaticRules.put("systemRule", systemRule); - mZenModeHelper.setConfig(config, null, UPDATE_ORIGIN_INIT, "", Process.SYSTEM_UID); + mZenModeHelper.setConfig(config, null, ORIGIN_INIT, "", SYSTEM_UID); assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_ALARMS); assertThat(mZenModeHelper.getAutomaticZenRuleState("systemRule")).isEqualTo( @@ -5805,13 +5826,13 @@ public class ZenModeHelperTest extends UiServiceTestCase { otherRule.condition = new Condition(otherRule.conditionId, "on", Condition.STATE_TRUE); ZenModeConfig config = mZenModeHelper.mConfig.copy(); config.automaticRules.put("otherRule", otherRule); - mZenModeHelper.setConfig(config, null, UPDATE_ORIGIN_INIT, "", Process.SYSTEM_UID); + mZenModeHelper.setConfig(config, null, ORIGIN_INIT, "", SYSTEM_UID); assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_ALARMS); // Should be ignored. mZenModeHelper.setAutomaticZenRuleState("otherRule", new Condition(otherRule.conditionId, "off", Condition.STATE_FALSE), - UPDATE_ORIGIN_APP, CUSTOM_PKG_UID); + ORIGIN_APP, CUSTOM_PKG_UID); assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_ALARMS); } @@ -5826,13 +5847,13 @@ public class ZenModeHelperTest extends UiServiceTestCase { otherRule.condition = new Condition(otherRule.conditionId, "on", Condition.STATE_TRUE); ZenModeConfig config = mZenModeHelper.mConfig.copy(); config.automaticRules.put("otherRule", otherRule); - mZenModeHelper.setConfig(config, null, UPDATE_ORIGIN_INIT, "", Process.SYSTEM_UID); + mZenModeHelper.setConfig(config, null, ORIGIN_INIT, "", SYSTEM_UID); assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_ALARMS); // Should be ignored. mZenModeHelper.setAutomaticZenRuleState(otherRule.conditionId, new Condition(otherRule.conditionId, "off", Condition.STATE_FALSE), - UPDATE_ORIGIN_APP, CUSTOM_PKG_UID); + ORIGIN_APP, CUSTOM_PKG_UID); assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_ALARMS); } @@ -5851,7 +5872,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { }); Policy totalSilencePolicy = new Policy(0, 0, 0); - mZenModeHelper.setNotificationPolicy(totalSilencePolicy, UPDATE_ORIGIN_APP, CUSTOM_PKG_UID); + mZenModeHelper.setNotificationPolicy(totalSilencePolicy, ORIGIN_APP, CUSTOM_PKG_UID); Policy callbackPolicy = futurePolicy.get(1, TimeUnit.SECONDS); assertThat(callbackPolicy.allowReminders()).isFalse(); @@ -5874,9 +5895,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setOwner(OWNER) .setInterruptionFilter(INTERRUPTION_FILTER_NONE) .build(), - UPDATE_ORIGIN_APP, "reasons", 0); + ORIGIN_APP, "reasons", 0); mZenModeHelper.setAutomaticZenRuleState(totalSilenceRuleId, - new Condition(CONDITION_ID, "", STATE_TRUE), UPDATE_ORIGIN_APP, CUSTOM_PKG_UID); + new Condition(CONDITION_ID, "", STATE_TRUE), ORIGIN_APP, CUSTOM_PKG_UID); Policy callbackPolicy = futureConsolidatedPolicy.get(1, TimeUnit.SECONDS); assertThat(callbackPolicy.allowMedia()).isFalse(); @@ -5905,7 +5926,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, ZEN_MODE_IMPORTANT_INTERRUPTIONS); - mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null, UPDATE_ORIGIN_APP, "test", "test", 0); + mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null, ORIGIN_APP, "test", "test", 0); assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(1); mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, @@ -5937,8 +5958,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { AutomaticZenRule userUpdateRule = new AutomaticZenRule.Builder(rule) .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS) .build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdateRule, UPDATE_ORIGIN_USER, "reason", - Process.SYSTEM_UID); + mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdateRule, ORIGIN_USER_IN_SYSTEMUI, + "reason", SYSTEM_UID); // From app, call "setInterruptionFilter" again. mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(pkg, CUSTOM_PKG_UID, @@ -5969,8 +5990,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { AutomaticZenRule userUpdateRule = new AutomaticZenRule.Builder(rule) .setName("Renamed") .build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdateRule, UPDATE_ORIGIN_USER, "reason", - Process.SYSTEM_UID); + mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdateRule, ORIGIN_USER_IN_SYSTEMUI, + "reason", SYSTEM_UID); // From app, call "setInterruptionFilter" again. mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(pkg, CUSTOM_PKG_UID, @@ -6020,7 +6041,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(1); assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).snoozing).isFalse(); - mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null, UPDATE_ORIGIN_APP, "test", "test", 0); + mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null, ORIGIN_APP, "test", "test", 0); assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).snoozing).isTrue(); mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, @@ -6121,8 +6142,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { AutomaticZenRule userUpdateRule = new AutomaticZenRule.Builder(rule) .setZenPolicy(userUpdateZenPolicy) .build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdateRule, UPDATE_ORIGIN_USER, "reason", - Process.SYSTEM_UID); + mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdateRule, ORIGIN_USER_IN_SYSTEMUI, + "reason", SYSTEM_UID); // From app, call "setNotificationPolicy" again. Policy appUpdatePolicy = new Policy(PRIORITY_CATEGORY_SYSTEM, 0, 0); @@ -6159,8 +6180,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { AutomaticZenRule userUpdateRule = new AutomaticZenRule.Builder(rule) .setName("Rule renamed, not touching policy") .build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdateRule, UPDATE_ORIGIN_USER, "reason", - Process.SYSTEM_UID); + mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdateRule, ORIGIN_USER_IN_SYSTEMUI, + "reason", SYSTEM_UID); // From app, call "setNotificationPolicy" again. Policy appUpdatePolicy = new Policy(PRIORITY_CATEGORY_SYSTEM, 0, 0); @@ -6215,7 +6236,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // If the policy then changes afterwards, it should inherit updates because user cannot // edit the policy in the UI. mZenModeHelper.setNotificationPolicy(new Policy(PRIORITY_CATEGORY_ALARMS, 0, 0), - UPDATE_ORIGIN_APP, 1); + ORIGIN_APP, 1); Policy readPolicy = mZenModeHelper.getNotificationPolicyFromImplicitZenRule( CUSTOM_PKG_NAME); @@ -6228,7 +6249,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { @EnableFlags(FLAG_MODES_API) public void getNotificationPolicyFromImplicitZenRule_noImplicitRule_returnsGlobalPolicy() { Policy policy = new Policy(PRIORITY_CATEGORY_CALLS, PRIORITY_SENDERS_STARRED, 0); - mZenModeHelper.setNotificationPolicy(policy, UPDATE_ORIGIN_USER, 1); + mZenModeHelper.setNotificationPolicy(policy, ORIGIN_APP, CUSTOM_PKG_UID); Policy readPolicy = mZenModeHelper.getNotificationPolicyFromImplicitZenRule( CUSTOM_PKG_NAME); @@ -6264,7 +6285,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZEN_MODE_IMPORTANT_INTERRUPTIONS, previousManualZenPolicy); Policy newManualPolicy = new Policy(PRIORITY_CATEGORY_EVENTS, 0, 0); - mZenModeHelper.setNotificationPolicy(newManualPolicy, UPDATE_ORIGIN_USER, 0); + mZenModeHelper.setNotificationPolicy(newManualPolicy, ORIGIN_APP, CUSTOM_PKG_UID); ZenPolicy newManualZenPolicy = ZenAdapters.notificationPolicyToZenPolicy(newManualPolicy); // Only app rules with default or same-as-manual policies were updated. @@ -6294,7 +6315,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), new AutomaticZenRule.Builder("Rule", CONDITION_ID).setIconResId(resourceId).build(), - UPDATE_ORIGIN_APP, "reason", CUSTOM_PKG_UID); + ORIGIN_APP, "reason", CUSTOM_PKG_UID); AutomaticZenRule storedRule = mZenModeHelper.getAutomaticZenRule(ruleId); assertThat(storedRule.getIconResId()).isEqualTo(0); @@ -6306,7 +6327,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenDeviceEffects effects = new ZenDeviceEffects.Builder() .setShouldDimWallpaper(true) .build(); - mZenModeHelper.setManualZenRuleDeviceEffects(effects, UPDATE_ORIGIN_USER, "settings", 1000); + mZenModeHelper.setManualZenRuleDeviceEffects(effects, ORIGIN_USER_IN_SYSTEMUI, "settings", + SYSTEM_UID); assertThat(mZenModeHelper.getConfig().manualRule).isNotNull(); assertThat(mZenModeHelper.getConfig().isManualActive()).isFalse(); @@ -6316,13 +6338,14 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) public void setManualZenRuleDeviceEffects_preexistingMode() { - mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, Uri.EMPTY, UPDATE_ORIGIN_USER, - "create manual rule", "settings", 1000); + mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, Uri.EMPTY, ORIGIN_USER_IN_SYSTEMUI, + "create manual rule", "settings", SYSTEM_UID); ZenDeviceEffects effects = new ZenDeviceEffects.Builder() .setShouldDimWallpaper(true) .build(); - mZenModeHelper.setManualZenRuleDeviceEffects(effects, UPDATE_ORIGIN_USER, "settings", 1000); + mZenModeHelper.setManualZenRuleDeviceEffects(effects, ORIGIN_USER_IN_SYSTEMUI, "settings", + SYSTEM_UID); assertThat(mZenModeHelper.getConfig().manualRule).isNotNull(); assertThat(mZenModeHelper.getConfig().isManualActive()).isFalse(); @@ -6337,11 +6360,12 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setEnabled(false) .build(); - String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, startsDisabled, UPDATE_ORIGIN_APP, + String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, startsDisabled, + ORIGIN_APP, "new", CUSTOM_PKG_UID); assertThat(mZenModeHelper.mConfig.automaticRules.get(ruleId).disabledOrigin).isEqualTo( - UPDATE_ORIGIN_APP); + ORIGIN_APP); } @Test @@ -6351,19 +6375,20 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setOwner(new ComponentName(mPkg, "SomeProvider")) .setEnabled(true) .build(); - String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, startsEnabled, UPDATE_ORIGIN_APP, + String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, startsEnabled, + ORIGIN_APP, "new", CUSTOM_PKG_UID); assertThat(mZenModeHelper.mConfig.automaticRules.get(ruleId).disabledOrigin).isEqualTo( - UPDATE_ORIGIN_UNKNOWN); + ORIGIN_UNKNOWN); AutomaticZenRule nowDisabled = new AutomaticZenRule.Builder(startsEnabled) .setEnabled(false) .build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, nowDisabled, UPDATE_ORIGIN_USER, "off", - Process.SYSTEM_UID); + mZenModeHelper.updateAutomaticZenRule(ruleId, nowDisabled, ORIGIN_USER_IN_SYSTEMUI, "off", + SYSTEM_UID); assertThat(mZenModeHelper.mConfig.automaticRules.get(ruleId).disabledOrigin).isEqualTo( - UPDATE_ORIGIN_USER); + ORIGIN_USER_IN_SYSTEMUI); } @Test @@ -6373,26 +6398,27 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setOwner(new ComponentName(mPkg, "SomeProvider")) .setEnabled(true) .build(); - String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, startsEnabled, UPDATE_ORIGIN_APP, + String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, startsEnabled, + ORIGIN_APP, "new", CUSTOM_PKG_UID); AutomaticZenRule nowDisabled = new AutomaticZenRule.Builder(startsEnabled) .setEnabled(false) .build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, nowDisabled, UPDATE_ORIGIN_USER, "off", - Process.SYSTEM_UID); + mZenModeHelper.updateAutomaticZenRule(ruleId, nowDisabled, ORIGIN_USER_IN_SYSTEMUI, "off", + SYSTEM_UID); assertThat(mZenModeHelper.mConfig.automaticRules.get(ruleId).disabledOrigin).isEqualTo( - UPDATE_ORIGIN_USER); + ORIGIN_USER_IN_SYSTEMUI); // Now update it again, for an unrelated reason with a different origin. AutomaticZenRule nowRenamed = new AutomaticZenRule.Builder(nowDisabled) .setName("Fancy pants rule") .build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, nowRenamed, UPDATE_ORIGIN_APP, "update", + mZenModeHelper.updateAutomaticZenRule(ruleId, nowRenamed, ORIGIN_APP, "update", CUSTOM_PKG_UID); // Identity of the disabler is preserved. assertThat(mZenModeHelper.mConfig.automaticRules.get(ruleId).disabledOrigin).isEqualTo( - UPDATE_ORIGIN_USER); + ORIGIN_USER_IN_SYSTEMUI); } @Test @@ -6402,26 +6428,27 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setOwner(new ComponentName(mPkg, "SomeProvider")) .setEnabled(true) .build(); - String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, startsEnabled, UPDATE_ORIGIN_APP, + String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, startsEnabled, + ORIGIN_APP, "new", CUSTOM_PKG_UID); AutomaticZenRule nowDisabled = new AutomaticZenRule.Builder(startsEnabled) .setEnabled(false) .build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, nowDisabled, UPDATE_ORIGIN_USER, "off", - Process.SYSTEM_UID); + mZenModeHelper.updateAutomaticZenRule(ruleId, nowDisabled, ORIGIN_USER_IN_SYSTEMUI, "off", + SYSTEM_UID); assertThat(mZenModeHelper.mConfig.automaticRules.get(ruleId).disabledOrigin).isEqualTo( - UPDATE_ORIGIN_USER); + ORIGIN_USER_IN_SYSTEMUI); // Now enable it again AutomaticZenRule nowEnabled = new AutomaticZenRule.Builder(nowDisabled) .setEnabled(true) .build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, nowEnabled, UPDATE_ORIGIN_APP, "on", + mZenModeHelper.updateAutomaticZenRule(ruleId, nowEnabled, ORIGIN_APP, "on", CUSTOM_PKG_UID); // Identity of the disabler was cleared. assertThat(mZenModeHelper.mConfig.automaticRules.get(ruleId).disabledOrigin).isEqualTo( - UPDATE_ORIGIN_UNKNOWN); + ORIGIN_UNKNOWN); } private static void addZenRule(ZenModeConfig config, String id, String ownerPkg, int zenMode, @@ -6494,7 +6521,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { SUPPRESSED_EFFECT_BADGE, 0, CONVERSATION_SENDERS_IMPORTANT); - mZenModeHelper.setNotificationPolicy(customPolicy, UPDATE_ORIGIN_UNKNOWN, 1); + mZenModeHelper.setNotificationPolicy(customPolicy, ORIGIN_UNKNOWN, 1); if (!Flags.modesUi()) { mZenModeHelper.mConfig.manualRule = null; } 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/HapticFeedbackVibrationProviderTest.java b/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackVibrationProviderTest.java index 4f7593184d83..240bd1ec56e3 100644 --- a/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackVibrationProviderTest.java +++ b/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackVibrationProviderTest.java @@ -50,7 +50,6 @@ import android.hardware.vibrator.IVibrator; import android.os.VibrationAttributes; import android.os.VibrationEffect; import android.os.VibratorInfo; -import android.os.vibrator.Flags; import android.platform.test.flag.junit.SetFlagsRule; import android.util.AtomicFile; import android.util.SparseArray; @@ -256,22 +255,7 @@ public class HapticFeedbackVibrationProviderTest { } @Test - public void testKeyboardHaptic_fixAmplitude_keyboardCategoryOff_defaultVibrationReturned() { - mSetFlagsRule.disableFlags(Flags.FLAG_KEYBOARD_CATEGORY_ENABLED); - mockVibratorPrimitiveSupport(PRIMITIVE_CLICK, PRIMITIVE_TICK); - mockKeyboardVibrationFixedAmplitude(KEYBOARD_VIBRATION_FIXED_AMPLITUDE); - - HapticFeedbackVibrationProvider hapticProvider = createProviderWithDefaultCustomizations(); - - assertThat(hapticProvider.getVibrationForHapticFeedback(KEYBOARD_TAP)) - .isEqualTo(VibrationEffect.get(EFFECT_CLICK, true /* fallback */)); - assertThat(hapticProvider.getVibrationForHapticFeedback(KEYBOARD_RELEASE)) - .isEqualTo(VibrationEffect.get(EFFECT_TICK, false /* fallback */)); - } - - @Test public void testKeyboardHaptic_fixAmplitude_keyboardCategoryOn_keyboardVibrationReturned() { - mSetFlagsRule.enableFlags(Flags.FLAG_KEYBOARD_CATEGORY_ENABLED); mockVibratorPrimitiveSupport(PRIMITIVE_CLICK, PRIMITIVE_TICK); mockKeyboardVibrationFixedAmplitude(KEYBOARD_VIBRATION_FIXED_AMPLITUDE); @@ -346,24 +330,7 @@ public class HapticFeedbackVibrationProviderTest { } @Test - public void testVibrationAttribute_keyboardCategoryOff_isIme_useTouchUsage() { - mSetFlagsRule.disableFlags(Flags.FLAG_KEYBOARD_CATEGORY_ENABLED); - HapticFeedbackVibrationProvider hapticProvider = createProviderWithDefaultCustomizations(); - - for (int effectId : KEYBOARD_FEEDBACK_CONSTANTS) { - VibrationAttributes attrs = hapticProvider.getVibrationAttributesForHapticFeedback( - effectId, /* flags */ 0, - HapticFeedbackConstants.PRIVATE_FLAG_APPLY_INPUT_METHOD_SETTINGS); - assertWithMessage("Expected USAGE_TOUCH for effect " + effectId) - .that(attrs.getUsage()).isEqualTo(USAGE_TOUCH); - assertWithMessage("Expected no CATEGORY_KEYBOARD for effect " + effectId) - .that(attrs.getCategory()).isEqualTo(CATEGORY_UNKNOWN); - } - } - - @Test public void testVibrationAttribute_keyboardCategoryOn_notIme_useTouchUsage() { - mSetFlagsRule.enableFlags(Flags.FLAG_KEYBOARD_CATEGORY_ENABLED); HapticFeedbackVibrationProvider hapticProvider = createProviderWithDefaultCustomizations(); for (int effectId : KEYBOARD_FEEDBACK_CONSTANTS) { @@ -378,7 +345,6 @@ public class HapticFeedbackVibrationProviderTest { @Test public void testVibrationAttribute_keyboardCategoryOn_isIme_useImeFeedbackUsage() { - mSetFlagsRule.enableFlags(Flags.FLAG_KEYBOARD_CATEGORY_ENABLED); HapticFeedbackVibrationProvider hapticProvider = createProviderWithDefaultCustomizations(); for (int effectId : KEYBOARD_FEEDBACK_CONSTANTS) { 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 8d4a6aa5ba29..72ef888aa061 100644 --- a/services/tests/vibrator/src/com/android/server/vibrator/VibrationSettingsTest.java +++ b/services/tests/vibrator/src/com/android/server/vibrator/VibrationSettingsTest.java @@ -70,9 +70,7 @@ import android.os.VibrationAttributes; import android.os.VibrationEffect; import android.os.Vibrator; import android.os.test.TestLooper; -import android.os.vibrator.Flags; import android.os.vibrator.VibrationConfig; -import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.provider.Settings; @@ -602,7 +600,6 @@ public class VibrationSettingsTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_KEYBOARD_CATEGORY_ENABLED) public void shouldIgnoreVibration_withKeyboardSettingsOff_shouldIgnoreKeyboardVibration() { setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_MEDIUM); setUserSetting(Settings.System.KEYBOARD_VIBRATION_ENABLED, 0 /* OFF*/); @@ -627,7 +624,6 @@ public class VibrationSettingsTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_KEYBOARD_CATEGORY_ENABLED) public void shouldIgnoreVibration_withKeyboardSettingsOn_shouldNotIgnoreKeyboardVibration() { setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_OFF); setUserSetting(Settings.System.KEYBOARD_VIBRATION_ENABLED, 1 /* ON */); @@ -645,7 +641,6 @@ public class VibrationSettingsTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_KEYBOARD_CATEGORY_ENABLED) public void shouldIgnoreVibration_notSupportKeyboardVibration_ignoresKeyboardTouchVibration() { setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_OFF); setUserSetting(Settings.System.KEYBOARD_VIBRATION_ENABLED, 1 /* ON */); 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/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/DragDropControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java index 4a9d5c7bc828..f339d292ed82 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java @@ -218,16 +218,6 @@ public class DragDropControllerTests extends WindowTestsBase { } @Test - public void testPerformDrag_NullDataToOtherUser() { - final WindowState otherUsersWindow = - createDropTargetWindow("Other user's window", 1 * UserHandle.PER_USER_RANGE); - doReturn(otherUsersWindow).when(mDisplayContent).getTouchableWinAtPointLocked(10, 10); - - doDragAndDrop(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ, null, 10, 10); - mToken = otherUsersWindow.mClient.asBinder(); - } - - @Test public void testPrivateInterceptGlobalDragDropFlagChecksPermission() { DisplayPolicy policy = mDisplayContent.getDisplayPolicy(); WindowManager.LayoutParams attrs = new WindowManager.LayoutParams(); 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/LetterboxUiControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java index 33df5d896f7f..695068a5842a 100644 --- a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java @@ -23,11 +23,9 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -36,8 +34,6 @@ import android.compat.testing.PlatformCompatChangeRule; import android.content.ComponentName; import android.content.res.Resources; import android.graphics.Rect; -import android.platform.test.annotations.DisableFlags; -import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import android.view.InsetsSource; import android.view.InsetsState; @@ -49,7 +45,6 @@ import android.view.WindowManager; import androidx.test.filters.SmallTest; import com.android.internal.R; -import com.android.window.flags.Flags; import org.junit.Before; import org.junit.Rule; @@ -296,106 +291,6 @@ public class LetterboxUiControllerTest extends WindowTestsBase { } @Test - public void testIsVerticalThinLetterboxed() { - // Vertical thin letterbox disabled - doReturn(-1).when(mActivity.mWmService.mAppCompatConfiguration) - .getThinLetterboxHeightPx(); - final AppCompatReachabilityOverrides reachabilityOverrides = mActivity.mAppCompatController - .getAppCompatReachabilityOverrides(); - assertFalse(reachabilityOverrides.isVerticalThinLetterboxed()); - // Define a Task 100x100 - final Task task = mock(Task.class); - doReturn(new Rect(0, 0, 100, 100)).when(task).getBounds(); - doReturn(10).when(mActivity.mWmService.mAppCompatConfiguration) - .getThinLetterboxHeightPx(); - - // Vertical thin letterbox disabled without Task - doReturn(null).when(mActivity).getTask(); - assertFalse(reachabilityOverrides.isVerticalThinLetterboxed()); - // Assign a Task for the Activity - doReturn(task).when(mActivity).getTask(); - - // (task.width() - act.width()) / 2 = 5 < 10 - doReturn(new Rect(5, 5, 95, 95)).when(mActivity).getBounds(); - assertTrue(reachabilityOverrides.isVerticalThinLetterboxed()); - - // (task.width() - act.width()) / 2 = 10 = 10 - doReturn(new Rect(10, 10, 90, 90)).when(mActivity).getBounds(); - assertTrue(reachabilityOverrides.isVerticalThinLetterboxed()); - - // (task.width() - act.width()) / 2 = 11 > 10 - doReturn(new Rect(11, 11, 89, 89)).when(mActivity).getBounds(); - assertFalse(reachabilityOverrides.isVerticalThinLetterboxed()); - } - - @Test - public void testIsHorizontalThinLetterboxed() { - // Horizontal thin letterbox disabled - doReturn(-1).when(mActivity.mWmService.mAppCompatConfiguration) - .getThinLetterboxWidthPx(); - final AppCompatReachabilityOverrides reachabilityOverrides = mActivity.mAppCompatController - .getAppCompatReachabilityOverrides(); - assertFalse(reachabilityOverrides.isHorizontalThinLetterboxed()); - // Define a Task 100x100 - final Task task = mock(Task.class); - doReturn(new Rect(0, 0, 100, 100)).when(task).getBounds(); - doReturn(10).when(mActivity.mWmService.mAppCompatConfiguration) - .getThinLetterboxWidthPx(); - - // Vertical thin letterbox disabled without Task - doReturn(null).when(mActivity).getTask(); - assertFalse(reachabilityOverrides.isHorizontalThinLetterboxed()); - // Assign a Task for the Activity - doReturn(task).when(mActivity).getTask(); - - // (task.height() - act.height()) / 2 = 5 < 10 - doReturn(new Rect(5, 5, 95, 95)).when(mActivity).getBounds(); - assertTrue(reachabilityOverrides.isHorizontalThinLetterboxed()); - - // (task.height() - act.height()) / 2 = 10 = 10 - doReturn(new Rect(10, 10, 90, 90)).when(mActivity).getBounds(); - assertTrue(reachabilityOverrides.isHorizontalThinLetterboxed()); - - // (task.height() - act.height()) / 2 = 11 > 10 - doReturn(new Rect(11, 11, 89, 89)).when(mActivity).getBounds(); - assertFalse(reachabilityOverrides.isHorizontalThinLetterboxed()); - } - - @Test - @EnableFlags(Flags.FLAG_DISABLE_THIN_LETTERBOXING_POLICY) - public void testAllowReachabilityForThinLetterboxWithFlagEnabled() { - final AppCompatReachabilityOverrides reachabilityOverrides = - mActivity.mAppCompatController.getAppCompatReachabilityOverrides(); - spyOn(reachabilityOverrides); - doReturn(true).when(reachabilityOverrides).isVerticalThinLetterboxed(); - assertFalse(reachabilityOverrides.allowVerticalReachabilityForThinLetterbox()); - doReturn(true).when(reachabilityOverrides).isHorizontalThinLetterboxed(); - assertFalse(reachabilityOverrides.allowHorizontalReachabilityForThinLetterbox()); - - doReturn(false).when(reachabilityOverrides).isVerticalThinLetterboxed(); - assertTrue(reachabilityOverrides.allowVerticalReachabilityForThinLetterbox()); - doReturn(false).when(reachabilityOverrides).isHorizontalThinLetterboxed(); - assertTrue(reachabilityOverrides.allowHorizontalReachabilityForThinLetterbox()); - } - - @Test - @DisableFlags(Flags.FLAG_DISABLE_THIN_LETTERBOXING_POLICY) - public void testAllowReachabilityForThinLetterboxWithFlagDisabled() { - final AppCompatReachabilityOverrides reachabilityOverrides = - mActivity.mAppCompatController.getAppCompatReachabilityOverrides(); - spyOn(reachabilityOverrides); - doReturn(true).when(reachabilityOverrides).isVerticalThinLetterboxed(); - assertTrue(reachabilityOverrides.allowVerticalReachabilityForThinLetterbox()); - doReturn(true).when(reachabilityOverrides).isHorizontalThinLetterboxed(); - assertTrue(reachabilityOverrides.allowHorizontalReachabilityForThinLetterbox()); - - doReturn(false).when(reachabilityOverrides).isVerticalThinLetterboxed(); - assertTrue(reachabilityOverrides.allowVerticalReachabilityForThinLetterbox()); - doReturn(false).when(reachabilityOverrides).isHorizontalThinLetterboxed(); - assertTrue(reachabilityOverrides.allowHorizontalReachabilityForThinLetterbox()); - } - - @Test public void testIsLetterboxEducationEnabled() { mController.isLetterboxEducationEnabled(); verify(mAppCompatConfiguration).getIsEducationEnabled(); diff --git a/services/tests/wmtests/src/com/android/server/wm/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 3e68b6b7bbe3..aa997ac42c66 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java @@ -889,7 +889,7 @@ public class SizeCompatTests extends WindowTestsBase { verify(mTask).onSizeCompatActivityChanged(); ActivityManager.RunningTaskInfo taskInfo = mTask.getTaskInfo(); - assertTrue(taskInfo.appCompatTaskInfo.topActivityInSizeCompat); + assertTrue(taskInfo.appCompatTaskInfo.isTopActivityInSizeCompat()); // Make the activity resizable again by restarting it clearInvocations(mTask); @@ -904,7 +904,7 @@ public class SizeCompatTests extends WindowTestsBase { verify(mTask).onSizeCompatActivityChanged(); taskInfo = mTask.getTaskInfo(); - assertFalse(taskInfo.appCompatTaskInfo.topActivityInSizeCompat); + assertFalse(taskInfo.appCompatTaskInfo.isTopActivityInSizeCompat()); } @Test @@ -922,7 +922,7 @@ public class SizeCompatTests extends WindowTestsBase { verify(mTask).onSizeCompatActivityChanged(); ActivityManager.RunningTaskInfo taskInfo = mTask.getTaskInfo(); - assertTrue(taskInfo.appCompatTaskInfo.topActivityInSizeCompat); + assertTrue(taskInfo.appCompatTaskInfo.isTopActivityInSizeCompat()); // Create another Task to hold another size compat activity. clearInvocations(mTask); @@ -942,7 +942,7 @@ public class SizeCompatTests extends WindowTestsBase { verify(mTask, never()).onSizeCompatActivityChanged(); taskInfo = secondTask.getTaskInfo(); - assertTrue(taskInfo.appCompatTaskInfo.topActivityInSizeCompat); + assertTrue(taskInfo.appCompatTaskInfo.isTopActivityInSizeCompat()); } @Test @@ -4744,7 +4744,7 @@ public class SizeCompatTests extends WindowTestsBase { assertTrue(mActivity.inSizeCompatMode()); assertEquals(mActivity.getState(), PAUSED); assertTrue(mActivity.isVisible()); - assertTrue(mTask.getTaskInfo().appCompatTaskInfo.topActivityInSizeCompat); + assertTrue(mTask.getTaskInfo().appCompatTaskInfo.isTopActivityInSizeCompat()); } /** diff --git a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java index b92af876ed22..1e39f0b963b7 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java +++ b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java @@ -139,7 +139,6 @@ public class SystemServicesTestRule implements TestRule { private ActivityTaskManagerService mAtmService; private WindowManagerService mWmService; private InputManagerService mImService; - private InputChannel mInputChannel; private Runnable mOnBeforeServicesCreated; /** * Spied {@link SurfaceControl.Transaction} class than can be used to verify calls. @@ -326,12 +325,15 @@ public class SystemServicesTestRule implements TestRule { // InputManagerService mImService = mock(InputManagerService.class); - // InputChannel cannot be mocked because it may pass to InputEventReceiver. - final InputChannel[] inputChannels = InputChannel.openInputChannelPair(TAG); - inputChannels[0].dispose(); - mInputChannel = inputChannels[1]; - doReturn(mInputChannel).when(mImService).monitorInput(anyString(), anyInt()); - doReturn(mInputChannel).when(mImService).createInputChannel(anyString()); + // InputChannel cannot be mocked because it may be passed to InputEventReceiver. + Answer<InputChannel> newInputChannel = invocation -> { + String name = invocation.getArgument(0); + final InputChannel[] channels = InputChannel.openInputChannelPair(name); + channels[0].dispose(); + return channels[1]; + }; + when(mImService.monitorInput(anyString(), anyInt())).thenAnswer(newInputChannel); + when(mImService.createInputChannel(anyString())).thenAnswer(newInputChannel); // StatusBarManagerInternal final StatusBarManagerInternal sbmi = mock(StatusBarManagerInternal.class); @@ -464,9 +466,6 @@ public class SystemServicesTestRule implements TestRule { SurfaceAnimationThread.dispose(); AnimationThread.dispose(); UiThread.dispose(); - if (mInputChannel != null) { - mInputChannel.dispose(); - } tearDownLocalServices(); // Reset priority booster because animation thread has been changed. 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/TaskTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java index a232ff0dfcb6..0a592f2ae92a 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java @@ -635,27 +635,27 @@ public class TaskTests extends WindowTestsBase { // The button should be eligible to be displayed assertTrue(task.getTaskInfo() - .appCompatTaskInfo.topActivityEligibleForUserAspectRatioButton); + .appCompatTaskInfo.eligibleForUserAspectRatioButton()); // When shouldApplyUserMinAspectRatioOverride is disable the button is not enabled doReturn(false).when( root.mAppCompatController.getAppCompatAspectRatioOverrides()) .shouldEnableUserAspectRatioSettings(); assertFalse(task.getTaskInfo() - .appCompatTaskInfo.topActivityEligibleForUserAspectRatioButton); + .appCompatTaskInfo.eligibleForUserAspectRatioButton()); doReturn(true).when(root.mAppCompatController .getAppCompatAspectRatioOverrides()).shouldEnableUserAspectRatioSettings(); // When in size compat mode the button is not enabled doReturn(true).when(root).inSizeCompatMode(); assertFalse(task.getTaskInfo() - .appCompatTaskInfo.topActivityEligibleForUserAspectRatioButton); + .appCompatTaskInfo.eligibleForUserAspectRatioButton()); doReturn(false).when(root).inSizeCompatMode(); // When the top activity is transparent, the button is not enabled doReturn(false).when(root).fillsParent(); assertFalse(task.getTaskInfo() - .appCompatTaskInfo.topActivityEligibleForUserAspectRatioButton); + .appCompatTaskInfo.eligibleForUserAspectRatioButton()); doReturn(true).when(root).fillsParent(); } 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 fb81a52bce85..2b611b754edd 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java @@ -1708,7 +1708,7 @@ public class WindowOrganizerTests extends WindowTestsBase { verify(organizer).onTaskInfoChanged(infoCaptor.capture()); RunningTaskInfo info = infoCaptor.getValue(); assertEquals(rootTask.mTaskId, info.taskId); - assertTrue(info.appCompatTaskInfo.topActivityInSizeCompat); + assertTrue(info.appCompatTaskInfo.isTopActivityInSizeCompat()); // Ensure task info show top activity that is not visible as not in size compat. clearInvocations(organizer); @@ -1718,7 +1718,7 @@ public class WindowOrganizerTests extends WindowTestsBase { verify(organizer).onTaskInfoChanged(infoCaptor.capture()); info = infoCaptor.getValue(); assertEquals(rootTask.mTaskId, info.taskId); - assertFalse(info.appCompatTaskInfo.topActivityInSizeCompat); + assertFalse(info.appCompatTaskInfo.isTopActivityInSizeCompat()); // Ensure task info show non size compat top activity as not in size compat. clearInvocations(organizer); @@ -1729,7 +1729,7 @@ public class WindowOrganizerTests extends WindowTestsBase { verify(organizer).onTaskInfoChanged(infoCaptor.capture()); info = infoCaptor.getValue(); assertEquals(rootTask.mTaskId, info.taskId); - assertFalse(info.appCompatTaskInfo.topActivityInSizeCompat); + assertFalse(info.appCompatTaskInfo.isTopActivityInSizeCompat()); } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java index 11df331ff398..39276a191fb9 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java @@ -77,8 +77,10 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.atMost; import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.reset; import static org.mockito.Mockito.when; import android.content.res.CompatibilityInfo; @@ -111,6 +113,7 @@ import android.window.TaskFragmentOrganizer; import androidx.test.filters.SmallTest; +import com.android.server.inputmethod.InputMethodManagerInternal; import com.android.server.testutils.StubTransaction; import com.android.server.wm.SensitiveContentPackages.PackageInfo; @@ -1337,8 +1340,8 @@ public class WindowStateTests extends WindowTestsBase { @Test public void testImeTargetChangeListener_OnImeInputTargetVisibilityChanged() { - final TestImeTargetChangeListener listener = new TestImeTargetChangeListener(); - mWm.mImeTargetChangeListener = listener; + final InputMethodManagerInternal immi = InputMethodManagerInternal.get(); + spyOn(immi); final WindowState imeTarget = createWindow(null /* parent */, TYPE_BASE_APPLICATION, createActivityRecord(mDisplayContent), "imeTarget"); @@ -1347,32 +1350,26 @@ public class WindowStateTests extends WindowTestsBase { makeWindowVisible(imeTarget); mDisplayContent.setImeInputTarget(imeTarget); waitHandlerIdle(mWm.mH); - - assertThat(listener.mImeTargetToken).isEqualTo(imeTarget.mClient.asBinder()); - assertThat(listener.mIsRemoved).isFalse(); - assertThat(listener.mIsVisibleForImeInputTarget).isTrue(); - assertThat(listener.mDisplayId).isEqualTo(mDisplayContent.getDisplayId()); + verify(immi).onImeInputTargetVisibilityChanged(imeTarget.mClient.asBinder(), + true /* visibleAndNotRemoved */, mDisplayContent.getDisplayId()); + reset(immi); imeTarget.mActivityRecord.setVisibleRequested(false); waitHandlerIdle(mWm.mH); - - assertThat(listener.mImeTargetToken).isEqualTo(imeTarget.mClient.asBinder()); - assertThat(listener.mIsRemoved).isFalse(); - assertThat(listener.mIsVisibleForImeInputTarget).isFalse(); - assertThat(listener.mDisplayId).isEqualTo(mDisplayContent.getDisplayId()); + verify(immi).onImeInputTargetVisibilityChanged(imeTarget.mClient.asBinder(), + false /* visibleAndNotRemoved */, mDisplayContent.getDisplayId()); + reset(immi); imeTarget.removeImmediately(); - assertThat(listener.mImeTargetToken).isEqualTo(imeTarget.mClient.asBinder()); - assertThat(listener.mIsRemoved).isTrue(); - assertThat(listener.mIsVisibleForImeInputTarget).isFalse(); - assertThat(listener.mDisplayId).isEqualTo(mDisplayContent.getDisplayId()); + verify(immi).onImeInputTargetVisibilityChanged(imeTarget.mClient.asBinder(), + false /* visibleAndNotRemoved */, mDisplayContent.getDisplayId()); } @SetupWindows(addWindows = {W_INPUT_METHOD}) @Test public void testImeTargetChangeListener_OnImeTargetOverlayVisibilityChanged() { - final TestImeTargetChangeListener listener = new TestImeTargetChangeListener(); - mWm.mImeTargetChangeListener = listener; + final InputMethodManagerInternal immi = InputMethodManagerInternal.get(); + spyOn(immi); // Scenario 1: test addWindow/relayoutWindow to add Ime layering overlay window as visible. final WindowToken windowToken = createTestWindowToken(TYPE_APPLICATION_OVERLAY, @@ -1402,10 +1399,10 @@ public class WindowStateTests extends WindowTestsBase { final WindowState imeLayeringTargetOverlay = mDisplayContent.getWindow( w -> w.mClient.asBinder() == client.asBinder()); assertThat(imeLayeringTargetOverlay.isVisible()).isTrue(); - assertThat(listener.mImeTargetToken).isEqualTo(client.asBinder()); - assertThat(listener.mIsRemoved).isFalse(); - assertThat(listener.mIsVisibleForImeTargetOverlay).isTrue(); - assertThat(listener.mDisplayId).isEqualTo(mDisplayContent.getDisplayId()); + verify(immi, atLeast(1)) + .setHasVisibleImeLayeringOverlay(true /* hasVisibleOverlay */, + mDisplayContent.getDisplayId()); + reset(immi); // Scenario 2: test relayoutWindow to let the Ime layering target overlay window invisible. mWm.relayoutWindow(session, client, params, 100, 200, View.GONE, 0, 0, 0, @@ -1413,19 +1410,16 @@ public class WindowStateTests extends WindowTestsBase { waitHandlerIdle(mWm.mH); assertThat(imeLayeringTargetOverlay.isVisible()).isFalse(); - assertThat(listener.mImeTargetToken).isEqualTo(client.asBinder()); - assertThat(listener.mIsRemoved).isFalse(); - assertThat(listener.mIsVisibleForImeTargetOverlay).isFalse(); - assertThat(listener.mDisplayId).isEqualTo(mDisplayContent.getDisplayId()); + verify(immi).setHasVisibleImeLayeringOverlay(false /* hasVisibleOverlay */, + mDisplayContent.getDisplayId()); + reset(immi); // Scenario 3: test removeWindow to remove the Ime layering target overlay window. mWm.removeClientToken(session, client.asBinder()); waitHandlerIdle(mWm.mH); - assertThat(listener.mImeTargetToken).isEqualTo(client.asBinder()); - assertThat(listener.mIsRemoved).isTrue(); - assertThat(listener.mIsVisibleForImeTargetOverlay).isFalse(); - assertThat(listener.mDisplayId).isEqualTo(mDisplayContent.getDisplayId()); + verify(immi).setHasVisibleImeLayeringOverlay(false /* hasVisibleOverlay */, + mDisplayContent.getDisplayId()); } @Test @@ -1468,31 +1462,4 @@ public class WindowStateTests extends WindowTestsBase { mWm.mSensitiveContentPackages.removeBlockScreenCaptureForApps(blockedPackages); assertFalse(window.isSecureLocked()); } - - private static class TestImeTargetChangeListener implements ImeTargetChangeListener { - private IBinder mImeTargetToken; - private boolean mIsRemoved; - private boolean mIsVisibleForImeTargetOverlay; - private boolean mIsVisibleForImeInputTarget; - private int mDisplayId; - - @Override - public void onImeTargetOverlayVisibilityChanged(IBinder overlayWindowToken, - @WindowManager.LayoutParams.WindowType int windowType, boolean visible, - boolean removed, int displayId) { - mImeTargetToken = overlayWindowToken; - mIsVisibleForImeTargetOverlay = visible; - mIsRemoved = removed; - mDisplayId = displayId; - } - - @Override - public void onImeInputTargetVisibilityChanged(IBinder imeInputTarget, - boolean visibleRequested, boolean removed, int displayId) { - mImeTargetToken = imeInputTarget; - mIsVisibleForImeInputTarget = visibleRequested; - mIsRemoved = removed; - mDisplayId = displayId; - } - } } 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/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/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 7212beb6ae4b..36bfbefdb086 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt @@ -33,6 +33,8 @@ import com.android.hoststubgen.visitors.PackageRedirectRemapper import org.objectweb.asm.ClassReader import org.objectweb.asm.ClassVisitor import org.objectweb.asm.ClassWriter +import org.objectweb.asm.commons.ClassRemapper +import org.objectweb.asm.commons.Remapper import org.objectweb.asm.util.CheckClassAdapter import java.io.BufferedInputStream import java.io.FileOutputStream @@ -56,21 +58,23 @@ 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. - val filter = buildFilter(errors, allClasses, options) + val (filter, policyFileRemapper) = buildFilter(errors, allClasses, options) // Transform the jar. convert( @@ -82,20 +86,25 @@ class HostStubGen(val options: HostStubGenOptions) { allClasses, errors, stats, + policyFileRemapper, + options.numShards.get, + options.shard.get, ) // 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") } } @@ -107,7 +116,7 @@ class HostStubGen(val options: HostStubGenOptions) { errors: HostStubGenErrors, allClasses: ClassNodes, options: HostStubGenOptions, - ): OutputFilter { + ): Pair<OutputFilter, Remapper?> { // We build a "chain" of multiple filters here. // // The filters are build in from "inside", meaning the first filter created here is @@ -160,10 +169,14 @@ class HostStubGen(val options: HostStubGenOptions) { filter, ) + var policyFileRemapper: Remapper? = null + // Next, "text based" filter, which allows to override polices without touching // the target code. options.policyOverrideFile.ifSet { - filter = createFilterFromTextPolicyFile(it, allClasses, filter) + val (f, p) = createFilterFromTextPolicyFile(it, allClasses, filter) + filter = f + policyFileRemapper = p } // If `--intersect-stub-jar` is provided, load from these jar files too. @@ -178,7 +191,7 @@ class HostStubGen(val options: HostStubGenOptions) { // Apply the implicit filter. filter = ImplicitOutputFilter(errors, allClasses, filter) - return filter + return Pair(filter, policyFileRemapper) } /** @@ -205,35 +218,55 @@ class HostStubGen(val options: HostStubGenOptions) { classes: ClassNodes, errors: HostStubGenErrors, stats: HostStubGenStats, + remapper: Remapper?, + numShards: Int, + shard: Int, ) { 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() + log.iTime("Transforming jar") { + val packageRedirector = PackageRedirectRemapper(options.packageRedirects) - val packageRedirector = PackageRedirectRemapper(options.packageRedirects) + 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 -> - maybeWithZipOutputStream(outStubJar) { stubOutStream -> - maybeWithZipOutputStream(outImplJar) { implOutStream -> - val inEntries = inZip.entries() - while (inEntries.hasMoreElements()) { - val entry = inEntries.nextElement() - convertSingleEntry(inZip, entry, stubOutStream, implOutStream, - filter, packageRedirector, enableChecker, classes, errors, - stats) + 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++ + } + 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).", (end - start) / 1000.0) } private fun <T> maybeWithZipOutputStream(filename: String?, block: (ZipOutputStream?) -> T): T { @@ -253,6 +286,7 @@ class HostStubGen(val options: HostStubGenOptions) { implOutStream: ZipOutputStream?, filter: OutputFilter, packageRedirector: PackageRedirectRemapper, + remapper: Remapper?, enableChecker: Boolean, classes: ClassNodes, errors: HostStubGenErrors, @@ -270,7 +304,7 @@ class HostStubGen(val options: HostStubGenOptions) { // If it's a class, convert it. if (name.endsWith(".class")) { processSingleClass(inZip, entry, stubOutStream, implOutStream, filter, - packageRedirector, enableChecker, classes, errors, stats) + packageRedirector, remapper, enableChecker, classes, errors, stats) return } @@ -321,6 +355,7 @@ class HostStubGen(val options: HostStubGenOptions) { implOutStream: ZipOutputStream?, filter: OutputFilter, packageRedirector: PackageRedirectRemapper, + remapper: Remapper?, enableChecker: Boolean, classes: ClassNodes, errors: HostStubGenErrors, @@ -332,16 +367,24 @@ class HostStubGen(val options: HostStubGenOptions) { log.d("Removing class: %s %s", classInternalName, classPolicy) return } + // If we're applying a remapper, we need to rename the file too. + var newName = entry.name + remapper?.mapType(classInternalName)?.let { remappedName -> + if (remappedName != classInternalName) { + log.d("Renaming class file: %s -> %s", classInternalName, remappedName) + newName = remappedName + ".class" + } + } // Generate stub first. if (stubOutStream != null && classPolicy.policy.needsInStub) { log.v("Creating stub class: %s Policy: %s", classInternalName, classPolicy) log.withIndent { BufferedInputStream(inZip.getInputStream(entry)).use { bis -> - val newEntry = ZipEntry(entry.name) + val newEntry = ZipEntry(newName) stubOutStream.putNextEntry(newEntry) convertClass(classInternalName, /*forImpl=*/false, bis, - stubOutStream, filter, packageRedirector, enableChecker, classes, - errors, null) + stubOutStream, filter, packageRedirector, remapper, + enableChecker, classes, errors, null) stubOutStream.closeEntry() } } @@ -350,11 +393,11 @@ class HostStubGen(val options: HostStubGenOptions) { log.v("Creating impl class: %s Policy: %s", classInternalName, classPolicy) log.withIndent { BufferedInputStream(inZip.getInputStream(entry)).use { bis -> - val newEntry = ZipEntry(entry.name) + val newEntry = ZipEntry(newName) implOutStream.putNextEntry(newEntry) convertClass(classInternalName, /*forImpl=*/true, bis, - implOutStream, filter, packageRedirector, enableChecker, classes, - errors, stats) + implOutStream, filter, packageRedirector, remapper, + enableChecker, classes, errors, stats) implOutStream.closeEntry() } } @@ -371,6 +414,7 @@ class HostStubGen(val options: HostStubGenOptions) { out: OutputStream, filter: OutputFilter, packageRedirector: PackageRedirectRemapper, + remapper: Remapper?, enableChecker: Boolean, classes: ClassNodes, errors: HostStubGenErrors, @@ -387,6 +431,12 @@ class HostStubGen(val options: HostStubGenOptions) { if (enableChecker) { outVisitor = CheckClassAdapter(outVisitor) } + + // Remapping should happen at the end. + remapper?.let { + outVisitor = ClassRemapper(outVisitor, remapper) + } + val visitorOptions = BaseAdapter.Options( enablePreTrace = options.enablePreTrace.get, enablePostTrace = options.enablePostTrace.get, @@ -395,7 +445,7 @@ class HostStubGen(val options: HostStubGenOptions) { stats = stats, ) outVisitor = BaseAdapter.getVisitor(classInternalName, classes, outVisitor, filter, - packageRedirector, forImpl, visitorOptions) + packageRedirector, remapper, forImpl, visitorOptions) cr.accept(outVisitor, ClassReader.EXPAND_FRAMES) val data = cw.toByteArray() 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/HostStubGenOptions.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt index e192516d334d..2f833a873133 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt @@ -112,6 +112,9 @@ class HostStubGenOptions( var statsFile: SetOnce<String?> = SetOnce(null), var apiListFile: SetOnce<String?> = SetOnce(null), + + var numShards: SetOnce<Int> = SetOnce(1), + var shard: SetOnce<Int> = SetOnce(0), ) { companion object { @@ -162,6 +165,13 @@ class HostStubGenOptions( fun SetOnce<String?>.setNextStringArg(): String = nextArg().also { this.set(it) } fun MutableSet<String>.addUniqueAnnotationArg(): String = nextArg().also { this += ensureUniqueAnnotation(it) } + fun SetOnce<Int>.setNextIntArg(): String = nextArg().also { + try { + this.set(it.toInt()) + } catch (e: NumberFormatException) { + throw ArgumentsException("Invalid integer for $arg: $it") + } + } try { when (arg) { @@ -259,6 +269,9 @@ class HostStubGenOptions( "--stats-file" -> ret.statsFile.setNextStringArg() "--supported-api-list-file" -> ret.apiListFile.setNextStringArg() + "--num-shards" -> ret.numShards.setNextIntArg() + "--shard-index" -> ret.shard.setNextIntArg() + else -> throw ArgumentsException("Unknown option: $arg") } } catch (e: SetOnce.SetMoreThanOnceException) { @@ -396,6 +409,8 @@ class HostStubGenOptions( enableNonStubMethodCallDetection=$enableNonStubMethodCallDetection, statsFile=$statsFile, apiListFile=$apiListFile, + numShards=$numShards, + shard=$shard, } """.trimIndent() } 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/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt index a89824eaf0b0..18280034c2f4 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt @@ -22,11 +22,13 @@ import com.android.hoststubgen.log import com.android.hoststubgen.normalizeTextLine import com.android.hoststubgen.whitespaceRegex import org.objectweb.asm.Opcodes +import org.objectweb.asm.commons.Remapper import org.objectweb.asm.tree.ClassNode import java.io.BufferedReader import java.io.FileReader import java.io.PrintWriter import java.util.Objects +import java.util.regex.Pattern /** * Print a class node as a "keep" policy. @@ -60,7 +62,7 @@ fun createFilterFromTextPolicyFile( filename: String, classes: ClassNodes, fallback: OutputFilter, - ): OutputFilter { + ): Pair<OutputFilter, Remapper?> { log.i("Loading offloaded annotations from $filename ...") log.withIndent { val subclassFilter = SubclassFilter(classes, fallback) @@ -73,6 +75,7 @@ fun createFilterFromTextPolicyFile( var featureFlagsPolicy: FilterPolicyWithReason? = null var syspropsPolicy: FilterPolicyWithReason? = null var rFilePolicy: FilterPolicyWithReason? = null + val typeRenameSpec = mutableListOf<TextFilePolicyRemapper.TypeRenameSpec>() try { BufferedReader(FileReader(filename)).use { reader -> @@ -251,6 +254,22 @@ fun createFilterFromTextPolicyFile( imf.setRenameTo(className, fromName, signature, name) } } + "r", "rename" -> { + if (fields.size < 3) { + throw ParseException("Rename ('r') expects 2 fields.") + } + // Add ".*" to make it a prefix match. + val pattern = Pattern.compile(fields[1] + ".*") + + // Removing the leading /'s from the prefix. This allows + // using a single '/' as an empty suffix, which is useful to have a + // "negative" rename rule to avoid subsequent raname's from getting + // applied. (Which is needed for services.jar) + val prefix = fields[2].trimStart('/') + + typeRenameSpec += TextFilePolicyRemapper.TypeRenameSpec( + pattern, prefix) + } else -> { throw ParseException("Unknown directive \"${fields[0]}\"") @@ -262,9 +281,16 @@ fun createFilterFromTextPolicyFile( throw e.withSourceInfo(filename, lineNo) } + var remapper: TextFilePolicyRemapper? = null + if (typeRenameSpec.isNotEmpty()) { + remapper = TextFilePolicyRemapper(typeRenameSpec) + } + // Wrap the in-memory-filter with AHF. - return AndroidHeuristicsFilter( - classes, aidlPolicy, featureFlagsPolicy, syspropsPolicy, rFilePolicy, imf) + return Pair( + AndroidHeuristicsFilter( + classes, aidlPolicy, featureFlagsPolicy, syspropsPolicy, rFilePolicy, imf), + remapper) } } diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFilePolicyRemapper.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFilePolicyRemapper.kt new file mode 100644 index 000000000000..2d94bb4758ba --- /dev/null +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFilePolicyRemapper.kt @@ -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. + */ +package com.android.hoststubgen.filters + +import com.android.hoststubgen.log +import org.objectweb.asm.commons.Remapper +import java.util.regex.Pattern + +/** + * A [Remapper] that provides a simple "jarjar" functionality. + */ +class TextFilePolicyRemapper( + val typeRenameSpecs: List<TypeRenameSpec> +) : Remapper() { + /** + * When a package name matches [typeInternalNamePattern], we prepend [typeInternalNamePrefix] + * to it. + */ + data class TypeRenameSpec( + val typeInternalNamePattern: Pattern, + val typeInternalNamePrefix: String, + ) + + private val cache = mutableMapOf<String, String>() + + override fun mapType(typeInternalName: String): String { +// if (typeInternalName == null) { +// return null // do we need it?? +// } + cache[typeInternalName]?.let { + return it + } + + var mapped: String = typeInternalName + typeRenameSpecs.forEach { + if (it.typeInternalNamePattern.matcher(typeInternalName).matches()) { + mapped = it.typeInternalNamePrefix + typeInternalName + log.d("Renaming type $typeInternalName to $mapped") + } + } + cache[typeInternalName] = mapped + return mapped + } + + // TODO Do we need to implement mapPackage(), etc too? +} diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt index c99ff0e5d990..bad0449f1efd 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt @@ -34,6 +34,7 @@ import org.objectweb.asm.FieldVisitor import org.objectweb.asm.MethodVisitor import org.objectweb.asm.Opcodes import org.objectweb.asm.commons.ClassRemapper +import org.objectweb.asm.commons.Remapper import org.objectweb.asm.util.TraceClassVisitor import java.io.PrintWriter @@ -259,13 +260,14 @@ abstract class BaseAdapter ( companion object { fun getVisitor( - classInternalName: String, - classes: ClassNodes, - nextVisitor: ClassVisitor, - filter: OutputFilter, - packageRedirector: PackageRedirectRemapper, - forImpl: Boolean, - options: Options, + classInternalName: String, + classes: ClassNodes, + nextVisitor: ClassVisitor, + filter: OutputFilter, + packageRedirector: PackageRedirectRemapper, + remapper: Remapper?, + forImpl: Boolean, + options: Options, ): ClassVisitor { var next = nextVisitor diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/PackageRedirectRemapper.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/PackageRedirectRemapper.kt index b3790e12a111..e90ecd7ef678 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/PackageRedirectRemapper.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/PackageRedirectRemapper.kt @@ -20,6 +20,18 @@ import org.objectweb.asm.commons.Remapper /** * A [Remapper] for `--package-redirect` + * + * This is a feature to update all calls to specific packages to another package, which allows + * implementing a class in a different package, when the original package isn't allowed to modify. + * + * For example, using this, we can implement `dalvik.*` APIs in a separate package, and update + * all calls to the `dalvik` package to a different package. + * + * For this purpose, we don't expect the "renamed-from" classes to be in the target jar, + * so we don't apply the remapper to them. (The exclusion happens at the callsite of this class) + * + * TODO: Currently it's not used. Maybe remove, or just unify with the other remapper feature + * with TextFileFilterPolicyParser.kt. */ class PackageRedirectRemapper( packageRedirects: List<Pair<String, String>>, diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/01-hoststubgen-test-tiny-framework-orig-dump.txt b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/01-hoststubgen-test-tiny-framework-orig-dump.txt index 3ef117567482..c127e677f84d 100644 --- a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/01-hoststubgen-test-tiny-framework-orig-dump.txt +++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/01-hoststubgen-test-tiny-framework-orig-dump.txt @@ -2706,6 +2706,95 @@ SourceFile: "TinyFrameworkPackageRedirect.java" RuntimeInvisibleAnnotations: x: #x() android.hosttest.annotation.HostSideTestWholeClassStub +## Class: com/android/hoststubgen/test/tinyframework/TinyFrameworkRenamedClassCaller.class + Compiled from "TinyFrameworkRenamedClassCaller.java" +public class com.android.hoststubgen.test.tinyframework.TinyFrameworkRenamedClassCaller + minor version: 0 + major version: 61 + flags: (0x0021) ACC_PUBLIC, ACC_SUPER + this_class: #x // com/android/hoststubgen/test/tinyframework/TinyFrameworkRenamedClassCaller + super_class: #x // java/lang/Object + interfaces: 0, fields: 0, methods: 2, attributes: 2 + public com.android.hoststubgen.test.tinyframework.TinyFrameworkRenamedClassCaller(); + descriptor: ()V + flags: (0x0001) ACC_PUBLIC + Code: + stack=1, locals=1, args_size=1 + x: aload_0 + x: invokespecial #x // Method java/lang/Object."<init>":()V + x: return + LineNumberTable: + LocalVariableTable: + Start Length Slot Name Signature + 0 5 0 this Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkRenamedClassCaller; + + public static int foo(int); + descriptor: (I)I + flags: (0x0009) ACC_PUBLIC, ACC_STATIC + Code: + stack=3, locals=1, args_size=1 + x: new #x // class com/android/hoststubgen/test/tinyframework/TinyFrameworkToBeRenamed + x: dup + x: iload_0 + x: invokespecial #x // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkToBeRenamed."<init>":(I)V + x: invokevirtual #x // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkToBeRenamed.getValue:()I + x: ireturn + LineNumberTable: + LocalVariableTable: + Start Length Slot Name Signature + 0 12 0 value I +} +SourceFile: "TinyFrameworkRenamedClassCaller.java" +RuntimeInvisibleAnnotations: + x: #x() + android.hosttest.annotation.HostSideTestWholeClassStub +## Class: com/android/hoststubgen/test/tinyframework/TinyFrameworkToBeRenamed.class + Compiled from "TinyFrameworkToBeRenamed.java" +public class com.android.hoststubgen.test.tinyframework.TinyFrameworkToBeRenamed + minor version: 0 + major version: 61 + flags: (0x0021) ACC_PUBLIC, ACC_SUPER + this_class: #x // com/android/hoststubgen/test/tinyframework/TinyFrameworkToBeRenamed + super_class: #x // java/lang/Object + interfaces: 0, fields: 1, methods: 2, attributes: 2 + private final int mValue; + descriptor: I + flags: (0x0012) ACC_PRIVATE, ACC_FINAL + + public com.android.hoststubgen.test.tinyframework.TinyFrameworkToBeRenamed(int); + descriptor: (I)V + flags: (0x0001) ACC_PUBLIC + Code: + stack=2, locals=2, args_size=2 + x: aload_0 + x: invokespecial #x // Method java/lang/Object."<init>":()V + x: aload_0 + x: iload_1 + x: putfield #x // Field mValue:I + x: return + LineNumberTable: + LocalVariableTable: + Start Length Slot Name Signature + 0 10 0 this Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkToBeRenamed; + 0 10 1 value I + + public int getValue(); + descriptor: ()I + flags: (0x0001) ACC_PUBLIC + Code: + stack=1, locals=1, args_size=1 + x: aload_0 + x: getfield #x // Field mValue:I + x: ireturn + LineNumberTable: + LocalVariableTable: + Start Length Slot Name Signature + 0 5 0 this Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkToBeRenamed; +} +SourceFile: "TinyFrameworkToBeRenamed.java" +RuntimeInvisibleAnnotations: + x: #x() + android.hosttest.annotation.HostSideTestWholeClassStub ## Class: com/android/hoststubgen/test/tinyframework/packagetest/A.class Compiled from "A.java" public class com.android.hoststubgen.test.tinyframework.packagetest.A diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/02-hoststubgen-test-tiny-framework-host-stub-dump.txt b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/02-hoststubgen-test-tiny-framework-host-stub-dump.txt index 0bbb4182859b..17ba48c67d98 100644 --- a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/02-hoststubgen-test-tiny-framework-host-stub-dump.txt +++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/02-hoststubgen-test-tiny-framework-host-stub-dump.txt @@ -2177,6 +2177,56 @@ RuntimeVisibleAnnotations: RuntimeInvisibleAnnotations: x: #x() android.hosttest.annotation.HostSideTestWholeClassStub +## Class: com/android/hoststubgen/test/tinyframework/TinyFrameworkRenamedClassCaller.class + Compiled from "TinyFrameworkRenamedClassCaller.java" +public class com.android.hoststubgen.test.tinyframework.TinyFrameworkRenamedClassCaller + minor version: 0 + major version: 61 + flags: (0x0021) ACC_PUBLIC, ACC_SUPER + this_class: #x // com/android/hoststubgen/test/tinyframework/TinyFrameworkRenamedClassCaller + super_class: #x // java/lang/Object + interfaces: 0, fields: 0, methods: 2, attributes: 3 + public com.android.hoststubgen.test.tinyframework.TinyFrameworkRenamedClassCaller(); + descriptor: ()V + flags: (0x0001) ACC_PUBLIC + Code: + stack=3, locals=1, args_size=1 + x: new #x // class java/lang/RuntimeException + x: dup + x: ldc #x // String Stub! + x: invokespecial #x // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V + x: athrow + RuntimeVisibleAnnotations: + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInStub + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInImpl + + public static int foo(int); + descriptor: (I)I + flags: (0x0009) ACC_PUBLIC, ACC_STATIC + Code: + stack=3, locals=1, args_size=1 + x: new #x // class java/lang/RuntimeException + x: dup + x: ldc #x // String Stub! + x: invokespecial #x // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V + x: athrow + RuntimeVisibleAnnotations: + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInStub + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInImpl +} +SourceFile: "TinyFrameworkRenamedClassCaller.java" +RuntimeVisibleAnnotations: + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInStub + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInImpl +RuntimeInvisibleAnnotations: + x: #x() + android.hosttest.annotation.HostSideTestWholeClassStub ## Class: com/android/hoststubgen/test/tinyframework/packagetest/A.class Compiled from "A.java" public class com.android.hoststubgen.test.tinyframework.packagetest.A @@ -2419,3 +2469,62 @@ RuntimeVisibleAnnotations: RuntimeInvisibleAnnotations: x: #x() android.hosttest.annotation.HostSideTestWholeClassStub +## Class: rename_prefix/com/android/hoststubgen/test/tinyframework/TinyFrameworkToBeRenamed.class + Compiled from "TinyFrameworkToBeRenamed.java" +public class rename_prefix.com.android.hoststubgen.test.tinyframework.TinyFrameworkToBeRenamed + minor version: 0 + major version: 61 + flags: (0x0021) ACC_PUBLIC, ACC_SUPER + this_class: #x // rename_prefix/com/android/hoststubgen/test/tinyframework/TinyFrameworkToBeRenamed + super_class: #x // java/lang/Object + interfaces: 0, fields: 1, methods: 2, attributes: 3 + private final int mValue; + descriptor: I + flags: (0x0012) ACC_PRIVATE, ACC_FINAL + RuntimeVisibleAnnotations: + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInStub + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInImpl + + public rename_prefix.com.android.hoststubgen.test.tinyframework.TinyFrameworkToBeRenamed(int); + descriptor: (I)V + flags: (0x0001) ACC_PUBLIC + Code: + stack=3, locals=2, args_size=2 + x: new #x // class java/lang/RuntimeException + x: dup + x: ldc #x // String Stub! + x: invokespecial #x // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V + x: athrow + RuntimeVisibleAnnotations: + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInStub + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInImpl + + public int getValue(); + descriptor: ()I + flags: (0x0001) ACC_PUBLIC + Code: + stack=3, locals=1, args_size=1 + x: new #x // class java/lang/RuntimeException + x: dup + x: ldc #x // String Stub! + x: invokespecial #x // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V + x: athrow + RuntimeVisibleAnnotations: + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInStub + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInImpl +} +SourceFile: "TinyFrameworkToBeRenamed.java" +RuntimeVisibleAnnotations: + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInStub + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInImpl +RuntimeInvisibleAnnotations: + x: #x() + android.hosttest.annotation.HostSideTestWholeClassStub diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/03-hoststubgen-test-tiny-framework-host-impl-dump.txt b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/03-hoststubgen-test-tiny-framework-host-impl-dump.txt index 57f3783e8a73..0f5f7e747a2e 100644 --- a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/03-hoststubgen-test-tiny-framework-host-impl-dump.txt +++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/03-hoststubgen-test-tiny-framework-host-impl-dump.txt @@ -3540,6 +3540,63 @@ RuntimeVisibleAnnotations: RuntimeInvisibleAnnotations: x: #x() android.hosttest.annotation.HostSideTestWholeClassStub +## Class: com/android/hoststubgen/test/tinyframework/TinyFrameworkRenamedClassCaller.class + Compiled from "TinyFrameworkRenamedClassCaller.java" +public class com.android.hoststubgen.test.tinyframework.TinyFrameworkRenamedClassCaller + minor version: 0 + major version: 61 + flags: (0x0021) ACC_PUBLIC, ACC_SUPER + this_class: #x // com/android/hoststubgen/test/tinyframework/TinyFrameworkRenamedClassCaller + super_class: #x // java/lang/Object + interfaces: 0, fields: 0, methods: 2, attributes: 3 + public com.android.hoststubgen.test.tinyframework.TinyFrameworkRenamedClassCaller(); + descriptor: ()V + flags: (0x0001) ACC_PUBLIC + Code: + stack=1, locals=1, args_size=1 + x: aload_0 + x: invokespecial #x // Method java/lang/Object."<init>":()V + x: return + LineNumberTable: + LocalVariableTable: + Start Length Slot Name Signature + 0 5 0 this Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkRenamedClassCaller; + RuntimeVisibleAnnotations: + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInStub + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInImpl + + public static int foo(int); + descriptor: (I)I + flags: (0x0009) ACC_PUBLIC, ACC_STATIC + Code: + stack=3, locals=1, args_size=1 + x: new #x // class rename_prefix/com/android/hoststubgen/test/tinyframework/TinyFrameworkToBeRenamed + x: dup + x: iload_0 + x: invokespecial #x // Method rename_prefix/com/android/hoststubgen/test/tinyframework/TinyFrameworkToBeRenamed."<init>":(I)V + x: invokevirtual #x // Method rename_prefix/com/android/hoststubgen/test/tinyframework/TinyFrameworkToBeRenamed.getValue:()I + x: ireturn + LineNumberTable: + LocalVariableTable: + Start Length Slot Name Signature + 0 12 0 value I + RuntimeVisibleAnnotations: + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInStub + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInImpl +} +SourceFile: "TinyFrameworkRenamedClassCaller.java" +RuntimeVisibleAnnotations: + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInStub + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInImpl +RuntimeInvisibleAnnotations: + x: #x() + android.hosttest.annotation.HostSideTestWholeClassStub ## Class: com/android/hoststubgen/test/tinyframework/packagetest/A.class Compiled from "A.java" public class com.android.hoststubgen.test.tinyframework.packagetest.A @@ -3962,3 +4019,70 @@ RuntimeVisibleAnnotations: RuntimeInvisibleAnnotations: x: #x() android.hosttest.annotation.HostSideTestWholeClassStub +## Class: rename_prefix/com/android/hoststubgen/test/tinyframework/TinyFrameworkToBeRenamed.class + Compiled from "TinyFrameworkToBeRenamed.java" +public class rename_prefix.com.android.hoststubgen.test.tinyframework.TinyFrameworkToBeRenamed + minor version: 0 + major version: 61 + flags: (0x0021) ACC_PUBLIC, ACC_SUPER + this_class: #x // rename_prefix/com/android/hoststubgen/test/tinyframework/TinyFrameworkToBeRenamed + super_class: #x // java/lang/Object + interfaces: 0, fields: 1, methods: 2, attributes: 3 + private final int mValue; + descriptor: I + flags: (0x0012) ACC_PRIVATE, ACC_FINAL + RuntimeVisibleAnnotations: + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInStub + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInImpl + + public rename_prefix.com.android.hoststubgen.test.tinyframework.TinyFrameworkToBeRenamed(int); + descriptor: (I)V + flags: (0x0001) ACC_PUBLIC + Code: + stack=2, locals=2, args_size=2 + x: aload_0 + x: invokespecial #x // Method java/lang/Object."<init>":()V + x: aload_0 + x: iload_1 + x: putfield #x // Field mValue:I + x: return + LineNumberTable: + LocalVariableTable: + Start Length Slot Name Signature + 0 10 0 this Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkToBeRenamed; + 0 10 1 value I + RuntimeVisibleAnnotations: + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInStub + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInImpl + + public int getValue(); + descriptor: ()I + flags: (0x0001) ACC_PUBLIC + Code: + stack=1, locals=1, args_size=1 + x: aload_0 + x: getfield #x // Field mValue:I + x: ireturn + LineNumberTable: + LocalVariableTable: + Start Length Slot Name Signature + 0 5 0 this Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkToBeRenamed; + RuntimeVisibleAnnotations: + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInStub + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInImpl +} +SourceFile: "TinyFrameworkToBeRenamed.java" +RuntimeVisibleAnnotations: + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInStub + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInImpl +RuntimeInvisibleAnnotations: + x: #x() + android.hosttest.annotation.HostSideTestWholeClassStub diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/12-hoststubgen-test-tiny-framework-host-ext-stub-dump.txt b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/12-hoststubgen-test-tiny-framework-host-ext-stub-dump.txt index 0bbb4182859b..17ba48c67d98 100644 --- a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/12-hoststubgen-test-tiny-framework-host-ext-stub-dump.txt +++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/12-hoststubgen-test-tiny-framework-host-ext-stub-dump.txt @@ -2177,6 +2177,56 @@ RuntimeVisibleAnnotations: RuntimeInvisibleAnnotations: x: #x() android.hosttest.annotation.HostSideTestWholeClassStub +## Class: com/android/hoststubgen/test/tinyframework/TinyFrameworkRenamedClassCaller.class + Compiled from "TinyFrameworkRenamedClassCaller.java" +public class com.android.hoststubgen.test.tinyframework.TinyFrameworkRenamedClassCaller + minor version: 0 + major version: 61 + flags: (0x0021) ACC_PUBLIC, ACC_SUPER + this_class: #x // com/android/hoststubgen/test/tinyframework/TinyFrameworkRenamedClassCaller + super_class: #x // java/lang/Object + interfaces: 0, fields: 0, methods: 2, attributes: 3 + public com.android.hoststubgen.test.tinyframework.TinyFrameworkRenamedClassCaller(); + descriptor: ()V + flags: (0x0001) ACC_PUBLIC + Code: + stack=3, locals=1, args_size=1 + x: new #x // class java/lang/RuntimeException + x: dup + x: ldc #x // String Stub! + x: invokespecial #x // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V + x: athrow + RuntimeVisibleAnnotations: + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInStub + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInImpl + + public static int foo(int); + descriptor: (I)I + flags: (0x0009) ACC_PUBLIC, ACC_STATIC + Code: + stack=3, locals=1, args_size=1 + x: new #x // class java/lang/RuntimeException + x: dup + x: ldc #x // String Stub! + x: invokespecial #x // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V + x: athrow + RuntimeVisibleAnnotations: + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInStub + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInImpl +} +SourceFile: "TinyFrameworkRenamedClassCaller.java" +RuntimeVisibleAnnotations: + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInStub + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInImpl +RuntimeInvisibleAnnotations: + x: #x() + android.hosttest.annotation.HostSideTestWholeClassStub ## Class: com/android/hoststubgen/test/tinyframework/packagetest/A.class Compiled from "A.java" public class com.android.hoststubgen.test.tinyframework.packagetest.A @@ -2419,3 +2469,62 @@ RuntimeVisibleAnnotations: RuntimeInvisibleAnnotations: x: #x() android.hosttest.annotation.HostSideTestWholeClassStub +## Class: rename_prefix/com/android/hoststubgen/test/tinyframework/TinyFrameworkToBeRenamed.class + Compiled from "TinyFrameworkToBeRenamed.java" +public class rename_prefix.com.android.hoststubgen.test.tinyframework.TinyFrameworkToBeRenamed + minor version: 0 + major version: 61 + flags: (0x0021) ACC_PUBLIC, ACC_SUPER + this_class: #x // rename_prefix/com/android/hoststubgen/test/tinyframework/TinyFrameworkToBeRenamed + super_class: #x // java/lang/Object + interfaces: 0, fields: 1, methods: 2, attributes: 3 + private final int mValue; + descriptor: I + flags: (0x0012) ACC_PRIVATE, ACC_FINAL + RuntimeVisibleAnnotations: + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInStub + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInImpl + + public rename_prefix.com.android.hoststubgen.test.tinyframework.TinyFrameworkToBeRenamed(int); + descriptor: (I)V + flags: (0x0001) ACC_PUBLIC + Code: + stack=3, locals=2, args_size=2 + x: new #x // class java/lang/RuntimeException + x: dup + x: ldc #x // String Stub! + x: invokespecial #x // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V + x: athrow + RuntimeVisibleAnnotations: + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInStub + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInImpl + + public int getValue(); + descriptor: ()I + flags: (0x0001) ACC_PUBLIC + Code: + stack=3, locals=1, args_size=1 + x: new #x // class java/lang/RuntimeException + x: dup + x: ldc #x // String Stub! + x: invokespecial #x // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V + x: athrow + RuntimeVisibleAnnotations: + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInStub + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInImpl +} +SourceFile: "TinyFrameworkToBeRenamed.java" +RuntimeVisibleAnnotations: + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInStub + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInImpl +RuntimeInvisibleAnnotations: + x: #x() + android.hosttest.annotation.HostSideTestWholeClassStub diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/13-hoststubgen-test-tiny-framework-host-ext-impl-dump.txt b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/13-hoststubgen-test-tiny-framework-host-ext-impl-dump.txt index 91104dea3f7b..3beea643823a 100644 --- a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/13-hoststubgen-test-tiny-framework-host-ext-impl-dump.txt +++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/13-hoststubgen-test-tiny-framework-host-ext-impl-dump.txt @@ -4408,6 +4408,83 @@ RuntimeVisibleAnnotations: RuntimeInvisibleAnnotations: x: #x() android.hosttest.annotation.HostSideTestWholeClassStub +## Class: com/android/hoststubgen/test/tinyframework/TinyFrameworkRenamedClassCaller.class + Compiled from "TinyFrameworkRenamedClassCaller.java" +public class com.android.hoststubgen.test.tinyframework.TinyFrameworkRenamedClassCaller + minor version: 0 + major version: 61 + flags: (0x0021) ACC_PUBLIC, ACC_SUPER + this_class: #x // com/android/hoststubgen/test/tinyframework/TinyFrameworkRenamedClassCaller + super_class: #x // java/lang/Object + interfaces: 0, fields: 0, methods: 3, attributes: 3 + private static {}; + descriptor: ()V + flags: (0x000a) ACC_PRIVATE, ACC_STATIC + Code: + stack=2, locals=0, args_size=0 + x: ldc #x // class com/android/hoststubgen/test/tinyframework/TinyFrameworkRenamedClassCaller + x: ldc #x // String com.android.hoststubgen.hosthelper.HostTestUtils.logClassLoaded + x: invokestatic #x // Method com/android/hoststubgen/hosthelper/HostTestUtils.onClassLoaded:(Ljava/lang/Class;Ljava/lang/String;)V + x: return + + public com.android.hoststubgen.test.tinyframework.TinyFrameworkRenamedClassCaller(); + descriptor: ()V + flags: (0x0001) ACC_PUBLIC + Code: + stack=4, locals=1, args_size=1 + x: ldc #x // class com/android/hoststubgen/test/tinyframework/TinyFrameworkRenamedClassCaller + x: ldc #x // String <init> + x: ldc #x // String ()V + x: ldc #x // String com.android.hoststubgen.hosthelper.HostTestUtils.logMethodCall + x: invokestatic #x // Method com/android/hoststubgen/hosthelper/HostTestUtils.callMethodCallHook:(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + x: aload_0 + x: invokespecial #x // Method java/lang/Object."<init>":()V + x: return + LineNumberTable: + LocalVariableTable: + Start Length Slot Name Signature + 11 5 0 this Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkRenamedClassCaller; + RuntimeVisibleAnnotations: + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInStub + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInImpl + + public static int foo(int); + descriptor: (I)I + flags: (0x0009) ACC_PUBLIC, ACC_STATIC + Code: + stack=4, locals=1, args_size=1 + x: ldc #x // class com/android/hoststubgen/test/tinyframework/TinyFrameworkRenamedClassCaller + x: ldc #x // String foo + x: ldc #x // String (I)I + x: ldc #x // String com.android.hoststubgen.hosthelper.HostTestUtils.logMethodCall + x: invokestatic #x // Method com/android/hoststubgen/hosthelper/HostTestUtils.callMethodCallHook:(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + x: new #x // class rename_prefix/com/android/hoststubgen/test/tinyframework/TinyFrameworkToBeRenamed + x: dup + x: iload_0 + x: invokespecial #x // Method rename_prefix/com/android/hoststubgen/test/tinyframework/TinyFrameworkToBeRenamed."<init>":(I)V + x: invokevirtual #x // Method rename_prefix/com/android/hoststubgen/test/tinyframework/TinyFrameworkToBeRenamed.getValue:()I + x: ireturn + LineNumberTable: + LocalVariableTable: + Start Length Slot Name Signature + 11 12 0 value I + RuntimeVisibleAnnotations: + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInStub + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInImpl +} +SourceFile: "TinyFrameworkRenamedClassCaller.java" +RuntimeVisibleAnnotations: + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInStub + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInImpl +RuntimeInvisibleAnnotations: + x: #x() + android.hosttest.annotation.HostSideTestWholeClassStub ## Class: com/android/hoststubgen/test/tinyframework/packagetest/A.class Compiled from "A.java" public class com.android.hoststubgen.test.tinyframework.packagetest.A @@ -5041,3 +5118,90 @@ RuntimeVisibleAnnotations: RuntimeInvisibleAnnotations: x: #x() android.hosttest.annotation.HostSideTestWholeClassStub +## Class: rename_prefix/com/android/hoststubgen/test/tinyframework/TinyFrameworkToBeRenamed.class + Compiled from "TinyFrameworkToBeRenamed.java" +public class rename_prefix.com.android.hoststubgen.test.tinyframework.TinyFrameworkToBeRenamed + minor version: 0 + major version: 61 + flags: (0x0021) ACC_PUBLIC, ACC_SUPER + this_class: #x // rename_prefix/com/android/hoststubgen/test/tinyframework/TinyFrameworkToBeRenamed + super_class: #x // java/lang/Object + interfaces: 0, fields: 1, methods: 3, attributes: 3 + private final int mValue; + descriptor: I + flags: (0x0012) ACC_PRIVATE, ACC_FINAL + RuntimeVisibleAnnotations: + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInStub + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInImpl + + private static {}; + descriptor: ()V + flags: (0x000a) ACC_PRIVATE, ACC_STATIC + Code: + stack=2, locals=0, args_size=0 + x: ldc #x // class com/android/hoststubgen/test/tinyframework/TinyFrameworkToBeRenamed + x: ldc #x // String com.android.hoststubgen.hosthelper.HostTestUtils.logClassLoaded + x: invokestatic #x // Method com/android/hoststubgen/hosthelper/HostTestUtils.onClassLoaded:(Ljava/lang/Class;Ljava/lang/String;)V + x: return + + public rename_prefix.com.android.hoststubgen.test.tinyframework.TinyFrameworkToBeRenamed(int); + descriptor: (I)V + flags: (0x0001) ACC_PUBLIC + Code: + stack=4, locals=2, args_size=2 + x: ldc #x // class com/android/hoststubgen/test/tinyframework/TinyFrameworkToBeRenamed + x: ldc #x // String <init> + x: ldc #x // String (I)V + x: ldc #x // String com.android.hoststubgen.hosthelper.HostTestUtils.logMethodCall + x: invokestatic #x // Method com/android/hoststubgen/hosthelper/HostTestUtils.callMethodCallHook:(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + x: aload_0 + x: invokespecial #x // Method java/lang/Object."<init>":()V + x: aload_0 + x: iload_1 + x: putfield #x // Field mValue:I + x: return + LineNumberTable: + LocalVariableTable: + Start Length Slot Name Signature + 11 10 0 this Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkToBeRenamed; + 11 10 1 value I + RuntimeVisibleAnnotations: + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInStub + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInImpl + + public int getValue(); + descriptor: ()I + flags: (0x0001) ACC_PUBLIC + Code: + stack=4, locals=1, args_size=1 + x: ldc #x // class com/android/hoststubgen/test/tinyframework/TinyFrameworkToBeRenamed + x: ldc #x // String getValue + x: ldc #x // String ()I + x: ldc #x // String com.android.hoststubgen.hosthelper.HostTestUtils.logMethodCall + x: invokestatic #x // Method com/android/hoststubgen/hosthelper/HostTestUtils.callMethodCallHook:(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + x: aload_0 + x: getfield #x // Field mValue:I + x: ireturn + LineNumberTable: + LocalVariableTable: + Start Length Slot Name Signature + 11 5 0 this Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkToBeRenamed; + RuntimeVisibleAnnotations: + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInStub + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInImpl +} +SourceFile: "TinyFrameworkToBeRenamed.java" +RuntimeVisibleAnnotations: + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInStub + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInImpl +RuntimeInvisibleAnnotations: + x: #x() + android.hosttest.annotation.HostSideTestWholeClassStub diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/policy-override-tiny-framework.txt b/tools/hoststubgen/hoststubgen/test-tiny-framework/policy-override-tiny-framework.txt index 530de431828e..75c9721020bd 100644 --- a/tools/hoststubgen/hoststubgen/test-tiny-framework/policy-override-tiny-framework.txt +++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/policy-override-tiny-framework.txt @@ -48,3 +48,9 @@ class com.android.hoststubgen.test.tinyframework.packagetest.sub.B remove # The following rules are the same as above # class com.android.hoststubgen.test.tinyframework.packagetest.A stub # class com.android.hoststubgen.test.tinyframework.packagetest.sub.A stub + + +# "rename" takes a type internal name, so '/'s is used as a separator. +# The leading / in the prefix is not needed (it'll be stripped), but it's added to make +# sure the stripping works. +rename ^.*/TinyFrameworkToBeRenamed$ /rename_prefix/
\ No newline at end of file diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkRenamedClassCaller.java b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkRenamedClassCaller.java new file mode 100644 index 000000000000..31a164af03f5 --- /dev/null +++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkRenamedClassCaller.java @@ -0,0 +1,28 @@ +/* + * 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.hoststubgen.test.tinyframework; + +import android.hosttest.annotation.HostSideTestWholeClassStub; + +@HostSideTestWholeClassStub +public class TinyFrameworkRenamedClassCaller { + /** Calls the class that'll be renamed. */ + public static int foo(int value) { + // When TinyFrameworkToBeRenamed gets renamed, this callsite should be updated too, + // so this code should work as-is. + return new TinyFrameworkToBeRenamed(value).getValue(); + } +} diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkToBeRenamed.java b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkToBeRenamed.java new file mode 100644 index 000000000000..1430bcb0276b --- /dev/null +++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkToBeRenamed.java @@ -0,0 +1,34 @@ +/* + * 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.hoststubgen.test.tinyframework; + +import android.hosttest.annotation.HostSideTestWholeClassStub; + +/** + * This class will be renamed by the "rename" directive in the policy file. + */ +@HostSideTestWholeClassStub +public class TinyFrameworkToBeRenamed { + private final int mValue; + + public TinyFrameworkToBeRenamed(int value) { + mValue = value; + } + + public int getValue() { + return mValue; + } +} diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassTest.java b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassTest.java index 37925e82bdb6..bf0f6545d1f5 100644 --- a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassTest.java +++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassTest.java @@ -334,4 +334,9 @@ public class TinyFrameworkClassTest { public void testRFileHeuristics() { assertThat(Nested.ARRAY.length).isEqualTo(1); } + + @Test + public void testTypeRename() { + assertThat(TinyFrameworkRenamedClassCaller.foo(1)).isEqualTo(1); + } } diff --git a/tools/hoststubgen/scripts/dump-jar b/tools/hoststubgen/scripts/dump-jar index 992665ed58ee..fe546fe9cc92 100755 --- a/tools/hoststubgen/scripts/dump-jar +++ b/tools/hoststubgen/scripts/dump-jar @@ -97,6 +97,7 @@ filter_output() { # - Remove the constant pool # - Remove the line number table # - Some other transient lines + # - Sometimes the javap shows mysterious warnings, so remove them too. # # `/PATTERN-1/,/PATTERN-1/{//!d}` is a trick to delete lines between two patterns, without # the start and the end lines. @@ -106,7 +107,8 @@ filter_output() { -e '/^ *line *[0-9][0-9]*: *[0-9][0-9]*$/d' \ -e '/SHA-256 checksum/d' \ -e '/Last modified/d' \ - -e '/^Classfile jar/d' + -e '/^Classfile jar/d' \ + -e '/\[warning\]/d' else cat # Print as-is. fi diff --git a/tools/protologtool/src/com/android/protolog/tool/ViewerConfigProtoBuilder.kt b/tools/protologtool/src/com/android/protolog/tool/ViewerConfigProtoBuilder.kt index cf0876a1f072..0115339a68b7 100644 --- a/tools/protologtool/src/com/android/protolog/tool/ViewerConfigProtoBuilder.kt +++ b/tools/protologtool/src/com/android/protolog/tool/ViewerConfigProtoBuilder.kt @@ -55,6 +55,7 @@ class ViewerConfigProtoBuilder : ProtoLogTool.ProtologViewerConfigBuilder { .setLevel( ProtoLogLevel.forNumber(log.logLevel.ordinal + 1)) .setGroupId(groupId) + .setLocation(log.position) ) } 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/wifi/java/src/android/net/wifi/WifiKeystore.java b/wifi/java/src/android/net/wifi/WifiKeystore.java index 2ba7468a8c9c..59f14a94b514 100644 --- a/wifi/java/src/android/net/wifi/WifiKeystore.java +++ b/wifi/java/src/android/net/wifi/WifiKeystore.java @@ -36,6 +36,8 @@ import java.util.Set; @SuppressLint("UnflaggedApi") // Promoting from @SystemApi(MODULE_LIBRARIES) public final class WifiKeystore { private static final String TAG = "WifiKeystore"; + private static final String sPrimaryDbName = + WifiBlobStore.supplicantCanAccessBlobstore() ? "WifiBlobstore" : "LegacyKeystore"; /** @hide */ WifiKeystore() { @@ -57,8 +59,13 @@ public final class WifiKeystore { // are able to access the same values. final long identity = Binder.clearCallingIdentity(); try { - Log.i(TAG, "put blob. alias " + alias); - return WifiBlobStore.getInstance().put(alias, blob); + Log.i(TAG, "put blob. alias=" + alias + ", primaryDb=" + sPrimaryDbName); + if (WifiBlobStore.supplicantCanAccessBlobstore()) { + return WifiBlobStore.getInstance().put(alias, blob); + } else { + WifiBlobStore.getLegacyKeystore().put(alias, Process.WIFI_UID, blob); + return true; + } } catch (Exception e) { Log.e(TAG, "Failed to put blob.", e); return false; @@ -80,7 +87,7 @@ public final class WifiKeystore { public static @NonNull byte[] get(@NonNull String alias) { final long identity = Binder.clearCallingIdentity(); try { - Log.i(TAG, "get blob. alias " + alias); + Log.i(TAG, "get blob. alias=" + alias + ", primaryDb=" + sPrimaryDbName); byte[] blob = WifiBlobStore.getInstance().get(alias); if (blob != null) { return blob; @@ -112,7 +119,7 @@ public final class WifiKeystore { boolean legacyKsSuccess = false; final long identity = Binder.clearCallingIdentity(); try { - Log.i(TAG, "remove blob. alias " + alias); + Log.i(TAG, "remove blob. alias=" + alias + ", primaryDb=" + sPrimaryDbName); blobStoreSuccess = WifiBlobStore.getInstance().remove(alias); // Legacy Keystore will throw an exception if the alias is not found. WifiBlobStore.getLegacyKeystore().remove(alias, Process.WIFI_UID); diff --git a/wifi/java/src/android/net/wifi/WifiMigration.java b/wifi/java/src/android/net/wifi/WifiMigration.java index 6ea20ecdac6e..7df1d4b47204 100644 --- a/wifi/java/src/android/net/wifi/WifiMigration.java +++ b/wifi/java/src/android/net/wifi/WifiMigration.java @@ -577,6 +577,10 @@ public final class WifiMigration { @FlaggedApi(Flags.FLAG_LEGACY_KEYSTORE_TO_WIFI_BLOBSTORE_MIGRATION_READ_ONLY) @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) public static void migrateLegacyKeystoreToWifiBlobstore() { + if (!WifiBlobStore.supplicantCanAccessBlobstore()) { + Log.i(TAG, "Avoiding migration since supplicant cannot access WifiBlobstore"); + return; + } final long identity = Binder.clearCallingIdentity(); try { ILegacyKeystore legacyKeystore = WifiBlobStore.getLegacyKeystore(); diff --git a/wifi/tests/src/android/net/wifi/WifiKeystoreTest.java b/wifi/tests/src/android/net/wifi/WifiKeystoreTest.java index c28a0ae00f69..4b1dc41f1426 100644 --- a/wifi/tests/src/android/net/wifi/WifiKeystoreTest.java +++ b/wifi/tests/src/android/net/wifi/WifiKeystoreTest.java @@ -61,6 +61,7 @@ public class WifiKeystoreTest { mSession = ExtendedMockito.mockitoSession() .mockStatic(WifiBlobStore.class, withSettings().lenient()) .startMocking(); + when(WifiBlobStore.supplicantCanAccessBlobstore()).thenReturn(true); when(WifiBlobStore.getLegacyKeystore()).thenReturn(mLegacyKeystore); when(WifiBlobStore.getInstance()).thenReturn(mWifiBlobStore); } @@ -74,16 +75,30 @@ public class WifiKeystoreTest { } /** - * Test that put() only writes to the WifiBlobStore database. + * Test that put() writes to the WifiBlobStore database when it + * is available to supplicant. */ @Test - public void testPut() throws Exception { + public void testPut_wifiBlobstore() throws Exception { + when(WifiBlobStore.supplicantCanAccessBlobstore()).thenReturn(true); WifiKeystore.put(TEST_ALIAS, TEST_VALUE); verify(mWifiBlobStore).put(anyString(), any()); verify(mLegacyKeystore, never()).put(anyString(), anyInt(), any()); } /** + * Test that put() writes to Legacy Keystore if the WifiBlobstore database + * is not available to supplicant. + */ + @Test + public void testPut_legacyKeystore() throws Exception { + when(WifiBlobStore.supplicantCanAccessBlobstore()).thenReturn(false); + WifiKeystore.put(TEST_ALIAS, TEST_VALUE); + verify(mLegacyKeystore).put(anyString(), anyInt(), any()); + verify(mWifiBlobStore, never()).put(anyString(), any()); + } + + /** * Test that if the alias is found in the WifiBlobStore database, * then the legacy database is not searched. */ diff --git a/wifi/tests/src/android/net/wifi/WifiMigrationTest.java b/wifi/tests/src/android/net/wifi/WifiMigrationTest.java index 8a5912f0ffdf..d95069d46879 100644 --- a/wifi/tests/src/android/net/wifi/WifiMigrationTest.java +++ b/wifi/tests/src/android/net/wifi/WifiMigrationTest.java @@ -56,6 +56,7 @@ public class WifiMigrationTest { mSession = ExtendedMockito.mockitoSession() .mockStatic(WifiBlobStore.class, withSettings().lenient()) .startMocking(); + when(WifiBlobStore.supplicantCanAccessBlobstore()).thenReturn(true); when(WifiBlobStore.getLegacyKeystore()).thenReturn(mLegacyKeystore); when(WifiBlobStore.getInstance()).thenReturn(mWifiBlobStore); when(mLegacyKeystore.get(anyString(), anyInt())).thenReturn(TEST_VALUE); @@ -70,6 +71,17 @@ public class WifiMigrationTest { } /** + * Verify that the Keystore migration is skipped if supplicant does not have + * access to the WifiBlobstore database. + */ + @Test + public void testKeystoreMigrationAvoidedOnLegacyVendorPartition() { + when(WifiBlobStore.supplicantCanAccessBlobstore()).thenReturn(false); + WifiMigration.migrateLegacyKeystoreToWifiBlobstore(); + verifyNoMoreInteractions(mLegacyKeystore, mWifiBlobStore); + } + + /** * Verify that the Keystore migration method returns immediately if no aliases * are found in Legacy Keystore. */ |