diff options
729 files changed, 21621 insertions, 10408 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 cebeb18b84bb..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"], 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/core/src/android/text/VariableFontPerfTest.java b/apct-tests/perftests/core/src/android/text/VariableFontPerfTest.java index fbe67a477f5d..c34936f930f9 100644 --- a/apct-tests/perftests/core/src/android/text/VariableFontPerfTest.java +++ b/apct-tests/perftests/core/src/android/text/VariableFontPerfTest.java @@ -19,6 +19,7 @@ package android.text; import android.graphics.Paint; import android.graphics.RecordingCanvas; import android.graphics.RenderNode; +import android.graphics.Typeface; import android.perftests.utils.BenchmarkState; import android.perftests.utils.PerfStatusReporter; @@ -120,13 +121,34 @@ public class VariableFontPerfTest { public void testSetFontVariationSettings() { final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); final Paint paint = new Paint(PAINT); - final Random random = new Random(0); while (state.keepRunning()) { state.pauseTiming(); - int weight = random.nextInt(1000); + paint.setTypeface(null); + paint.setFontVariationSettings(null); + Typeface.clearTypefaceCachesForTestingPurpose(); state.resumeTiming(); - paint.setFontVariationSettings("'wght' " + weight); + paint.setFontVariationSettings("'wght' 450"); + } + Typeface.clearTypefaceCachesForTestingPurpose(); + } + + @Test + public void testSetFontVariationSettings_Cached() { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + final Paint paint = new Paint(PAINT); + Typeface.clearTypefaceCachesForTestingPurpose(); + + while (state.keepRunning()) { + state.pauseTiming(); + paint.setTypeface(null); + paint.setFontVariationSettings(null); + state.resumeTiming(); + + paint.setFontVariationSettings("'wght' 450"); } + + Typeface.clearTypefaceCachesForTestingPurpose(); } + } diff --git a/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/config/preloaded-classes-denylist b/config/preloaded-classes-denylist index dd2569a240d0..a413bbd68f60 100644 --- a/config/preloaded-classes-denylist +++ b/config/preloaded-classes-denylist @@ -1,23 +1,13 @@ android.content.AsyncTaskLoader$LoadTask -android.media.MediaCodecInfo$CodecCapabilities$FeatureList -android.media.MediaCodecInfo$LazyHolder android.net.ConnectivityThread$Singleton -android.net.rtp.AudioGroup -android.net.rtp.AudioStream -android.net.rtp.RtpStream android.os.FileObserver android.os.NullVibrator -android.permission.PermissionManager -android.provider.MediaStore android.speech.tts.TextToSpeech$Connection$SetupConnectionAsyncTask -android.view.HdrRenderState -android.text.TextFlags android.widget.Magnifier -com.android.internal.jank.InteractionJankMonitor$InstanceHolder -com.android.internal.os.BinderCallsStats$SettingsObserver -com.android.internal.util.LatencyTracker$SLatencyTrackerHolder -com.android.server.BootReceiver$2 gov.nist.core.net.DefaultNetworkLayer -java.util.ImmutableCollections +android.net.rtp.AudioGroup +android.net.rtp.AudioStream +android.net.rtp.RtpStream java.util.concurrent.ThreadLocalRandom -sun.nio.fs.UnixChannelFactory +java.util.ImmutableCollections +com.android.internal.jank.InteractionJankMonitor$InstanceHolder diff --git a/core/api/current.txt b/core/api/current.txt index 354e26b2eb02..ea039a7103a3 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -59034,7 +59034,7 @@ package android.widget { } public static interface CompoundButton.OnCheckedChangeListener { - method public void onCheckedChanged(android.widget.CompoundButton, boolean); + method public void onCheckedChanged(@NonNull android.widget.CompoundButton, boolean); } public abstract class CursorAdapter extends android.widget.BaseAdapter implements android.widget.Filterable android.widget.ThemedSpinnerAdapter { @@ -60099,7 +60099,7 @@ package android.widget { } public static interface RadioGroup.OnCheckedChangeListener { - method public void onCheckedChanged(android.widget.RadioGroup, @IdRes int); + method public void onCheckedChanged(@NonNull android.widget.RadioGroup, @IdRes int); } public class RatingBar extends android.widget.AbsSeekBar { diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 15e5706db9d1..445a57220757 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -11981,6 +11981,7 @@ package android.provider { field public static final String ACTION_MANAGE_APP_OVERLAY_PERMISSION = "android.settings.MANAGE_APP_OVERLAY_PERMISSION"; field public static final String ACTION_MANAGE_DOMAIN_URLS = "android.settings.MANAGE_DOMAIN_URLS"; field public static final String ACTION_MANAGE_MORE_DEFAULT_APPS_SETTINGS = "android.settings.MANAGE_MORE_DEFAULT_APPS_SETTINGS"; + field @FlaggedApi("android.nfc.nfc_action_manage_services_settings") public static final String ACTION_MANAGE_OTHER_NFC_SERVICES_SETTINGS = "android.settings.MANAGE_OTHER_NFC_SERVICES_SETTINGS"; field public static final String ACTION_NOTIFICATION_POLICY_ACCESS_DETAIL_SETTINGS = "android.settings.NOTIFICATION_POLICY_ACCESS_DETAIL_SETTINGS"; field public static final String ACTION_REQUEST_ENABLE_CONTENT_CAPTURE = "android.settings.REQUEST_ENABLE_CONTENT_CAPTURE"; field public static final String ACTION_SHOW_ADMIN_SUPPORT_DETAILS = "android.settings.SHOW_ADMIN_SUPPORT_DETAILS"; diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index 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/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/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/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 06c516aee8f3..28f2c2530ae9 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -4824,6 +4824,7 @@ public class UserManager { * <p>Note that this does not alter the user's pre-existing user restrictions. * * @param userId the id of the user to become admin + * @throws SecurityException if changing ADMIN status of the user is not allowed * @hide */ @RequiresPermission(allOf = { @@ -4844,6 +4845,7 @@ public class UserManager { * <p>Note that this does not alter the user's pre-existing user restrictions. * * @param userId the id of the user to revoke admin rights from + * @throws SecurityException if changing ADMIN status of the user is not allowed * @hide */ @RequiresPermission(allOf = { diff --git a/core/java/android/os/VibrationEffect.java b/core/java/android/os/VibrationEffect.java index f3ef9e15b8f0..e68b74683292 100644 --- a/core/java/android/os/VibrationEffect.java +++ b/core/java/android/os/VibrationEffect.java @@ -663,6 +663,15 @@ public abstract class VibrationEffect implements Parcelable { * @hide */ public static float scale(float intensity, float scaleFactor) { + if (Flags.hapticsScaleV2Enabled()) { + if (Float.compare(scaleFactor, 1) <= 0 || Float.compare(intensity, 0) == 0) { + // Scaling down or scaling zero intensity is straightforward. + return scaleFactor * intensity; + } + // Using S * x / (1 + (S - 1) * x^2) as the scale up function to converge to 1.0. + return (scaleFactor * intensity) / (1 + (scaleFactor - 1) * intensity * intensity); + } + // Applying gamma correction to the scale factor, which is the same as encoding the input // value, scaling it, then decoding the scaled value. float scale = MathUtils.pow(scaleFactor, 1f / SCALE_GAMMA); diff --git a/core/java/android/os/Vibrator.java b/core/java/android/os/Vibrator.java index 161cce0293e7..36233b7be2bb 100644 --- a/core/java/android/os/Vibrator.java +++ b/core/java/android/os/Vibrator.java @@ -553,6 +553,31 @@ public abstract class Vibrator { } /** + * Performs a haptic feedback. Similar to {@link #performHapticFeedback} but also take into the + * consideration the {@link InputDevice} that triggered the haptic + * + * <p>A haptic feedback is a short vibration feedback. The type of feedback is identified via + * the {@code constant}, which should be one of the effect constants provided in + * {@link HapticFeedbackConstants}. The haptic feedback provided for a given effect ID is + * consistent across all usages on the same device. + * + * @param constant the ID for the haptic feedback. This should be one of the constants + * defined in {@link HapticFeedbackConstants}. + * @param inputDeviceId the integer id of the input device that triggered the haptic feedback. + * @param inputSource the {@link InputDevice.Source} that triggered the haptic feedback. + * @param reason the reason for this haptic feedback. + * @param flags Additional flags as per {@link HapticFeedbackConstants}. + * @param privFlags Additional private flags as per {@link HapticFeedbackConstants}. + * @hide + */ + public void performHapticFeedbackForInputDevice( + int constant, int inputDeviceId, int inputSource, String reason, + @HapticFeedbackConstants.Flags int flags, + @HapticFeedbackConstants.PrivateFlags int privFlags) { + Log.w(TAG, "performHapticFeedbackForInputDevice is not supported"); + } + + /** * Query whether the vibrator natively supports the given effects. * * <p>If an effect is not supported, the system may still automatically fall back to playing diff --git a/core/java/android/os/VibratorManager.java b/core/java/android/os/VibratorManager.java index 2c7a852cf29f..0428876891f9 100644 --- a/core/java/android/os/VibratorManager.java +++ b/core/java/android/os/VibratorManager.java @@ -155,6 +155,27 @@ public abstract class VibratorManager { } /** + * Performs a haptic feedback. Similar to {@link #performHapticFeedback} but also take input + * into consideration. + * + * @param constant the ID of the requested haptic feedback. Should be one of the constants + * defined in {@link HapticFeedbackConstants}. + * @param inputDeviceId the integer id of the input device that customizes the haptic feedback + * corresponding to the {@code constant}. + * @param inputSource the {@link InputDevice.Source} that customizes the haptic feedback + * corresponding to the {@code constant}. + * @param reason the reason for this haptic feedback. + * @param flags Additional flags as per {@link HapticFeedbackConstants}. + * @param privFlags Additional private flags as per {@link HapticFeedbackConstants}. + * @hide + */ + public void performHapticFeedbackForInputDevice(int constant, int inputDeviceId, + int inputSource, String reason, @HapticFeedbackConstants.Flags int flags, + @HapticFeedbackConstants.PrivateFlags int privFlags) { + Log.w(TAG, "performHapticFeedbackForInputDevice is not supported"); + } + + /** * Turn all the vibrators off. */ @RequiresPermission(android.Manifest.permission.VIBRATE) diff --git a/core/java/android/os/vibrator/VibrationConfig.java b/core/java/android/os/vibrator/VibrationConfig.java index a4164e9f204c..e6e5a27bd731 100644 --- a/core/java/android/os/vibrator/VibrationConfig.java +++ b/core/java/android/os/vibrator/VibrationConfig.java @@ -49,8 +49,22 @@ import java.util.Arrays; */ public class VibrationConfig { + /** + * Hardcoded default scale level gain to be applied between each scale level to define their + * scale factor value. + * + * <p>Default gain defined as 3 dBs. + */ + private static final float DEFAULT_SCALE_LEVEL_GAIN = 1.4f; + + /** + * Hardcoded default amplitude to be used when device config is invalid, i.e. not in [1,255]. + */ + private static final int DEFAULT_AMPLITUDE = 255; + // TODO(b/191150049): move these to vibrator static config file private final float mHapticChannelMaxVibrationAmplitude; + private final int mDefaultVibrationAmplitude; private final int mRampStepDurationMs; private final int mRampDownDurationMs; private final int mRequestVibrationParamsTimeoutMs; @@ -75,8 +89,10 @@ public class VibrationConfig { /** @hide */ public VibrationConfig(@Nullable Resources resources) { + mDefaultVibrationAmplitude = resources.getInteger( + com.android.internal.R.integer.config_defaultVibrationAmplitude); mHapticChannelMaxVibrationAmplitude = loadFloat(resources, - com.android.internal.R.dimen.config_hapticChannelMaxVibrationAmplitude, 0); + com.android.internal.R.dimen.config_hapticChannelMaxVibrationAmplitude); mRampDownDurationMs = loadInteger(resources, com.android.internal.R.integer.config_vibrationWaveformRampDownDuration, 0); mRampStepDurationMs = loadInteger(resources, @@ -87,9 +103,9 @@ public class VibrationConfig { com.android.internal.R.array.config_requestVibrationParamsForUsages); mIgnoreVibrationsOnWirelessCharger = loadBoolean(resources, - com.android.internal.R.bool.config_ignoreVibrationsOnWirelessCharger, false); + com.android.internal.R.bool.config_ignoreVibrationsOnWirelessCharger); mKeyboardVibrationSettingsSupported = loadBoolean(resources, - com.android.internal.R.bool.config_keyboardVibrationSettingsSupported, false); + com.android.internal.R.bool.config_keyboardVibrationSettingsSupported); mDefaultAlarmVibrationIntensity = loadDefaultIntensity(resources, com.android.internal.R.integer.config_defaultAlarmVibrationIntensity); @@ -115,16 +131,16 @@ public class VibrationConfig { return value; } - private static float loadFloat(@Nullable Resources res, int resId, float defaultValue) { - return res != null ? res.getFloat(resId) : defaultValue; + private static float loadFloat(@Nullable Resources res, int resId) { + return res != null ? res.getFloat(resId) : 0f; } private static int loadInteger(@Nullable Resources res, int resId, int defaultValue) { return res != null ? res.getInteger(resId) : defaultValue; } - private static boolean loadBoolean(@Nullable Resources res, int resId, boolean defaultValue) { - return res != null ? res.getBoolean(resId) : defaultValue; + private static boolean loadBoolean(@Nullable Resources res, int resId) { + return res != null && res.getBoolean(resId); } private static int[] loadIntArray(@Nullable Resources res, int resId) { @@ -145,6 +161,26 @@ public class VibrationConfig { } /** + * Return the device default vibration amplitude value to replace the + * {@link android.os.VibrationEffect#DEFAULT_AMPLITUDE} constant. + */ + public int getDefaultVibrationAmplitude() { + if (mDefaultVibrationAmplitude < 1 || mDefaultVibrationAmplitude > 255) { + return DEFAULT_AMPLITUDE; + } + return mDefaultVibrationAmplitude; + } + + /** + * Return the device default gain to be applied between scale levels to define the scale factor + * for each level. + */ + public float getDefaultVibrationScaleLevelGain() { + // TODO(b/356407380): add device config for this + return DEFAULT_SCALE_LEVEL_GAIN; + } + + /** * The duration, in milliseconds, that should be applied to the ramp to turn off the vibrator * when a vibration is cancelled or finished at non-zero amplitude. */ @@ -233,6 +269,7 @@ public class VibrationConfig { public String toString() { return "VibrationConfig{" + "mIgnoreVibrationsOnWirelessCharger=" + mIgnoreVibrationsOnWirelessCharger + + ", mDefaultVibrationAmplitude=" + mDefaultVibrationAmplitude + ", mHapticChannelMaxVibrationAmplitude=" + mHapticChannelMaxVibrationAmplitude + ", mRampStepDurationMs=" + mRampStepDurationMs + ", mRampDownDurationMs=" + mRampDownDurationMs @@ -258,6 +295,7 @@ public class VibrationConfig { pw.println("VibrationConfig:"); pw.increaseIndent(); pw.println("ignoreVibrationsOnWirelessCharger = " + mIgnoreVibrationsOnWirelessCharger); + pw.println("defaultVibrationAmplitude = " + mDefaultVibrationAmplitude); pw.println("hapticChannelMaxAmplitude = " + mHapticChannelMaxVibrationAmplitude); pw.println("rampStepDurationMs = " + mRampStepDurationMs); pw.println("rampDownDurationMs = " + mRampDownDurationMs); diff --git a/core/java/android/os/vibrator/flags.aconfig b/core/java/android/os/vibrator/flags.aconfig index 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..4d176f2939a8 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,24 @@ flag { metadata { purpose: PURPOSE_BUGFIX } +} + +flag { + name: "handwriting_gesture_with_transformation" + namespace: "text" + description: "Fix handwriting gesture is not working when view has transformation." + bug: "342619429" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "typeface_cache_for_var_settings" + namespace: "text" + description: "Cache Typeface instance for font variation settings." + bug: "355462362" + metadata { + purpose: PURPOSE_BUGFIX + } }
\ No newline at end of file diff --git a/core/java/android/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/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/graphics/PaintFontVariationTest.java b/core/tests/coretests/src/android/graphics/PaintFontVariationTest.java new file mode 100644 index 000000000000..8a54e5b998e7 --- /dev/null +++ b/core/tests/coretests/src/android/graphics/PaintFontVariationTest.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics; + +import static com.google.common.truth.Truth.assertThat; + +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.test.InstrumentationTestCase; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.text.flags.Flags; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * PaintTest tests {@link Paint}. + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class PaintFontVariationTest extends InstrumentationTestCase { + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + + @RequiresFlagsEnabled(Flags.FLAG_TYPEFACE_CACHE_FOR_VAR_SETTINGS) + @Test + public void testDerivedFromSameTypeface() { + final Paint p = new Paint(); + + p.setTypeface(Typeface.SANS_SERIF); + assertThat(p.setFontVariationSettings("'wght' 450")).isTrue(); + Typeface first = p.getTypeface(); + + p.setTypeface(Typeface.SANS_SERIF); + assertThat(p.setFontVariationSettings("'wght' 480")).isTrue(); + Typeface second = p.getTypeface(); + + assertThat(first.getDerivedFrom()).isSameInstanceAs(second.getDerivedFrom()); + } + + @RequiresFlagsEnabled(Flags.FLAG_TYPEFACE_CACHE_FOR_VAR_SETTINGS) + @Test + public void testDerivedFromChained() { + final Paint p = new Paint(); + + p.setTypeface(Typeface.SANS_SERIF); + assertThat(p.setFontVariationSettings("'wght' 450")).isTrue(); + Typeface first = p.getTypeface(); + + assertThat(p.setFontVariationSettings("'wght' 480")).isTrue(); + Typeface second = p.getTypeface(); + + assertThat(first.getDerivedFrom()).isSameInstanceAs(second.getDerivedFrom()); + } +} diff --git a/core/tests/coretests/src/android/graphics/PaintTest.java b/core/tests/coretests/src/android/graphics/PaintTest.java index 0dec756d7611..878ba703c8fe 100644 --- a/core/tests/coretests/src/android/graphics/PaintTest.java +++ b/core/tests/coretests/src/android/graphics/PaintTest.java @@ -16,13 +16,22 @@ package android.graphics; +import static com.google.common.truth.Truth.assertThat; + import static org.junit.Assert.assertNotEquals; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.test.InstrumentationTestCase; import android.text.TextUtils; import androidx.test.filters.SmallTest; +import com.android.text.flags.Flags; + +import org.junit.Rule; + import java.util.Arrays; import java.util.HashSet; @@ -30,6 +39,9 @@ import java.util.HashSet; * PaintTest tests {@link Paint}. */ public class PaintTest extends InstrumentationTestCase { + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + private static final String FONT_PATH = "fonts/HintedAdvanceWidthTest-Regular.ttf"; static void assertEquals(String message, float[] expected, float[] actual) { @@ -403,4 +415,33 @@ public class PaintTest extends InstrumentationTestCase { assertEquals(6, getClusterCount(p, rtlStr + ltrStr)); assertEquals(9, getClusterCount(p, ltrStr + rtlStr + ltrStr)); } + + @RequiresFlagsEnabled(Flags.FLAG_TYPEFACE_CACHE_FOR_VAR_SETTINGS) + public void testDerivedFromSameTypeface() { + final Paint p = new Paint(); + + p.setTypeface(Typeface.SANS_SERIF); + assertThat(p.setFontVariationSettings("'wght' 450")).isTrue(); + Typeface first = p.getTypeface(); + + p.setTypeface(Typeface.SANS_SERIF); + assertThat(p.setFontVariationSettings("'wght' 480")).isTrue(); + Typeface second = p.getTypeface(); + + assertThat(first.getDerivedFrom()).isSameInstanceAs(second.getDerivedFrom()); + } + + @RequiresFlagsEnabled(Flags.FLAG_TYPEFACE_CACHE_FOR_VAR_SETTINGS) + public void testDerivedFromChained() { + final Paint p = new Paint(); + + p.setTypeface(Typeface.SANS_SERIF); + assertThat(p.setFontVariationSettings("'wght' 450")).isTrue(); + Typeface first = p.getTypeface(); + + assertThat(p.setFontVariationSettings("'wght' 480")).isTrue(); + Typeface second = p.getTypeface(); + + assertThat(first.getDerivedFrom()).isSameInstanceAs(second.getDerivedFrom()); + } } diff --git a/core/tests/coretests/src/android/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..a3d8d3dbf31a 100644 --- a/core/tests/resourceflaggingtests/src/com/android/resourceflaggingtests/ResourceFlaggingTest.java +++ b/core/tests/resourceflaggingtests/src/com/android/resourceflaggingtests/ResourceFlaggingTest.java @@ -68,12 +68,29 @@ public class ResourceFlaggingTest { assertThat(getBoolean("res3")).isTrue(); } + @Test + public void testFlagDisabledNoValue() { + assertThat(getString("str1")).isEqualTo(""); + } + 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/graphics/java/android/graphics/Typeface.java b/graphics/java/android/graphics/Typeface.java index fd788167a0d8..889a778556b7 100644 --- a/graphics/java/android/graphics/Typeface.java +++ b/graphics/java/android/graphics/Typeface.java @@ -56,6 +56,7 @@ import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.Preconditions; +import com.android.text.flags.Flags; import dalvik.annotation.optimization.CriticalNative; import dalvik.annotation.optimization.FastNative; @@ -74,6 +75,7 @@ import java.nio.ByteOrder; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Objects; @@ -143,6 +145,23 @@ public class Typeface { private static final LruCache<String, Typeface> sDynamicTypefaceCache = new LruCache<>(16); private static final Object sDynamicCacheLock = new Object(); + private static final LruCache<Long, LruCache<String, Typeface>> sVariableCache = + new LruCache<>(16); + private static final Object sVariableCacheLock = new Object(); + + /** @hide */ + @VisibleForTesting + public static void clearTypefaceCachesForTestingPurpose() { + synchronized (sWeightCacheLock) { + sWeightTypefaceCache.clear(); + } + synchronized (sDynamicCacheLock) { + sDynamicTypefaceCache.evictAll(); + } + synchronized (sVariableCacheLock) { + sVariableCache.evictAll(); + } + } @GuardedBy("SYSTEM_FONT_MAP_LOCK") static Typeface sDefaultTypeface; @@ -195,6 +214,8 @@ public class Typeface { @UnsupportedAppUsage public final long native_instance; + private final Typeface mDerivedFrom; + private final String mSystemFontFamilyName; private final Runnable mCleaner; @@ -274,6 +295,18 @@ public class Typeface { } /** + * Returns the Typeface used for creating this Typeface. + * + * Maybe null if this is not derived from other Typeface. + * TODO(b/357707916): Make this public API. + * @hide + */ + @VisibleForTesting + public final @Nullable Typeface getDerivedFrom() { + return mDerivedFrom; + } + + /** * Returns the system font family name if the typeface was created from a system font family, * otherwise returns null. */ @@ -1021,9 +1054,51 @@ public class Typeface { return typeface; } - /** @hide */ + private static String axesToVarKey(@NonNull List<FontVariationAxis> axes) { + // The given list can be mutated because it is allocated in Paint#setFontVariationSettings. + // Currently, Paint#setFontVariationSettings is the only code path reaches this method. + axes.sort(Comparator.comparingInt(FontVariationAxis::getOpenTypeTagValue)); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < axes.size(); ++i) { + final FontVariationAxis fva = axes.get(i); + sb.append(fva.getTag()); + sb.append(fva.getStyleValue()); + } + return sb.toString(); + } + + /** + * TODO(b/357707916): Make this public API. + * @hide + */ public static Typeface createFromTypefaceWithVariation(@Nullable Typeface family, @NonNull List<FontVariationAxis> axes) { + if (Flags.typefaceCacheForVarSettings()) { + final Typeface target = (family == null) ? Typeface.DEFAULT : family; + final Typeface base = (target.mDerivedFrom == null) ? target : target.mDerivedFrom; + + final String key = axesToVarKey(axes); + + synchronized (sVariableCacheLock) { + LruCache<String, Typeface> innerCache = sVariableCache.get(base.native_instance); + if (innerCache == null) { + // Cache up to 16 var instance per root Typeface + innerCache = new LruCache<>(16); + sVariableCache.put(base.native_instance, innerCache); + } else { + Typeface cached = innerCache.get(key); + if (cached != null) { + return cached; + } + } + Typeface typeface = new Typeface( + nativeCreateFromTypefaceWithVariation(base.native_instance, axes), + base.getSystemFontFamilyName(), base); + innerCache.put(key, typeface); + return typeface; + } + } + final Typeface base = family == null ? Typeface.DEFAULT : family; Typeface typeface = new Typeface( nativeCreateFromTypefaceWithVariation(base.native_instance, axes), @@ -1184,11 +1259,19 @@ public class Typeface { // don't allow clients to call this directly @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) private Typeface(long ni) { - this(ni, null); + this(ni, null, null); } + // don't allow clients to call this directly + // This is kept for robolectric. private Typeface(long ni, @Nullable String systemFontFamilyName) { + this(ni, systemFontFamilyName, null); + } + + // don't allow clients to call this directly + private Typeface(long ni, @Nullable String systemFontFamilyName, + @Nullable Typeface derivedFrom) { if (ni == 0) { throw new RuntimeException("native typeface cannot be made"); } @@ -1198,6 +1281,7 @@ public class Typeface { mStyle = nativeGetStyle(ni); mWeight = nativeGetWeight(ni); mSystemFontFamilyName = systemFontFamilyName; + mDerivedFrom = derivedFrom; } /** diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/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/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/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/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..c374478ca580 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 @@ -194,6 +194,13 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView @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..8389c819f8ea 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,6 +17,7 @@ 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; @@ -27,6 +28,8 @@ 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 +42,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 +69,18 @@ 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(); } - private void updateActionsBackgroundColor() { + private void updateThemeColors() { try (TypedArray ta = mContext.obtainStyledAttributes(new int[]{ - com.android.internal.R.attr.materialColorSurfaceBright})) { + com.android.internal.R.attr.materialColorSurfaceBright, + com.android.internal.R.attr.materialColorOnSurface + })) { mActionsSectionView.getBackground().setTint(ta.getColor(0, Color.WHITE)); + ImageViewCompat.setImageTintList(mBubbleDismissIconView, + ColorStateList.valueOf(ta.getColor(1, Color.BLACK))); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java index 02918db124e3..0d72998eb2e8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java @@ -19,12 +19,13 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Color; import android.graphics.drawable.Icon; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import androidx.core.content.ContextCompat; import androidx.dynamicanimation.animation.DynamicAnimation; import androidx.dynamicanimation.animation.SpringForce; @@ -172,12 +173,17 @@ class BubbleBarMenuViewController { private ArrayList<BubbleBarMenuView.MenuAction> createMenuActions(Bubble bubble) { ArrayList<BubbleBarMenuView.MenuAction> menuActions = new ArrayList<>(); Resources resources = mContext.getResources(); - + int tintColor; + try (TypedArray ta = mContext.obtainStyledAttributes(new int[]{ + com.android.internal.R.attr.materialColorOnSurface})) { + tintColor = ta.getColor(0, Color.TRANSPARENT); + } if (bubble.isConversation()) { // Don't bubble conversation action menuActions.add(new BubbleBarMenuView.MenuAction( Icon.createWithResource(mContext, R.drawable.bubble_ic_stop_bubble), resources.getString(R.string.bubbles_dont_bubble_conversation), + tintColor, view -> { hideMenu(true /* animated */); if (mListener != null) { @@ -204,7 +210,7 @@ class BubbleBarMenuViewController { menuActions.add(new BubbleBarMenuView.MenuAction( Icon.createWithResource(resources, R.drawable.ic_remove_no_shadow), resources.getString(R.string.bubble_dismiss_text), - ContextCompat.getColor(mContext, R.color.bubble_bar_expanded_view_menu_close), + tintColor, view -> { hideMenu(true /* animated */); if (mListener != null) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java index 1fb0e1745e3e..c4c177cbcc28 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java @@ -47,6 +47,8 @@ public class DisplayInsetsController implements DisplayController.OnDisplaysChan private final SparseArray<PerDisplay> mInsetsPerDisplay = new SparseArray<>(); private final SparseArray<CopyOnWriteArrayList<OnInsetsChangedListener>> mListeners = new SparseArray<>(); + private final CopyOnWriteArrayList<OnInsetsChangedListener> mGlobalListeners = + new CopyOnWriteArrayList<>(); public DisplayInsetsController(IWindowManager wmService, ShellInit shellInit, @@ -81,6 +83,16 @@ public class DisplayInsetsController implements DisplayController.OnDisplaysChan } /** + * Adds a callback to listen for insets changes for any display. Note that the + * listener will not be updated with the existing state of the insets on any display. + */ + public void addGlobalInsetsChangedListener(OnInsetsChangedListener listener) { + if (!mGlobalListeners.contains(listener)) { + mGlobalListeners.add(listener); + } + } + + /** * Removes a callback listening for insets changes from a particular display. */ public void removeInsetsChangedListener(int displayId, OnInsetsChangedListener listener) { @@ -91,6 +103,13 @@ public class DisplayInsetsController implements DisplayController.OnDisplaysChan listeners.remove(listener); } + /** + * Removes a callback listening for insets changes from any display. + */ + public void removeGlobalInsetsChangedListener(OnInsetsChangedListener listener) { + mGlobalListeners.remove(listener); + } + @Override public void onDisplayAdded(int displayId) { PerDisplay pd = new PerDisplay(displayId); @@ -138,12 +157,17 @@ public class DisplayInsetsController implements DisplayController.OnDisplaysChan private void insetsChanged(InsetsState insetsState) { CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(mDisplayId); - if (listeners == null) { + if (listeners == null && mGlobalListeners.isEmpty()) { return; } mDisplayController.updateDisplayInsets(mDisplayId, insetsState); - for (OnInsetsChangedListener listener : listeners) { - listener.insetsChanged(insetsState); + for (OnInsetsChangedListener listener : mGlobalListeners) { + listener.insetsChanged(mDisplayId, insetsState); + } + if (listeners != null) { + for (OnInsetsChangedListener listener : listeners) { + listener.insetsChanged(mDisplayId, insetsState); + } } } @@ -285,6 +309,13 @@ public class DisplayInsetsController implements DisplayController.OnDisplaysChan default void insetsChanged(InsetsState insetsState) {} /** + * Called when the window insets configuration has changed for the given display. + */ + default void insetsChanged(int displayId, InsetsState insetsState) { + insetsChanged(insetsState); + } + + /** * Called when this window retrieved control over a specified set of insets sources. */ default void insetsControlChanged(InsetsState insetsState, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java index 19a109e9a28c..e2988bc6f2aa 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java @@ -23,7 +23,10 @@ import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMA import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; +import static com.android.wm.shell.common.split.SplitLayout.BEHIND_APP_VEIL_LAYER; +import static com.android.wm.shell.common.split.SplitLayout.FRONT_APP_VEIL_LAYER; import static com.android.wm.shell.common.split.SplitScreenConstants.FADE_DURATION; +import static com.android.wm.shell.common.split.SplitScreenConstants.VEIL_DELAY_DURATION; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -74,7 +77,7 @@ public class SplitDecorManager extends WindowlessWindowManager { private final SurfaceSession mSurfaceSession; private Drawable mIcon; - private ImageView mResizingIconView; + private ImageView mVeilIconView; private SurfaceControlViewHost mViewHost; private SurfaceControl mHostLeash; private SurfaceControl mIconLeash; @@ -83,13 +86,14 @@ public class SplitDecorManager extends WindowlessWindowManager { private SurfaceControl mScreenshot; private boolean mShown; - private boolean mIsResizing; + /** True if the task is going through some kind of transition (moving or changing size). */ + private boolean mIsCurrentlyChanging; /** The original bounds of the main task, captured at the beginning of a resize transition. */ private final Rect mOldMainBounds = new Rect(); /** The original bounds of the side task, captured at the beginning of a resize transition. */ private final Rect mOldSideBounds = new Rect(); /** The current bounds of the main task, mid-resize. */ - private final Rect mResizingBounds = new Rect(); + private final Rect mInstantaneousBounds = new Rect(); private final Rect mTempRect = new Rect(); private ValueAnimator mFadeAnimator; private ValueAnimator mScreenshotAnimator; @@ -134,7 +138,7 @@ public class SplitDecorManager extends WindowlessWindowManager { mIconSize = context.getResources().getDimensionPixelSize(R.dimen.split_icon_size); final FrameLayout rootLayout = (FrameLayout) LayoutInflater.from(context) .inflate(R.layout.split_decor, null); - mResizingIconView = rootLayout.findViewById(R.id.split_resizing_icon); + mVeilIconView = rootLayout.findViewById(R.id.split_resizing_icon); final WindowManager.LayoutParams lp = new WindowManager.LayoutParams( 0 /* width */, 0 /* height */, TYPE_APPLICATION_OVERLAY, @@ -191,28 +195,28 @@ public class SplitDecorManager extends WindowlessWindowManager { } mHostLeash = null; mIcon = null; - mResizingIconView = null; - mIsResizing = false; + mVeilIconView = null; + mIsCurrentlyChanging = false; mShown = false; mOldMainBounds.setEmpty(); mOldSideBounds.setEmpty(); - mResizingBounds.setEmpty(); + mInstantaneousBounds.setEmpty(); } /** Showing resizing hint. */ public void onResizing(ActivityManager.RunningTaskInfo resizingTask, Rect newBounds, Rect sideBounds, SurfaceControl.Transaction t, int offsetX, int offsetY, boolean immediately) { - if (mResizingIconView == null) { + if (mVeilIconView == null) { return; } - if (!mIsResizing) { - mIsResizing = true; + if (!mIsCurrentlyChanging) { + mIsCurrentlyChanging = true; mOldMainBounds.set(newBounds); mOldSideBounds.set(sideBounds); } - mResizingBounds.set(newBounds); + mInstantaneousBounds.set(newBounds); mOffsetX = offsetX; mOffsetY = offsetY; @@ -254,8 +258,8 @@ public class SplitDecorManager extends WindowlessWindowManager { if (mIcon == null && resizingTask.topActivityInfo != null) { mIcon = mIconProvider.getIcon(resizingTask.topActivityInfo); - mResizingIconView.setImageDrawable(mIcon); - mResizingIconView.setVisibility(View.VISIBLE); + mVeilIconView.setImageDrawable(mIcon); + mVeilIconView.setVisibility(View.VISIBLE); WindowManager.LayoutParams lp = (WindowManager.LayoutParams) mViewHost.getView().getLayoutParams(); @@ -275,7 +279,12 @@ public class SplitDecorManager extends WindowlessWindowManager { t.setAlpha(mIconLeash, showVeil ? 1f : 0f); t.setVisibility(mIconLeash, showVeil); } else { - startFadeAnimation(showVeil, false, null); + startFadeAnimation( + showVeil, + false /* releaseSurface */, + null /* finishedCallback */, + false /* addDelay */ + ); } mShown = showVeil; } @@ -320,19 +329,19 @@ public class SplitDecorManager extends WindowlessWindowManager { mScreenshotAnimator.start(); } - if (mResizingIconView == null) { + if (mVeilIconView == null) { if (mRunningAnimationCount == 0 && animFinishedCallback != null) { animFinishedCallback.accept(false); } return; } - mIsResizing = false; + mIsCurrentlyChanging = false; mOffsetX = 0; mOffsetY = 0; mOldMainBounds.setEmpty(); mOldSideBounds.setEmpty(); - mResizingBounds.setEmpty(); + mInstantaneousBounds.setEmpty(); if (mFadeAnimator != null && mFadeAnimator.isRunning()) { if (!mShown) { // If fade-out animation is running, just add release callback to it. @@ -356,7 +365,7 @@ public class SplitDecorManager extends WindowlessWindowManager { if (mRunningAnimationCount == 0 && animFinishedCallback != null) { animFinishedCallback.accept(true); } - }); + }, false /* addDelay */); } else { // Decor surface is hidden so release it directly. releaseDecor(t); @@ -366,9 +375,94 @@ public class SplitDecorManager extends WindowlessWindowManager { } } + /** + * Called (on every frame) when two split apps are swapping, and a veil is needed. + */ + public void drawNextVeilFrameForSwapAnimation(ActivityManager.RunningTaskInfo resizingTask, + Rect newBounds, SurfaceControl.Transaction t, boolean isGoingBehind, + SurfaceControl leash, float iconOffsetX, float iconOffsetY) { + if (mVeilIconView == null) { + return; + } + + if (!mIsCurrentlyChanging) { + mIsCurrentlyChanging = true; + } + + mInstantaneousBounds.set(newBounds); + mOffsetX = (int) iconOffsetX; + mOffsetY = (int) iconOffsetY; + + t.setLayer(leash, isGoingBehind ? BEHIND_APP_VEIL_LAYER : FRONT_APP_VEIL_LAYER); + + if (!mShown) { + if (mFadeAnimator != null && mFadeAnimator.isRunning()) { + // Cancel mFadeAnimator if it is running + mFadeAnimator.cancel(); + } + } + + if (mBackgroundLeash == null) { + // Initialize background + mBackgroundLeash = SurfaceUtils.makeColorLayer(mHostLeash, + RESIZING_BACKGROUND_SURFACE_NAME, mSurfaceSession); + t.setColor(mBackgroundLeash, getResizingBackgroundColor(resizingTask)) + .setLayer(mBackgroundLeash, Integer.MAX_VALUE - 1); + } + + if (mIcon == null && resizingTask.topActivityInfo != null) { + // Initialize icon + mIcon = mIconProvider.getIcon(resizingTask.topActivityInfo); + mVeilIconView.setImageDrawable(mIcon); + mVeilIconView.setVisibility(View.VISIBLE); + + WindowManager.LayoutParams lp = + (WindowManager.LayoutParams) mViewHost.getView().getLayoutParams(); + lp.width = mIconSize; + lp.height = mIconSize; + mViewHost.relayout(lp); + + t.setLayer(mIconLeash, Integer.MAX_VALUE); + } + + t.setPosition(mIconLeash, + newBounds.width() / 2 - mIconSize / 2 - mOffsetX, + newBounds.height() / 2 - mIconSize / 2 - mOffsetY); + + // If this is the first frame, we need to trigger the veil's fade-in animation. + if (!mShown) { + startFadeAnimation( + true /* show */, + false /* releaseSurface */, + null /* finishedCallball */, + false /* addDelay */ + ); + mShown = true; + } + } + + /** Called at the end of the swap animation. */ + public void fadeOutVeilAndCleanUp(SurfaceControl.Transaction t) { + if (mVeilIconView == null) { + return; + } + + // Recenter icon + t.setPosition(mIconLeash, + mInstantaneousBounds.width() / 2f - mIconSize / 2f, + mInstantaneousBounds.height() / 2f - mIconSize / 2f); + + mIsCurrentlyChanging = false; + mOffsetX = 0; + mOffsetY = 0; + mInstantaneousBounds.setEmpty(); + + fadeOutDecor(() -> {}, true /* addDelay */); + } + /** Screenshot host leash and attach on it if meet some conditions */ public void screenshotIfNeeded(SurfaceControl.Transaction t) { - if (!mShown && mIsResizing && !mOldMainBounds.equals(mResizingBounds)) { + if (!mShown && mIsCurrentlyChanging && !mOldMainBounds.equals(mInstantaneousBounds)) { if (mScreenshotAnimator != null && mScreenshotAnimator.isRunning()) { mScreenshotAnimator.cancel(); } else if (mScreenshot != null) { @@ -386,7 +480,7 @@ public class SplitDecorManager extends WindowlessWindowManager { public void setScreenshotIfNeeded(SurfaceControl screenshot, SurfaceControl.Transaction t) { if (screenshot == null || !screenshot.isValid()) return; - if (!mShown && mIsResizing && !mOldMainBounds.equals(mResizingBounds)) { + if (!mShown && mIsCurrentlyChanging && !mOldMainBounds.equals(mInstantaneousBounds)) { if (mScreenshotAnimator != null && mScreenshotAnimator.isRunning()) { mScreenshotAnimator.cancel(); } else if (mScreenshot != null) { @@ -401,24 +495,35 @@ public class SplitDecorManager extends WindowlessWindowManager { /** Fade-out decor surface with animation end callback, if decor is hidden, run the callback * directly. */ - public void fadeOutDecor(Runnable finishedCallback) { + public void fadeOutDecor(Runnable finishedCallback, boolean addDelay) { if (mShown) { // If previous animation is running, just cancel it. if (mFadeAnimator != null && mFadeAnimator.isRunning()) { mFadeAnimator.cancel(); } - startFadeAnimation(false /* show */, true, finishedCallback); + startFadeAnimation( + false /* show */, true /* releaseSurface */, finishedCallback, addDelay); mShown = false; } else { if (finishedCallback != null) finishedCallback.run(); } } + /** + * Fades the veil in or out. Called at the first frame of a movement or resize when a veil is + * needed (with show = true), and called again at the end (with show = false). + * @param addDelay If true, adds a short delay before fading out to get the app behind the veil + * time to redraw. + */ private void startFadeAnimation(boolean show, boolean releaseSurface, - Runnable finishedCallback) { + Runnable finishedCallback, boolean addDelay) { final SurfaceControl.Transaction animT = new SurfaceControl.Transaction(); + mFadeAnimator = ValueAnimator.ofFloat(0f, 1f); + if (addDelay) { + mFadeAnimator.setStartDelay(VEIL_DELAY_DURATION); + } mFadeAnimator.setDuration(FADE_DURATION); mFadeAnimator.addUpdateListener(valueAnimator-> { final float progress = (float) valueAnimator.getAnimatedValue(); @@ -481,8 +586,8 @@ public class SplitDecorManager extends WindowlessWindowManager { } if (mIcon != null) { - mResizingIconView.setVisibility(View.GONE); - mResizingIconView.setImageDrawable(null); + mVeilIconView.setVisibility(View.GONE); + mVeilIconView.setImageDrawable(null); t.hide(mIconLeash); mIcon = null; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java index 51f9de8305f8..0e050694c733 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java @@ -53,6 +53,8 @@ import android.view.RoundedCorner; import android.view.SurfaceControl; import android.view.WindowInsets; import android.view.WindowManager; +import android.view.animation.Interpolator; +import android.view.animation.PathInterpolator; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; @@ -68,10 +70,12 @@ import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayImeController; import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.common.split.SplitScreenConstants.PersistentSnapPosition; import com.android.wm.shell.common.split.SplitScreenConstants.SnapPosition; import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition; import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.splitscreen.StageTaskListener; import java.io.PrintWriter; import java.util.function.Consumer; @@ -87,10 +91,29 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange public static final int PARALLAX_ALIGN_CENTER = 2; public static final int FLING_RESIZE_DURATION = 250; - private static final int FLING_SWITCH_DURATION = 350; private static final int FLING_ENTER_DURATION = 450; private static final int FLING_EXIT_DURATION = 450; + // Here are some (arbitrarily decided) layer definitions used during animations to make sure the + // layers stay in order. Note: This does not affect any other layer numbering systems because + // the layer system in WindowManager is local within sibling groups. So, for example, each + // "veil layer" defined here actually has two sub-layers; and *their* layer values, which we set + // in SplitDecorManager, are only important relative to each other. + public static final int DIVIDER_LAYER = 0; + public static final int FRONT_APP_VEIL_LAYER = DIVIDER_LAYER + 20; + public static final int FRONT_APP_LAYER = DIVIDER_LAYER + 10; + public static final int BEHIND_APP_VEIL_LAYER = DIVIDER_LAYER - 10; + public static final int BEHIND_APP_LAYER = DIVIDER_LAYER - 20; + + // Animation specs for the swap animation + private static final int SWAP_ANIMATION_TOTAL_DURATION = 500; + private static final float SWAP_ANIMATION_SHRINK_DURATION = 83; + private static final float SWAP_ANIMATION_SHRINK_MARGIN_DP = 14; + private static final Interpolator SHRINK_INTERPOLATOR = + new PathInterpolator(0.2f, 0f, 0f, 1f); + private static final Interpolator GROW_INTERPOLATOR = + new PathInterpolator(0.45f, 0f, 0.5f, 1f); + private int mDividerWindowWidth; private int mDividerInsets; private int mDividerSize; @@ -134,6 +157,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange private final InteractionJankMonitor mInteractionJankMonitor; private boolean mIsLeftRightSplit; private ValueAnimator mDividerFlingAnimator; + private AnimatorSet mSwapAnimator; public SplitLayout(String windowName, Context context, Configuration configuration, SplitLayoutHandler splitLayoutHandler, @@ -579,6 +603,10 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange } void onDoubleTappedDivider() { + if (isCurrentlySwapping()) { + return; + } + mSplitLayoutHandler.onDoubleTappedDivider(); } @@ -685,36 +713,43 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange } /** Switch both surface position with animation. */ - public void splitSwitching(SurfaceControl.Transaction t, SurfaceControl leash1, - SurfaceControl leash2, Consumer<Rect> finishCallback) { + public void playSwapAnimation(SurfaceControl.Transaction t, StageTaskListener topLeftStage, + StageTaskListener bottomRightStage, Consumer<Rect> finishCallback) { final Rect insets = getDisplayStableInsets(mContext); + // If we have insets in the direction of the swap, the animation won't look correct because + // window contents will shift and redraw again at the end. So we show a veil to hide that. insets.set(mIsLeftRightSplit ? insets.left : 0, mIsLeftRightSplit ? 0 : insets.top, mIsLeftRightSplit ? insets.right : 0, mIsLeftRightSplit ? 0 : insets.bottom); + final boolean shouldVeil = + insets.left != 0 || insets.top != 0 || insets.right != 0 || insets.bottom != 0; final int dividerPos = mDividerSnapAlgorithm.calculateNonDismissingSnapTarget( mIsLeftRightSplit ? mBounds2.width() : mBounds2.height()).position; - final Rect distBounds1 = new Rect(); - final Rect distBounds2 = new Rect(); - final Rect distDividerBounds = new Rect(); - // Compute dist bounds. - updateBounds(dividerPos, distBounds2, distBounds1, distDividerBounds, + final Rect endBounds1 = new Rect(); + final Rect endBounds2 = new Rect(); + final Rect endDividerBounds = new Rect(); + // Compute destination bounds. + updateBounds(dividerPos, endBounds2, endBounds1, endDividerBounds, false /* setEffectBounds */); // Offset to real position under root container. - distBounds1.offset(-mRootBounds.left, -mRootBounds.top); - distBounds2.offset(-mRootBounds.left, -mRootBounds.top); - distDividerBounds.offset(-mRootBounds.left, -mRootBounds.top); - - ValueAnimator animator1 = moveSurface(t, leash1, getRefBounds1(), distBounds1, - -insets.left, -insets.top); - ValueAnimator animator2 = moveSurface(t, leash2, getRefBounds2(), distBounds2, - insets.left, insets.top); - ValueAnimator animator3 = moveSurface(t, getDividerLeash(), getRefDividerBounds(), - distDividerBounds, 0 /* offsetX */, 0 /* offsetY */); - - AnimatorSet set = new AnimatorSet(); - set.playTogether(animator1, animator2, animator3); - set.setDuration(FLING_SWITCH_DURATION); - set.addListener(new AnimatorListenerAdapter() { + endBounds1.offset(-mRootBounds.left, -mRootBounds.top); + endBounds2.offset(-mRootBounds.left, -mRootBounds.top); + endDividerBounds.offset(-mRootBounds.left, -mRootBounds.top); + + ValueAnimator animator1 = moveSurface(t, topLeftStage, getRefBounds1(), endBounds1, + -insets.left, -insets.top, true /* roundCorners */, true /* isGoingBehind */, + shouldVeil); + ValueAnimator animator2 = moveSurface(t, bottomRightStage, getRefBounds2(), endBounds2, + insets.left, insets.top, true /* roundCorners */, false /* isGoingBehind */, + shouldVeil); + ValueAnimator animator3 = moveSurface(t, null /* stage */, getRefDividerBounds(), + endDividerBounds, 0 /* offsetX */, 0 /* offsetY */, false /* roundCorners */, + false /* isGoingBehind */, false /* addVeil */); + + mSwapAnimator = new AnimatorSet(); + mSwapAnimator.playTogether(animator1, animator2, animator3); + mSwapAnimator.setDuration(SWAP_ANIMATION_TOTAL_DURATION); + mSwapAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { mInteractionJankMonitor.begin(getDividerLeash(), @@ -734,36 +769,144 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange mInteractionJankMonitor.cancel(CUJ_SPLIT_SCREEN_DOUBLE_TAP_DIVIDER); } }); - set.start(); + mSwapAnimator.start(); + } + + /** Returns true if a swap animation is currently playing. */ + public boolean isCurrentlySwapping() { + return mSwapAnimator != null && mSwapAnimator.isRunning(); } - private ValueAnimator moveSurface(SurfaceControl.Transaction t, SurfaceControl leash, - Rect start, Rect end, float offsetX, float offsetY) { + /** + * Animates a task leash across the screen. Currently used only for the swap animation. + * + * @param stage The stage holding the task being animated. If null, it is the divider. + * @param roundCorners Whether we should round the corners of the task while animating. + * @param isGoingBehind Whether we should a shrink-and-grow effect to the task while it is + * moving. (Simulates moving behind the divider.) + */ + private ValueAnimator moveSurface(SurfaceControl.Transaction t, StageTaskListener stage, + Rect start, Rect end, float offsetX, float offsetY, boolean roundCorners, + boolean isGoingBehind, boolean addVeil) { + final boolean isApp = stage != null; // check if this is an app or a divider + final SurfaceControl leash = isApp ? stage.getRootLeash() : getDividerLeash(); + final ActivityManager.RunningTaskInfo taskInfo = isApp ? stage.getRunningTaskInfo() : null; + final SplitDecorManager decorManager = isApp ? stage.getDecorManager() : null; + Rect tempStart = new Rect(start); Rect tempEnd = new Rect(end); final float diffX = tempEnd.left - tempStart.left; final float diffY = tempEnd.top - tempStart.top; final float diffWidth = tempEnd.width() - tempStart.width(); final float diffHeight = tempEnd.height() - tempStart.height(); + + // Get display measurements (for possible shrink animation). + final RoundedCorner roundedCorner = mSplitWindowManager.getDividerView().getDisplay() + .getRoundedCorner(0 /* position */); + float cornerRadius = roundedCorner == null ? 0 : roundedCorner.getRadius(); + float shrinkMarginPx = PipUtils.dpToPx( + SWAP_ANIMATION_SHRINK_MARGIN_DP, mContext.getResources().getDisplayMetrics()); + float shrinkAmountPx = shrinkMarginPx * 2; + + // Timing calculations + float shrinkPortion = SWAP_ANIMATION_SHRINK_DURATION / SWAP_ANIMATION_TOTAL_DURATION; + float growPortion = 1 - shrinkPortion; + ValueAnimator animator = ValueAnimator.ofFloat(0, 1); + animator.setInterpolator(Interpolators.EMPHASIZED); animator.addUpdateListener(animation -> { if (leash == null) return; + if (roundCorners) { + // Add rounded corners to the task leash while it is animating. + t.setCornerRadius(leash, cornerRadius); + } + + final float progress = (float) animation.getAnimatedValue(); + float instantaneousX = tempStart.left + progress * diffX; + float instantaneousY = tempStart.top + progress * diffY; + int width = (int) (tempStart.width() + progress * diffWidth); + int height = (int) (tempStart.height() + progress * diffHeight); + + if (isGoingBehind) { + float shrinkDiffX; // the position adjustments needed for this frame + float shrinkDiffY; + float shrinkScaleX; // the scale adjustments needed for this frame + float shrinkScaleY; + + // Find the max amount we will be shrinking this leash, as a proportion (e.g. 0.1f). + float maxShrinkX = shrinkAmountPx / height; + float maxShrinkY = shrinkAmountPx / width; + + // Find if we are in the shrinking part of the animation, or the growing part. + boolean shrinking = progress <= shrinkPortion; + + if (shrinking) { + // Find how far into the shrink portion we are (e.g. 0.5f). + float shrinkProgress = progress / shrinkPortion; + // Find how much we should have progressed in shrinking the leash (e.g. 0.8f). + float interpolatedShrinkProgress = + SHRINK_INTERPOLATOR.getInterpolation(shrinkProgress); + // Find how much width proportion we should be taking off (e.g. 0.1f) + float widthProportionLost = maxShrinkX * interpolatedShrinkProgress; + shrinkScaleX = 1 - widthProportionLost; + // Find how much height proportion we should be taking off (e.g. 0.1f) + float heightProportionLost = maxShrinkY * interpolatedShrinkProgress; + shrinkScaleY = 1 - heightProportionLost; + // Add a small amount to the leash's position to keep the task centered. + shrinkDiffX = (width * widthProportionLost) / 2; + shrinkDiffY = (height * heightProportionLost) / 2; + } else { + // Find how far into the grow portion we are (e.g. 0.5f). + float growProgress = (progress - shrinkPortion) / growPortion; + // Find how much we should have progressed in growing the leash (e.g. 0.8f). + float interpolatedGrowProgress = + GROW_INTERPOLATOR.getInterpolation(growProgress); + // Find how much width proportion we should be taking off (e.g. 0.1f) + float widthProportionLost = maxShrinkX * (1 - interpolatedGrowProgress); + shrinkScaleX = 1 - widthProportionLost; + // Find how much height proportion we should be taking off (e.g. 0.1f) + float heightProportionLost = maxShrinkY * (1 - interpolatedGrowProgress); + shrinkScaleY = 1 - heightProportionLost; + // Add a small amount to the leash's position to keep the task centered. + shrinkDiffX = (width * widthProportionLost) / 2; + shrinkDiffY = (height * heightProportionLost) / 2; + } + + instantaneousX += shrinkDiffX; + instantaneousY += shrinkDiffY; + width *= shrinkScaleX; + height *= shrinkScaleY; + // Set scale on the leash's contents. + t.setScale(leash, shrinkScaleX, shrinkScaleY); + } + + // Set layers + if (taskInfo != null) { + t.setLayer(leash, isGoingBehind ? BEHIND_APP_LAYER : FRONT_APP_LAYER); + } else { + t.setLayer(leash, DIVIDER_LAYER); + } - final float scale = (float) animation.getAnimatedValue(); - final float distX = tempStart.left + scale * diffX; - final float distY = tempStart.top + scale * diffY; - final int width = (int) (tempStart.width() + scale * diffWidth); - final int height = (int) (tempStart.height() + scale * diffHeight); if (offsetX == 0 && offsetY == 0) { - t.setPosition(leash, distX, distY); + t.setPosition(leash, instantaneousX, instantaneousY); + mTempRect.set((int) instantaneousX, (int) instantaneousY, + (int) (instantaneousX + width), (int) (instantaneousY + height)); t.setWindowCrop(leash, width, height); + if (addVeil) { + decorManager.drawNextVeilFrameForSwapAnimation( + taskInfo, mTempRect, t, isGoingBehind, leash, 0, 0); + } } else { - final int diffOffsetX = (int) (scale * offsetX); - final int diffOffsetY = (int) (scale * offsetY); - t.setPosition(leash, distX + diffOffsetX, distY + diffOffsetY); + final int diffOffsetX = (int) (progress * offsetX); + final int diffOffsetY = (int) (progress * offsetY); + t.setPosition(leash, instantaneousX + diffOffsetX, instantaneousY + diffOffsetY); mTempRect.set(0, 0, width, height); mTempRect.offsetTo(-diffOffsetX, -diffOffsetY); t.setCrop(leash, mTempRect); + if (addVeil) { + decorManager.drawNextVeilFrameForSwapAnimation( + taskInfo, mTempRect, t, isGoingBehind, leash, diffOffsetX, diffOffsetY); + } } t.apply(); }); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java index e8c809e5db4a..8c06de79ba76 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java @@ -29,6 +29,8 @@ import com.android.wm.shell.shared.TransitionUtil; public class SplitScreenConstants { /** Duration used for every split fade-in or fade-out. */ public static final int FADE_DURATION = 133; + /** Duration where we keep an app veiled to allow it to redraw itself behind the scenes. */ + public static final int VEIL_DELAY_DURATION = 400; /** Key for passing in widget intents when invoking split from launcher workspace. */ public static final String KEY_EXTRA_WIDGET_INTENT = "key_extra_widget_intent"; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java index 7c0455e17cf2..c2ee223b916a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java @@ -186,6 +186,9 @@ public class CompatUIController implements OnDisplaysChangedListener, */ private boolean mIsFirstReachabilityEducationRunning; + @NonNull + private final CompatUIStatusManager mCompatUIStatusManager; + public CompatUIController(@NonNull Context context, @NonNull ShellInit shellInit, @NonNull ShellController shellController, @@ -198,7 +201,8 @@ public class CompatUIController implements OnDisplaysChangedListener, @NonNull DockStateReader dockStateReader, @NonNull CompatUIConfiguration compatUIConfiguration, @NonNull CompatUIShellCommandHandler compatUIShellCommandHandler, - @NonNull AccessibilityManager accessibilityManager) { + @NonNull AccessibilityManager accessibilityManager, + @NonNull CompatUIStatusManager compatUIStatusManager) { mContext = context; mShellController = shellController; mDisplayController = displayController; @@ -213,6 +217,7 @@ public class CompatUIController implements OnDisplaysChangedListener, mCompatUIShellCommandHandler = compatUIShellCommandHandler; mDisappearTimeSupplier = flags -> accessibilityManager.getRecommendedTimeoutMillis( DISAPPEAR_DELAY_MS, flags); + mCompatUIStatusManager = compatUIStatusManager; shellInit.addInitCallback(this::onInit, this); } @@ -520,7 +525,7 @@ public class CompatUIController implements OnDisplaysChangedListener, mSyncQueue, taskListener, mDisplayController.getDisplayLayout(taskInfo.displayId), mTransitionsLazy.get(), stateInfo -> createOrUpdateReachabilityEduLayout(stateInfo.first, stateInfo.second), - mDockStateReader, mCompatUIConfiguration); + mDockStateReader, mCompatUIConfiguration, mCompatUIStatusManager); } private void createOrUpdateRestartDialogLayout(@NonNull TaskInfo taskInfo, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIStatusManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIStatusManager.java new file mode 100644 index 000000000000..915a8a149d54 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIStatusManager.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.compatui; + +import android.annotation.NonNull; + +import java.util.function.IntConsumer; +import java.util.function.IntSupplier; + +/** Handle the visibility state of the Compat UI components. */ +public class CompatUIStatusManager { + + public static final int COMPAT_UI_EDUCATION_HIDDEN = 0; + public static final int COMPAT_UI_EDUCATION_VISIBLE = 1; + + @NonNull + private final IntConsumer mWriter; + @NonNull + private final IntSupplier mReader; + + public CompatUIStatusManager(@NonNull IntConsumer writer, @NonNull IntSupplier reader) { + mWriter = writer; + mReader = reader; + } + + public CompatUIStatusManager() { + this(i -> { }, () -> COMPAT_UI_EDUCATION_HIDDEN); + } + + void onEducationShown() { + mWriter.accept(COMPAT_UI_EDUCATION_VISIBLE); + } + + void onEducationHidden() { + mWriter.accept(COMPAT_UI_EDUCATION_HIDDEN); + } + + boolean isEducationVisible() { + return mReader.getAsInt() == COMPAT_UI_EDUCATION_VISIBLE; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/LetterboxEduWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/LetterboxEduWindowManager.java index 234703277c7d..3124a397162f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/LetterboxEduWindowManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/LetterboxEduWindowManager.java @@ -19,6 +19,7 @@ package com.android.wm.shell.compatui; import static android.provider.Settings.Secure.LAUNCHER_TASKBAR_EDUCATION_SHOWING; import static android.window.TaskConstants.TASK_CHILD_LAYER_COMPAT_UI; +import android.annotation.NonNull; import android.annotation.Nullable; import android.app.TaskInfo; import android.content.Context; @@ -76,15 +77,19 @@ class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract { private final DockStateReader mDockStateReader; + @NonNull + private final CompatUIStatusManager mCompatUIStatusManager; + LetterboxEduWindowManager(Context context, TaskInfo taskInfo, SyncTransactionQueue syncQueue, ShellTaskOrganizer.TaskListener taskListener, DisplayLayout displayLayout, Transitions transitions, Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> onDismissCallback, - DockStateReader dockStateReader, CompatUIConfiguration compatUIConfiguration) { + DockStateReader dockStateReader, CompatUIConfiguration compatUIConfiguration, + @NonNull CompatUIStatusManager compatUIStatusManager) { this(context, taskInfo, syncQueue, taskListener, displayLayout, transitions, onDismissCallback, new DialogAnimationController<>(context, /* tag */ "LetterboxEduWindowManager"), - dockStateReader, compatUIConfiguration); + dockStateReader, compatUIConfiguration, compatUIStatusManager); } @VisibleForTesting @@ -93,7 +98,8 @@ class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract { DisplayLayout displayLayout, Transitions transitions, Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> onDismissCallback, DialogAnimationController<LetterboxEduDialogLayout> animationController, - DockStateReader dockStateReader, CompatUIConfiguration compatUIConfiguration) { + DockStateReader dockStateReader, CompatUIConfiguration compatUIConfiguration, + @NonNull CompatUIStatusManager compatUIStatusManager) { super(context, taskInfo, syncQueue, taskListener, displayLayout); mTransitions = transitions; mOnDismissCallback = onDismissCallback; @@ -103,6 +109,7 @@ class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract { R.dimen.letterbox_education_dialog_margin); mDockStateReader = dockStateReader; mCompatUIConfiguration = compatUIConfiguration; + mCompatUIStatusManager = compatUIStatusManager; mEligibleForLetterboxEducation = taskInfo.appCompatTaskInfo.eligibleForLetterboxEducation(); } @@ -139,7 +146,7 @@ class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract { protected View createLayout() { mLayout = inflateLayout(); updateDialogMargins(); - + mCompatUIStatusManager.onEducationShown(); // startEnterAnimation will be called immediately if shell-transitions are disabled. mTransitions.runOnIdle(this::startEnterAnimation); return mLayout; @@ -199,6 +206,7 @@ class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract { @Override public void release() { mAnimationController.cancelAnimation(); + mCompatUIStatusManager.onEducationHidden(); super.release(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/OWNERS new file mode 100644 index 000000000000..1875675296a8 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/OWNERS @@ -0,0 +1,4 @@ +# WM shell sub-module compat ui owners +mariiasand@google.com +gracielawputri@google.com +mcarli@google.com diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUISpec.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUISpec.kt index a520d5e60fe5..022906cf568c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUISpec.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUISpec.kt @@ -16,6 +16,9 @@ package com.android.wm.shell.compatui.api +import com.android.internal.protolog.ProtoLog +import com.android.wm.shell.protolog.ShellProtoLogGroup + /** * Defines the predicates to invoke for understanding if a component can be created or destroyed. */ @@ -39,6 +42,7 @@ class CompatUILifecyclePredicates( * Describes each compat ui component to the framework. */ class CompatUISpec( + val log: (String) -> Unit = { str -> ProtoLog.v(ShellProtoLogGroup.WM_SHELL_COMPAT_UI, str) }, // Unique name for the component. It's used for debug and for generating the // unique component identifier in the system. val name: String, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index f22dcce00907..04cd225ea4a3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -16,6 +16,9 @@ package com.android.wm.shell.dagger; +import static android.provider.Settings.Secure.COMPAT_UI_EDUCATION_SHOWING; + +import static com.android.wm.shell.compatui.CompatUIStatusManager.COMPAT_UI_EDUCATION_HIDDEN; import static com.android.wm.shell.onehanded.OneHandedController.SUPPORT_ONE_HANDED_MODE; import android.annotation.NonNull; @@ -24,6 +27,7 @@ import android.content.Context; import android.content.pm.PackageManager; import android.os.Handler; import android.os.SystemProperties; +import android.provider.Settings; import android.view.IWindowManager; import android.view.accessibility.AccessibilityManager; import android.window.SystemPerformanceHinter; @@ -72,6 +76,7 @@ import com.android.wm.shell.common.pip.SizeSpecSource; import com.android.wm.shell.compatui.CompatUIConfiguration; import com.android.wm.shell.compatui.CompatUIController; import com.android.wm.shell.compatui.CompatUIShellCommandHandler; +import com.android.wm.shell.compatui.CompatUIStatusManager; import com.android.wm.shell.compatui.api.CompatUIComponentIdGenerator; import com.android.wm.shell.compatui.api.CompatUIHandler; import com.android.wm.shell.compatui.api.CompatUIRepository; @@ -254,7 +259,8 @@ public abstract class WMShellBaseModule { Lazy<AccessibilityManager> accessibilityManager, CompatUIRepository compatUIRepository, @NonNull CompatUIState compatUIState, - @NonNull CompatUIComponentIdGenerator componentIdGenerator) { + @NonNull CompatUIComponentIdGenerator componentIdGenerator, + CompatUIStatusManager compatUIStatusManager) { if (!context.getResources().getBoolean(R.bool.config_enableCompatUIController)) { return Optional.empty(); } @@ -276,7 +282,22 @@ public abstract class WMShellBaseModule { dockStateReader.get(), compatUIConfiguration.get(), compatUIShellCommandHandler.get(), - accessibilityManager.get())); + accessibilityManager.get(), + compatUIStatusManager)); + } + + @WMSingleton + @Provides + static CompatUIStatusManager provideCompatUIStatusManager(@NonNull Context context) { + if (Flags.enableCompatUiVisibilityStatus()) { + return new CompatUIStatusManager( + newState -> Settings.Secure.putInt(context.getContentResolver(), + COMPAT_UI_EDUCATION_SHOWING, newState), + () -> Settings.Secure.getInt(context.getContentResolver(), + COMPAT_UI_EDUCATION_SHOWING, COMPAT_UI_EDUCATION_HIDDEN)); + } else { + return new CompatUIStatusManager(); + } } @WMSingleton diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index a18bbadbde69..b8b62a76c568 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,6 +70,7 @@ 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.draganddrop.DragAndDropController; import com.android.wm.shell.draganddrop.GlobalDragListener; @@ -603,10 +606,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 diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt index 3e7b4fe89b45..6c03dc333515 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt @@ -54,6 +54,12 @@ fun calculateInitialBounds( // Instead default to the desired initial bounds. val stableBounds = Rect() displayLayout.getStableBoundsForDesktopMode(stableBounds) + if (hasFullscreenOverride(taskInfo)) { + // If the activity has a fullscreen override applied, it should be treated as + // resizeable and match the device orientation. Thus the ideal size can be + // applied. + return positionInScreen(idealSize, stableBounds) + } val topActivityInfo = taskInfo.topActivityInfo ?: return positionInScreen(idealSize, stableBounds) @@ -62,13 +68,17 @@ fun calculateInitialBounds( ORIENTATION_LANDSCAPE -> { if (taskInfo.isResizeable) { if (isFixedOrientationPortrait(topActivityInfo.screenOrientation)) { - // Respect apps fullscreen width + // For portrait resizeable activities, respect apps fullscreen width but + // apply ideal size height. Size(taskInfo.appCompatTaskInfo.topActivityLetterboxAppWidth, idealSize.height) } else { + // For landscape resizeable activities, simply apply ideal size. idealSize } } else { + // If activity is unresizeable, regardless of orientation, calculate maximum + // size (within the ideal size) maintaining original aspect ratio. maximizeSizeGivenAspectRatio(taskInfo, idealSize, appAspectRatio) } } @@ -77,23 +87,29 @@ fun calculateInitialBounds( screenBounds.width() - (DESKTOP_MODE_LANDSCAPE_APP_PADDING * 2) if (taskInfo.isResizeable) { if (isFixedOrientationLandscape(topActivityInfo.screenOrientation)) { - // Respect apps fullscreen height and apply custom app width + // For landscape resizeable activities, respect apps fullscreen height and + // apply custom app width. Size( customPortraitWidthForLandscapeApp, taskInfo.appCompatTaskInfo.topActivityLetterboxAppHeight ) } else { + // For portrait resizeable activities, simply apply ideal size. idealSize } } else { if (isFixedOrientationLandscape(topActivityInfo.screenOrientation)) { - // Apply custom app width and calculate maximum size + // For landscape unresizeable activities, apply custom app width to ideal + // size and calculate maximum size with this area while maintaining original + // aspect ratio. maximizeSizeGivenAspectRatio( taskInfo, Size(customPortraitWidthForLandscapeApp, idealSize.height), appAspectRatio ) } else { + // For portrait unresizeable activities, calculate maximum size (within the + // ideal size) maintaining original aspect ratio. maximizeSizeGivenAspectRatio(taskInfo, idealSize, appAspectRatio) } } @@ -209,3 +225,8 @@ fun TaskInfo.hasPortraitTopActivity(): Boolean { else -> isFixedOrientationPortrait(configuration.orientation) } } + +private fun hasFullscreenOverride(taskInfo: RunningTaskInfo): Boolean { + return taskInfo.appCompatTaskInfo.isUserFullscreenOverrideEnabled + || taskInfo.appCompatTaskInfo.isSystemFullscreenOverrideEnabled +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java index 6011db7fc752..09f9139cb1d5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java @@ -27,6 +27,7 @@ import android.animation.AnimatorListenerAdapter; import android.animation.RectEvaluator; import android.animation.ValueAnimator; import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.ActivityManager; import android.app.WindowConfiguration; import android.content.Context; @@ -262,12 +263,22 @@ public class DesktopModeVisualIndicator { /** * Fade out indicator without fully releasing it. Animator fades it out while shrinking bounds. + * + * @param finishCallback called when animation ends or gets cancelled */ - private void fadeOutIndicator() { + void fadeOutIndicator(@Nullable Runnable finishCallback) { final VisualIndicatorAnimator animator = VisualIndicatorAnimator .fadeBoundsOut(mView, mCurrentType, mDisplayController.getDisplayLayout(mTaskInfo.displayId)); animator.start(); + if (finishCallback != null) { + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + finishCallback.run(); + } + }); + } mCurrentType = IndicatorType.NO_INDICATOR; } @@ -282,7 +293,7 @@ public class DesktopModeVisualIndicator { if (mCurrentType == IndicatorType.NO_INDICATOR) { fadeInIndicator(newType); } else if (newType == IndicatorType.NO_INDICATOR) { - fadeOutIndicator(); + fadeOutIndicator(null /* finishCallback */); } else { final VisualIndicatorAnimator animator = VisualIndicatorAnimator.animateIndicatorType( mView, mDisplayController.getDisplayLayout(mTaskInfo.displayId), mCurrentType, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskPosition.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskPosition.kt index 97abda81d12d..65f12cf4a196 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskPosition.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskPosition.kt @@ -116,10 +116,10 @@ fun canChangeTaskPosition(taskInfo: TaskInfo): Boolean { @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) fun Rect.getDesktopTaskPosition(bounds: Rect): DesktopTaskPosition { return when { - top == bounds.top && left == bounds.left -> TopLeft - top == bounds.top && right == bounds.right -> TopRight - bottom == bounds.bottom && left == bounds.left -> BottomLeft - bottom == bounds.bottom && right == bounds.right -> BottomRight + top == bounds.top && left == bounds.left && bottom != bounds.bottom -> TopLeft + top == bounds.top && right == bounds.right && bottom != bounds.bottom -> TopRight + bottom == bounds.bottom && left == bounds.left && top != bounds.top -> BottomLeft + bottom == bounds.bottom && right == bounds.right && top != bounds.top -> BottomRight else -> Center } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index e154da58028a..f54b44b29683 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -163,8 +163,10 @@ class DesktopTasksController( } private fun removeVisualIndicator(tx: SurfaceControl.Transaction) { - visualIndicator?.releaseVisualIndicator(tx) - visualIndicator = null + visualIndicator?.fadeOutIndicator { + visualIndicator?.releaseVisualIndicator(tx) + visualIndicator = null + } } } @@ -193,7 +195,7 @@ class DesktopTasksController( ) transitions.addHandler(this) taskRepository.addVisibleTasksListener(taskVisibilityListener, mainExecutor) - dragToDesktopTransitionHandler.setDragToDesktopStateListener(dragToDesktopStateListener) + dragToDesktopTransitionHandler.dragToDesktopStateListener = dragToDesktopStateListener recentsTransitionHandler.addTransitionStateListener( object : RecentsTransitionStateListener { override fun onAnimationStateChanged(running: Boolean) { @@ -213,7 +215,7 @@ class DesktopTasksController( fun setOnTaskResizeAnimationListener(listener: OnTaskResizeAnimationListener) { toggleResizeDesktopTaskTransitionHandler.setOnTaskResizeAnimationListener(listener) enterDesktopTaskTransitionHandler.setOnTaskResizeAnimationListener(listener) - dragToDesktopTransitionHandler.setOnTaskResizeAnimatorListener(listener) + dragToDesktopTransitionHandler.onTaskResizeAnimationListener = listener } fun setOnTaskRepositionAnimationListener(listener: OnTaskRepositionAnimationListener) { @@ -1068,6 +1070,11 @@ class DesktopTasksController( // In some launches home task is moved behind new task being launched. Make sure // that's not the case for launches in desktop. moveHomeTask(wct, toTop = false) + // Move existing minimized tasks behind Home + taskRepository.getFreeformTasksInZOrder(task.displayId) + .filter { taskId -> taskRepository.isMinimizedTask(taskId) } + .mapNotNull { taskId -> shellTaskOrganizer.getRunningTaskInfo(taskId) } + .forEach { taskInfo -> wct.reorder(taskInfo.token, /* onTop= */ false) } // Desktop Mode is already showing and we're launching a new Task - we might need to // minimize another Task. val taskToMinimize = addAndGetMinimizeChangesIfNeeded(task.displayId, wct, task) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/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/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/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 9f735c46eceb..d1be12f6d1bb 100644 --- a/libs/WindowManager/Shell/tests/OWNERS +++ b/libs/WindowManager/Shell/tests/OWNERS @@ -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/unittest/res/layout/caption_layout.xml b/libs/WindowManager/Shell/tests/unittest/res/layout/caption_layout.xml new file mode 100644 index 000000000000..079ee13ba4da --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/res/layout/caption_layout.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<com.android.wm.shell.windowdecor.WindowDecorLinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/caption" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="end" + android:background="@drawable/caption_decor_title"/>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java index 669e433ba386..9df9956fa0e1 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java @@ -18,6 +18,7 @@ package com.android.wm.shell.common; import static android.view.Display.DEFAULT_DISPLAY; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -160,6 +161,19 @@ public class DisplayInsetsControllerTest extends ShellTestCase { assertTrue(secondListener.hideInsetsCount == 1); } + @Test + public void testGlobalListenerCallback() throws RemoteException { + TrackedListener globalListener = new TrackedListener(); + addDisplay(SECOND_DISPLAY); + mController.addGlobalInsetsChangedListener(globalListener); + + mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).insetsChanged(null); + mInsetsControllersByDisplayId.get(SECOND_DISPLAY).insetsChanged(null); + mExecutor.flushAll(); + + assertEquals(2, globalListener.insetsChangedCount); + } + private void addDisplay(int displayId) throws RemoteException { mController.onDisplayAdded(displayId); verify(mWm, times(mInsetsControllersByDisplayId.size() + 1)) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java index de1659b1a163..b39cf19a155a 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java @@ -43,6 +43,7 @@ import android.view.InsetsSource; import android.view.InsetsState; import android.view.accessibility.AccessibilityManager; +import androidx.annotation.NonNull; import androidx.test.filters.SmallTest; import com.android.window.flags.Flags; @@ -128,6 +129,9 @@ public class CompatUIControllerTest extends ShellTestCase { @Captor ArgumentCaptor<OnInsetsChangedListener> mOnInsetsChangedListenerCaptor; + @NonNull + private CompatUIStatusManager mCompatUIStatusManager; + @Before public void setUp() { MockitoAnnotations.initMocks(this); @@ -147,11 +151,13 @@ public class CompatUIControllerTest extends ShellTestCase { doReturn(true).when(mMockRestartDialogLayout).createLayout(anyBoolean()); doReturn(true).when(mMockRestartDialogLayout).updateCompatInfo(any(), any(), anyBoolean()); + mCompatUIStatusManager = new CompatUIStatusManager(); mShellInit = spy(new ShellInit(mMockExecutor)); mController = new CompatUIController(mContext, mShellInit, mMockShellController, mMockDisplayController, mMockDisplayInsetsController, mMockImeController, mMockSyncQueue, mMockExecutor, mMockTransitionsLazy, mDockStateReader, - mCompatUIConfiguration, mCompatUIShellCommandHandler, mAccessibilityManager) { + mCompatUIConfiguration, mCompatUIShellCommandHandler, mAccessibilityManager, + mCompatUIStatusManager) { @Override CompatUIWindowManager createCompatUiWindowManager(Context context, TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener) { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIStatusManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIStatusManagerTest.java new file mode 100644 index 000000000000..d6059a88e9c7 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIStatusManagerTest.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.compatui; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.testing.AndroidTestingRunner; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.ShellTestCase; + + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.function.IntConsumer; +import java.util.function.IntSupplier; + +/** + * Tests for {@link CompatUILayout}. + * + * Build/Install/Run: + * atest WMShellUnitTests:CompatUIStatusManagerTest + */ +@RunWith(AndroidTestingRunner.class) +@SmallTest +public class CompatUIStatusManagerTest extends ShellTestCase { + + private FakeCompatUIStatusManagerTest mTestState; + private CompatUIStatusManager mStatusManager; + + @Before + public void setUp() { + mTestState = new FakeCompatUIStatusManagerTest(); + mStatusManager = new CompatUIStatusManager(mTestState.mWriter, mTestState.mReader); + } + + @Test + public void isEducationShown() { + assertFalse(mStatusManager.isEducationVisible()); + + mStatusManager.onEducationShown(); + assertTrue(mStatusManager.isEducationVisible()); + + mStatusManager.onEducationHidden(); + assertFalse(mStatusManager.isEducationVisible()); + } + + static class FakeCompatUIStatusManagerTest { + + int mCurrentStatus = 0; + + final IntSupplier mReader = () -> mCurrentStatus; + + final IntConsumer mWriter = newStatus -> mCurrentStatus = newStatus; + + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduWindowManagerTest.java index 7617269cf5d3..94dbd112bb75 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduWindowManagerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduWindowManagerTest.java @@ -20,9 +20,13 @@ import static android.content.res.Configuration.UI_MODE_NIGHT_YES; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.window.flags.Flags.FLAG_APP_COMPAT_UI_FRAMEWORK; +import static com.android.wm.shell.compatui.CompatUIStatusManager.COMPAT_UI_EDUCATION_HIDDEN; +import static com.android.wm.shell.compatui.CompatUIStatusManager.COMPAT_UI_EDUCATION_VISIBLE; import static com.google.common.truth.Truth.assertThat; +import static junit.framework.Assert.assertEquals; + import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; @@ -38,6 +42,7 @@ import android.app.ActivityManager; import android.app.TaskInfo; import android.graphics.Insets; import android.graphics.Rect; +import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.RequiresFlagsDisabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; @@ -54,6 +59,7 @@ import android.view.accessibility.AccessibilityEvent; import androidx.test.filters.SmallTest; +import com.android.window.flags.Flags; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; @@ -61,6 +67,7 @@ import com.android.wm.shell.TestShellExecutor; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.DockStateReader; import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.compatui.CompatUIStatusManagerTest.FakeCompatUIStatusManagerTest; import com.android.wm.shell.transition.Transitions; import org.junit.After; @@ -120,6 +127,8 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { private CompatUIConfiguration mCompatUIConfiguration; private TestShellExecutor mExecutor; + private FakeCompatUIStatusManagerTest mCompatUIStatus; + private CompatUIStatusManager mCompatUIStatusManager; @Rule public final CheckFlagsRule mCheckFlagsRule = @@ -129,6 +138,9 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { public void setUp() { MockitoAnnotations.initMocks(this); mExecutor = new TestShellExecutor(); + mCompatUIStatus = new FakeCompatUIStatusManagerTest(); + mCompatUIStatusManager = new CompatUIStatusManager(mCompatUIStatus.mWriter, + mCompatUIStatus.mReader); mCompatUIConfiguration = new CompatUIConfiguration(mContext, mExecutor) { final Set<Integer> mHasSeenSet = new HashSet<>(); @@ -414,6 +426,21 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { assertFalse(windowManager.needsToBeRecreated(newTaskInfo, mTaskListener)); } + @Test + @EnableFlags(Flags.FLAG_ENABLE_COMPAT_UI_VISIBILITY_STATUS) + public void testCompatUIStatus_dialogIsShown() { + // We display the dialog + LetterboxEduWindowManager windowManager = createWindowManager(/* eligible= */ true, + USER_ID_1, /* isTaskbarEduShowing= */ false); + assertTrue(windowManager.createLayout(/* canShow= */ true)); + assertNotNull(windowManager.mLayout); + assertEquals(/* expected= */ COMPAT_UI_EDUCATION_VISIBLE, mCompatUIStatus.mCurrentStatus); + + // We dismiss + windowManager.release(); + assertEquals(/* expected= */ COMPAT_UI_EDUCATION_HIDDEN, mCompatUIStatus.mCurrentStatus); + } + private void verifyLayout(LetterboxEduDialogLayout layout, ViewGroup.LayoutParams params, int expectedWidth, int expectedHeight, int expectedExtraTopMargin, int expectedExtraBottomMargin) { @@ -464,7 +491,7 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { windowManager = new LetterboxEduWindowManager(mContext, createTaskInfo(eligible, userId), mSyncTransactionQueue, mTaskListener, createDisplayLayout(), mTransitions, mOnDismissCallback, mAnimationController, - mDockStateReader, mCompatUIConfiguration); + mDockStateReader, mCompatUIConfiguration, mCompatUIStatusManager); spyOn(windowManager); doReturn(mViewHost).when(windowManager).createSurfaceViewHost(); doReturn(isTaskbarEduShowing).when(windowManager).isTaskbarEduShowing(); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index 92f705097c33..7bb54498b877 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -731,6 +731,64 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS) + fun addMoveToDesktopChanges_lastWindowSnapLeft_positionResetsToCenter() { + setUpLandscapeDisplay() + val stableBounds = Rect() + displayLayout.getStableBoundsForDesktopMode(stableBounds) + + // Add freeform task with half display size snap bounds at left side. + setUpFreeformTask(bounds = Rect(stableBounds.left, stableBounds.top, 500, stableBounds.bottom)) + + val task = setUpFullscreenTask() + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) + + val finalBounds = findBoundsChange(wct, task) + assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!)) + .isEqualTo(DesktopTaskPosition.Center) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS) + fun addMoveToDesktopChanges_lastWindowSnapRight_positionResetsToCenter() { + setUpLandscapeDisplay() + val stableBounds = Rect() + displayLayout.getStableBoundsForDesktopMode(stableBounds) + + // Add freeform task with half display size snap bounds at right side. + setUpFreeformTask(bounds = Rect( + stableBounds.right - 500, stableBounds.top, stableBounds.right, stableBounds.bottom)) + + val task = setUpFullscreenTask() + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) + + val finalBounds = findBoundsChange(wct, task) + assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!)) + .isEqualTo(DesktopTaskPosition.Center) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS) + fun addMoveToDesktopChanges_lastWindowMaximised_positionResetsToCenter() { + setUpLandscapeDisplay() + val stableBounds = Rect() + displayLayout.getStableBoundsForDesktopMode(stableBounds) + + // Add maximised freeform task. + setUpFreeformTask(bounds = Rect(stableBounds)) + + val task = setUpFullscreenTask() + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) + + val finalBounds = findBoundsChange(wct, task) + assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!)) + .isEqualTo(DesktopTaskPosition.Center) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS) fun addMoveToDesktopChanges_defaultToCenterIfFree() { setUpLandscapeDisplay() val stableBounds = Rect() @@ -751,6 +809,50 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun addMoveToDesktopChanges_landscapeDevice_userFullscreenOverride_defaultPortraitBounds() { + setUpLandscapeDisplay() + val task = setUpFullscreenTask(enableUserFullscreenOverride = true) + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) + + assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun addMoveToDesktopChanges_landscapeDevice_systemFullscreenOverride_defaultPortraitBounds() { + setUpLandscapeDisplay() + val task = setUpFullscreenTask(enableSystemFullscreenOverride = true) + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) + + assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun addMoveToDesktopChanges_portraitDevice_userFullscreenOverride_defaultPortraitBounds() { + setUpPortraitDisplay() + val task = setUpFullscreenTask(enableUserFullscreenOverride = true) + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) + + assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun addMoveToDesktopChanges_portraitDevice_systemFullscreenOverride_defaultPortraitBounds() { + setUpPortraitDisplay() + val task = setUpFullscreenTask(enableSystemFullscreenOverride = true) + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) + + assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS) + } + + @Test fun moveToDesktop_tdaFullscreen_windowingModeSetToFreeform() { val task = setUpFullscreenTask() val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! @@ -1305,13 +1407,36 @@ class DesktopTasksControllerTest : ShellTestCase() { val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() } freeformTasks.forEach { markTaskVisible(it) } val fullscreenTask = createFullscreenTask() + val homeTask = setUpHomeTask(DEFAULT_DISPLAY) val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask)) // Make sure we reorder the new task to top, and the back task to the bottom - assertThat(wct!!.hierarchyOps.size).isEqualTo(2) + assertThat(wct!!.hierarchyOps.size).isEqualTo(3) wct.assertReorderAt(0, fullscreenTask, toTop = true) - wct.assertReorderAt(1, freeformTasks[0], toTop = false) + wct.assertReorderAt(1, homeTask, toTop = false) + wct.assertReorderAt(2, freeformTasks[0], toTop = false) + } + + @Test + fun handleRequest_fullscreenTaskToFreeform_alreadyBeyondLimit_existingAndNewTasksAreMinimized() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val minimizedTask = setUpFreeformTask() + taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = minimizedTask.taskId) + val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() } + freeformTasks.forEach { markTaskVisible(it) } + val homeTask = setUpHomeTask() + val fullscreenTask = createFullscreenTask() + + val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask)) + + assertThat(wct!!.hierarchyOps.size).isEqualTo(4) + wct.assertReorderAt(0, fullscreenTask, toTop = true) + // Make sure we reorder the home task to the bottom, and minimized tasks below the home task. + wct.assertReorderAt(1, homeTask, toTop = false) + wct.assertReorderAt(2, minimizedTask, toTop = false) + wct.assertReorderAt(3, freeformTasks[0], toTop = false) } @Test @@ -2712,13 +2837,15 @@ class DesktopTasksControllerTest : ShellTestCase() { } private fun setUpFullscreenTask( - displayId: Int = DEFAULT_DISPLAY, - isResizable: Boolean = true, - windowingMode: Int = WINDOWING_MODE_FULLSCREEN, - deviceOrientation: Int = ORIENTATION_LANDSCAPE, - screenOrientation: Int = SCREEN_ORIENTATION_UNSPECIFIED, - shouldLetterbox: Boolean = false, - gravity: Int = Gravity.NO_GRAVITY + displayId: Int = DEFAULT_DISPLAY, + isResizable: Boolean = true, + windowingMode: Int = WINDOWING_MODE_FULLSCREEN, + deviceOrientation: Int = ORIENTATION_LANDSCAPE, + screenOrientation: Int = SCREEN_ORIENTATION_UNSPECIFIED, + shouldLetterbox: Boolean = false, + gravity: Int = Gravity.NO_GRAVITY, + enableUserFullscreenOverride: Boolean = false, + enableSystemFullscreenOverride: Boolean = false ): RunningTaskInfo { val task = createFullscreenTask(displayId) val activityInfo = ActivityInfo() @@ -2729,6 +2856,8 @@ class DesktopTasksControllerTest : ShellTestCase() { isResizeable = isResizable configuration.orientation = deviceOrientation configuration.windowConfiguration.windowingMode = windowingMode + appCompatTaskInfo.isUserFullscreenOverrideEnabled = enableUserFullscreenOverride + appCompatTaskInfo.isSystemFullscreenOverrideEnabled = enableSystemFullscreenOverride if (shouldLetterbox) { if (deviceOrientation == ORIENTATION_LANDSCAPE && diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt index e4e2bd216c94..c97bcfb1a4cb 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt @@ -11,6 +11,7 @@ import android.os.IBinder import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper import android.view.SurfaceControl +import android.view.WindowManager.TRANSIT_OPEN import android.window.TransitionInfo import android.window.TransitionInfo.FLAG_IS_WALLPAPER import android.window.WindowContainerTransaction @@ -27,6 +28,7 @@ import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_CANCEL_D import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP import com.android.wm.shell.windowdecor.MoveToDesktopAnimator +import java.util.function.Supplier import junit.framework.Assert.assertFalse import org.junit.Before import org.junit.Test @@ -40,7 +42,6 @@ import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.verifyZeroInteractions import org.mockito.kotlin.whenever -import java.util.function.Supplier /** Tests of [DragToDesktopTransitionHandler]. */ @SmallTest @@ -52,17 +53,26 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { @Mock private lateinit var taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer @Mock private lateinit var splitScreenController: SplitScreenController @Mock private lateinit var dragAnimator: MoveToDesktopAnimator - @Mock - private lateinit var mockInteractionJankMonitor: InteractionJankMonitor + @Mock private lateinit var mockInteractionJankMonitor: InteractionJankMonitor + @Mock private lateinit var draggedTaskLeash: SurfaceControl + @Mock private lateinit var homeTaskLeash: SurfaceControl private val transactionSupplier = Supplier { mock<SurfaceControl.Transaction>() } - private lateinit var handler: DragToDesktopTransitionHandler + private lateinit var defaultHandler: DragToDesktopTransitionHandler + private lateinit var springHandler: SpringDragToDesktopTransitionHandler @Before fun setUp() { - handler = - DragToDesktopTransitionHandler( + defaultHandler = DefaultDragToDesktopTransitionHandler( + context, + transitions, + taskDisplayAreaOrganizer, + mockInteractionJankMonitor, + transactionSupplier, + ) + .apply { setSplitScreenController(splitScreenController) } + springHandler = SpringDragToDesktopTransitionHandler( context, transitions, taskDisplayAreaOrganizer, @@ -76,10 +86,10 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { fun startDragToDesktop_animateDragWhenReady() { val task = createTask() // Simulate transition is started. - val transition = startDragToDesktopTransition(task, dragAnimator) + val transition = startDragToDesktopTransition(defaultHandler, task, dragAnimator) // Now it's ready to animate. - handler.startAnimation( + defaultHandler.startAnimation( transition = transition, info = createTransitionInfo( @@ -96,65 +106,70 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { @Test fun startDragToDesktop_cancelledBeforeReady_startCancelTransition() { - performEarlyCancel(DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL) + performEarlyCancel( + defaultHandler, + DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL + ) verify(transitions) - .startTransition(eq(TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP), any(), eq(handler)) + .startTransition( + eq(TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP), + any(), + eq(defaultHandler) + ) } @Test fun startDragToDesktop_cancelledBeforeReady_verifySplitLeftCancel() { - performEarlyCancel(DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_LEFT) - verify(splitScreenController).requestEnterSplitSelect( - any(), - any(), - eq(SPLIT_POSITION_TOP_OR_LEFT), - any() + performEarlyCancel( + defaultHandler, + DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_LEFT ) + verify(splitScreenController) + .requestEnterSplitSelect(any(), any(), eq(SPLIT_POSITION_TOP_OR_LEFT), any()) } @Test fun startDragToDesktop_cancelledBeforeReady_verifySplitRightCancel() { - performEarlyCancel(DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_RIGHT) - verify(splitScreenController).requestEnterSplitSelect( - any(), - any(), - eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), - any() + performEarlyCancel( + defaultHandler, + DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_RIGHT ) + verify(splitScreenController) + .requestEnterSplitSelect(any(), any(), eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), any()) } @Test fun startDragToDesktop_aborted_finishDropped() { val task = createTask() // Simulate transition is started. - val transition = startDragToDesktopTransition(task, dragAnimator) + val transition = startDragToDesktopTransition(defaultHandler, task, dragAnimator) // But the transition was aborted. - handler.onTransitionConsumed(transition, aborted = true, mock()) + defaultHandler.onTransitionConsumed(transition, aborted = true, mock()) // Attempt to finish the failed drag start. - handler.finishDragToDesktopTransition(WindowContainerTransaction()) + defaultHandler.finishDragToDesktopTransition(WindowContainerTransaction()) // Should not be attempted and state should be reset. verify(transitions, never()) - .startTransition(eq(TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP), any(), any()) - assertFalse(handler.inProgress) + .startTransition(eq(TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP), any(), any()) + assertFalse(defaultHandler.inProgress) } @Test fun startDragToDesktop_aborted_cancelDropped() { val task = createTask() // Simulate transition is started. - val transition = startDragToDesktopTransition(task, dragAnimator) + val transition = startDragToDesktopTransition(defaultHandler, task, dragAnimator) // But the transition was aborted. - handler.onTransitionConsumed(transition, aborted = true, mock()) + defaultHandler.onTransitionConsumed(transition, aborted = true, mock()) // Attempt to finish the failed drag start. - handler.cancelDragToDesktopTransition( + defaultHandler.cancelDragToDesktopTransition( DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL ) // Should not be attempted and state should be reset. - assertFalse(handler.inProgress) + assertFalse(defaultHandler.inProgress) } @Test @@ -162,23 +177,24 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { val task = createTask() // Simulate attempt to start two drag to desktop transitions. - startDragToDesktopTransition(task, dragAnimator) - startDragToDesktopTransition(task, dragAnimator) + startDragToDesktopTransition(defaultHandler, task, dragAnimator) + startDragToDesktopTransition(defaultHandler, task, dragAnimator) // Verify transition only started once. - verify(transitions, times(1)).startTransition( + verify(transitions, times(1)) + .startTransition( eq(TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP), any(), - eq(handler) - ) + eq(defaultHandler) + ) } @Test fun cancelDragToDesktop_startWasReady_cancel() { - startDrag() + startDrag(defaultHandler) // Then user cancelled after it had already started. - handler.cancelDragToDesktopTransition( + defaultHandler.cancelDragToDesktopTransition( DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL ) @@ -188,48 +204,40 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { @Test fun cancelDragToDesktop_splitLeftCancelType_splitRequested() { - startDrag() + startDrag(defaultHandler) // Then user cancelled it, requesting split. - handler.cancelDragToDesktopTransition( + defaultHandler.cancelDragToDesktopTransition( DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_LEFT ) // Verify the request went through split controller. - verify(splitScreenController).requestEnterSplitSelect( - any(), - any(), - eq(SPLIT_POSITION_TOP_OR_LEFT), - any() - ) + verify(splitScreenController) + .requestEnterSplitSelect(any(), any(), eq(SPLIT_POSITION_TOP_OR_LEFT), any()) } @Test fun cancelDragToDesktop_splitRightCancelType_splitRequested() { - startDrag() + startDrag(defaultHandler) // Then user cancelled it, requesting split. - handler.cancelDragToDesktopTransition( + defaultHandler.cancelDragToDesktopTransition( DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_RIGHT ) // Verify the request went through split controller. - verify(splitScreenController).requestEnterSplitSelect( - any(), - any(), - eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), - any() - ) + verify(splitScreenController) + .requestEnterSplitSelect(any(), any(), eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), any()) } @Test fun cancelDragToDesktop_startWasNotReady_animateCancel() { val task = createTask() // Simulate transition is started and is ready to animate. - startDragToDesktopTransition(task, dragAnimator) + startDragToDesktopTransition(defaultHandler, task, dragAnimator) // Then user cancelled before the transition was ready and animated. - handler.cancelDragToDesktopTransition( + defaultHandler.cancelDragToDesktopTransition( DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL ) @@ -240,50 +248,139 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { @Test fun cancelDragToDesktop_transitionNotInProgress_dropCancel() { // Then cancel is called before the transition was started. - handler.cancelDragToDesktopTransition( + defaultHandler.cancelDragToDesktopTransition( DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL ) // Verify cancel is dropped. - verify(transitions, never()).startTransition( + verify(transitions, never()) + .startTransition( eq(TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP), any(), - eq(handler) - ) + eq(defaultHandler) + ) } @Test fun finishDragToDesktop_transitionNotInProgress_dropFinish() { // Then finish is called before the transition was started. - handler.finishDragToDesktopTransition(WindowContainerTransaction()) + defaultHandler.finishDragToDesktopTransition(WindowContainerTransaction()) // Verify finish is dropped. - verify(transitions, never()).startTransition( + verify(transitions, never()) + .startTransition( eq(TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP), any(), - eq(handler) + eq(defaultHandler) + ) + } + + @Test + fun mergeAnimation_otherTransition_doesNotMerge() { + val transaction = mock<SurfaceControl.Transaction>() + val finishCallback = mock<Transitions.TransitionFinishCallback>() + val task = createTask() + + startDrag(defaultHandler, task) + defaultHandler.mergeAnimation( + transition = mock(), + info = createTransitionInfo(type = TRANSIT_OPEN, draggedTask = task), + t = transaction, + mergeTarget = mock(), + finishCallback = finishCallback ) + + // Should NOT have any transaction changes + verifyZeroInteractions(transaction) + // Should NOT merge animation + verify(finishCallback, never()).onTransitionFinished(any()) } - private fun startDrag() { + @Test + fun mergeAnimation_endTransition_mergesAnimation() { + val playingFinishTransaction = mock<SurfaceControl.Transaction>() + val mergedStartTransaction = mock<SurfaceControl.Transaction>() + val finishCallback = mock<Transitions.TransitionFinishCallback>() val task = createTask() + val startTransition = + startDrag(defaultHandler, task, finishTransaction = playingFinishTransaction) + defaultHandler.onTaskResizeAnimationListener = mock() + + defaultHandler.mergeAnimation( + transition = mock(), + info = + createTransitionInfo( + type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, + draggedTask = task + ), + t = mergedStartTransaction, + mergeTarget = startTransition, + finishCallback = finishCallback + ) + + // Should show dragged task layer in start and finish transaction + verify(mergedStartTransaction).show(draggedTaskLeash) + verify(playingFinishTransaction).show(draggedTaskLeash) + // Should merge animation + verify(finishCallback).onTransitionFinished(null) + } + + @Test + fun mergeAnimation_endTransition_springHandler_hidesHome() { + whenever(dragAnimator.computeCurrentVelocity()).thenReturn(PointF()) + val playingFinishTransaction = mock<SurfaceControl.Transaction>() + val mergedStartTransaction = mock<SurfaceControl.Transaction>() + val finishCallback = mock<Transitions.TransitionFinishCallback>() + val task = createTask() + val startTransition = + startDrag(springHandler, task, finishTransaction = playingFinishTransaction) + springHandler.onTaskResizeAnimationListener = mock() + + springHandler.mergeAnimation( + transition = mock(), + info = + createTransitionInfo( + type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, + draggedTask = task + ), + t = mergedStartTransaction, + mergeTarget = startTransition, + finishCallback = finishCallback + ) + + // Should show dragged task layer in start and finish transaction + verify(mergedStartTransaction).show(draggedTaskLeash) + verify(playingFinishTransaction).show(draggedTaskLeash) + // Should hide home task leash in finish transaction + verify(playingFinishTransaction).hide(homeTaskLeash) + // Should merge animation + verify(finishCallback).onTransitionFinished(null) + } + + private fun startDrag( + handler: DragToDesktopTransitionHandler, + task: RunningTaskInfo = createTask(), + finishTransaction: SurfaceControl.Transaction = mock() + ): IBinder { whenever(dragAnimator.position).thenReturn(PointF()) // Simulate transition is started and is ready to animate. - val transition = startDragToDesktopTransition(task, dragAnimator) + val transition = startDragToDesktopTransition(handler, task, dragAnimator) handler.startAnimation( transition = transition, info = - createTransitionInfo( - type = TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP, - draggedTask = task - ), + createTransitionInfo( + type = TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP, + draggedTask = task + ), startTransaction = mock(), - finishTransaction = mock(), + finishTransaction = finishTransaction, finishCallback = {} ) + return transition } private fun startDragToDesktopTransition( + handler: DragToDesktopTransitionHandler, task: RunningTaskInfo, dragAnimator: MoveToDesktopAnimator ): IBinder { @@ -300,20 +397,23 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { return token } - private fun performEarlyCancel(cancelState: DragToDesktopTransitionHandler.CancelState) { + private fun performEarlyCancel( + handler: DragToDesktopTransitionHandler, + cancelState: DragToDesktopTransitionHandler.CancelState + ) { val task = createTask() // Simulate transition is started and is ready to animate. - val transition = startDragToDesktopTransition(task, dragAnimator) + val transition = startDragToDesktopTransition(handler, task, dragAnimator) handler.cancelDragToDesktopTransition(cancelState) handler.startAnimation( transition = transition, info = - createTransitionInfo( - type = TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP, - draggedTask = task - ), + createTransitionInfo( + type = TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP, + draggedTask = task + ), startTransaction = mock(), finishTransaction = mock(), finishCallback = {} @@ -340,7 +440,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { private fun createTransitionInfo(type: Int, draggedTask: RunningTaskInfo): TransitionInfo { return TransitionInfo(type, 0 /* flags */).apply { addChange( // Home. - TransitionInfo.Change(mock(), mock()).apply { + TransitionInfo.Change(mock(), homeTaskLeash).apply { parent = null taskInfo = TestRunningTaskInfoBuilder().setActivityType(ACTIVITY_TYPE_HOME).build() @@ -348,7 +448,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { } ) addChange( // Dragged Task. - TransitionInfo.Change(mock(), mock()).apply { + TransitionInfo.Change(mock(), draggedTaskLeash).apply { parent = null taskInfo = draggedTask } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/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..6d68797b4430 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,7 +55,6 @@ 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 androidx.test.filters.SmallTest @@ -85,11 +83,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 +170,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 +224,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,26 +356,6 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { } @Test - fun testCaptionIsNotCreatedWhenKeyguardIsVisible() { - val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN, focused = true) - val keyguardListenerCaptor = argumentCaptor<KeyguardChangeListener>() - verify(mockShellController).addKeyguardChangeListener(keyguardListenerCaptor.capture()) - - keyguardListenerCaptor.firstValue.onKeyguardVisibilityChanged( - true /* visible */, - true /* occluded */, - false /* animatingDismiss */ - ) - onTaskOpening(task) - - task.setWindowingMode(WINDOWING_MODE_UNDEFINED) - task.setWindowingMode(ACTIVITY_TYPE_UNDEFINED) - onTaskChanging(task) - - assertFalse(windowDecorByTaskIdSpy.contains(task.taskId)) - } - - @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) fun testDecorationIsCreatedForTopTranslucentActivitiesWithStyleFloating() { val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN, focused = true).apply { @@ -418,67 +400,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/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..7124ed2d96b8 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java @@ -261,9 +261,9 @@ public class CsipDeviceManager { } CachedBluetoothDevice dualModeDevice = groupDevicesList.stream() - .filter(cachedDevice -> cachedDevice.getConnectableProfiles().stream() + .filter(cachedDevice -> cachedDevice.getUiAccessibleProfiles().stream() .anyMatch(profile -> profile instanceof LeAudioProfile)) - .filter(cachedDevice -> cachedDevice.getConnectableProfiles().stream() + .filter(cachedDevice -> cachedDevice.getUiAccessibleProfiles().stream() .anyMatch(profile -> profile instanceof A2dpProfile || profile instanceof HeadsetProfile)) .findFirst().orElse(null); diff --git a/packages/SettingsLib/src/com/android/settingslib/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/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/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java index 3f59da4bf24e..f94f21fe5d45 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java @@ -145,18 +145,18 @@ public class CsipDeviceManagerTest { profiles.add(mHfpProfile); profiles.add(mA2dpProfile); profiles.add(mLeAudioProfile); - when(mCachedDevice1.getConnectableProfiles()).thenReturn(profiles); + when(mCachedDevice1.getUiAccessibleProfiles()).thenReturn(profiles); when(mCachedDevice1.isConnected()).thenReturn(true); profiles.clear(); profiles.add(mLeAudioProfile); - when(mCachedDevice2.getConnectableProfiles()).thenReturn(profiles); + when(mCachedDevice2.getUiAccessibleProfiles()).thenReturn(profiles); when(mCachedDevice2.isConnected()).thenReturn(true); profiles.clear(); profiles.add(mHfpProfile); profiles.add(mA2dpProfile); - when(mCachedDevice3.getConnectableProfiles()).thenReturn(profiles); + when(mCachedDevice3.getUiAccessibleProfiles()).thenReturn(profiles); when(mCachedDevice3.isConnected()).thenReturn(true); } @@ -253,7 +253,7 @@ public class CsipDeviceManagerTest { when(mDevice2.isConnected()).thenReturn(false); List<LocalBluetoothProfile> profiles = new ArrayList<LocalBluetoothProfile>(); profiles.add(mLeAudioProfile); - when(mCachedDevice1.getConnectableProfiles()).thenReturn(profiles); + when(mCachedDevice1.getUiAccessibleProfiles()).thenReturn(profiles); CachedBluetoothDevice expectedDevice = mCachedDevice1; assertThat( diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/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 23c6098e1114..e6fae7b588ce 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -475,18 +475,6 @@ flag { } flag { - name: "centralized_status_bar_height_fix" - namespace: "systemui" - description: "Refactors shade header and keyguard status bar to read status bar dimens from a" - " central place, instead of reading resources directly. This is to take into account display" - " cutouts and other special cases. " - bug: "317016114" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { name: "enable_layout_tracing" namespace: "systemui" description: "Enables detailed traversal slices during measure and layout in perfetto traces" @@ -1209,6 +1197,17 @@ flag { } flag { + name: "hubmode_fullscreen_vertical_swipe_fix" + namespace: "systemui" + description: "Bug fix that enables fullscreen vertical swiping in hub mode to bring up and down the bouncer and shade" + bug: "340177049" + metadata { + purpose: PURPOSE_BUGFIX + } +} + + +flag { namespace: "systemui" name: "remove_update_listener_in_qs_icon_view_impl" description: "Remove update listeners in QsIconViewImpl class to avoid memory leak." diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterContentObserverSyncViaSettingsProxyDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterContentObserverSyncViaSettingsProxyDetector.kt new file mode 100644 index 000000000000..d8c7c06c8c5b --- /dev/null +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterContentObserverSyncViaSettingsProxyDetector.kt @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.systemui.lint + +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Implementation +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.Scope +import com.android.tools.lint.detector.api.Severity +import com.android.tools.lint.detector.api.SourceCodeScanner +import com.intellij.psi.PsiMethod +import org.jetbrains.uast.UCallExpression + +/** + * Checks if the synchronous APIs like registerContentObserverSync/unregisterContentObserverSync are + * invoked for SettingsProxy or it's sub-classes, and raise a warning notifying the caller to use + * the asynchronous/suspend APIs instead. + */ +@Suppress("UnstableApiUsage") +class RegisterContentObserverSyncViaSettingsProxyDetector : Detector(), SourceCodeScanner { + + override fun getApplicableMethodNames(): List<String> { + return SYNC_METHOD_LIST + } + + override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) { + + val evaluator = context.evaluator + if (evaluator.isMemberInSubClassOf(method, SETTINGS_PROXY_CLASS)) { + context.report( + issue = SYNC_WARNING, + location = context.getNameLocation(node), + message = + "`Avoid using ${method.name}()` if calling the API is not " + + "required on the main thread. Instead use an appropriate async interface " + + "API call for eg. `registerContentObserver()` or " + + "`registerContentObserverAsync()`." + ) + } + } + + companion object { + val SYNC_WARNING: Issue = + Issue.create( + id = "RegisterContentObserverSyncWarning", + briefDescription = + "Synchronous content observer registration API called " + + "instead of the async APIs.`", + // lint trims indents and converts \ to line continuations + explanation = + """ + ContentObserver registration/de-registration done via \ + `SettingsProxy.registerContentObserverSync` will block the main thread \ + and may cause missed frames. Instead, use \ + `SettingsProxy.registerContentObserver()` or \ + `SettingsProxy.registerContentObserverAsync()`. These APIs will ensure \ + that the registrations/de-registrations happen sequentially on a + background worker thread.""", + category = Category.PERFORMANCE, + priority = 8, + severity = Severity.WARNING, + implementation = + Implementation( + RegisterContentObserverSyncViaSettingsProxyDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) + ) + + private val SYNC_METHOD_LIST = + listOf( + "registerContentObserverSync", + "unregisterContentObserverSync", + "registerContentObserverForUserSync" + ) + + private val SETTINGS_PROXY_CLASS = "com.android.systemui.util.settings.SettingsProxy" + } +} diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt index 73ac6ccf8f76..5206b05a3f4e 100644 --- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt @@ -46,10 +46,12 @@ class SystemUIIssueRegistry : IssueRegistry() { DemotingTestWithoutBugDetector.ISSUE, TestFunctionNameViolationDetector.ISSUE, MissingApacheLicenseDetector.ISSUE, + RegisterContentObserverSyncViaSettingsProxyDetector.SYNC_WARNING ) override val api: Int get() = CURRENT_API + override val minApi: Int get() = 8 diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterContentObserverSyncViaSettingsProxyDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterContentObserverSyncViaSettingsProxyDetectorTest.kt new file mode 100644 index 000000000000..57347d351543 --- /dev/null +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterContentObserverSyncViaSettingsProxyDetectorTest.kt @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.internal.systemui.lint + +import com.android.tools.lint.checks.infrastructure.TestFiles +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Issue +import org.junit.Test + +/** Test class for [RegisterContentObserverSyncViaSettingsProxyDetector]. */ +class RegisterContentObserverSyncViaSettingsProxyDetectorTest : SystemUILintDetectorTest() { + override fun getDetector(): Detector = RegisterContentObserverSyncViaSettingsProxyDetector() + + override fun getIssues(): List<Issue> = + listOf(RegisterContentObserverSyncViaSettingsProxyDetector.SYNC_WARNING) + + @Test + fun testRegisterContentObserverSync_throwError() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import com.android.systemui.util.settings.SecureSettings; + public class TestClass { + public void register(SecureSettings secureSettings) { + secureSettings. + registerContentObserverSync(Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED), + false, mSettingObserver); + } + } + """ + ) + .indented(), + *stubs + ) + .issues(RegisterContentObserverSyncViaSettingsProxyDetector.SYNC_WARNING) + .run() + .expect( + """ + src/test/pkg/TestClass.java:6: Warning: Avoid using registerContentObserverSync() if calling the API is not required on the main thread. Instead use an appropriate async interface API call for eg. registerContentObserver() or registerContentObserverAsync(). [RegisterContentObserverSyncWarning] + registerContentObserverSync(Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~ +0 errors, 1 warnings + """ + .trimIndent() + ) + } + + @Test + fun testRegisterContentObserverForUserSync_throwError() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import com.android.systemui.util.settings.SecureSettings; + public class TestClass { + public void register(SecureSettings secureSettings) { + secureSettings. + registerContentObserverForUserSync(Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED), + false, mSettingObserver); + } + } + """ + ) + .indented(), + *stubs + ) + .issues(RegisterContentObserverSyncViaSettingsProxyDetector.SYNC_WARNING) + .run() + .expect( + """ + src/test/pkg/TestClass.java:6: Warning: Avoid using registerContentObserverForUserSync() if calling the API is not required on the main thread. Instead use an appropriate async interface API call for eg. registerContentObserver() or registerContentObserverAsync(). [RegisterContentObserverSyncWarning] + registerContentObserverForUserSync(Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +0 errors, 1 warnings + """ + .trimIndent() + ) + } + + @Test + fun testSuppressRegisterContentObserverSync() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import com.android.systemui.util.settings.SecureSettings; + public class TestClass { + @SuppressWarnings("RegisterContentObserverSyncWarning") + public void register(SecureSettings secureSettings) { + secureSettings. + registerContentObserverForUserSync(Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED), + false, mSettingObserver); + } + } + """ + ) + .indented(), + *stubs + ) + .issues(RegisterContentObserverSyncViaSettingsProxyDetector.SYNC_WARNING) + .run() + .expectClean() + } + + @Test + fun testNoopIfNoCall() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import com.android.systemui.util.settings.SecureSettings; + public class TestClass { + public void register(SecureSettings secureSettings) { + } + } + """ + ) + .indented(), + *stubs + ) + .issues(RegisterContentObserverSyncViaSettingsProxyDetector.SYNC_WARNING) + .run() + .expectClean() + } + + @Test + fun testUnRegisterContentObserverSync_throwError() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import com.android.systemui.util.settings.SecureSettings; + public class TestClass { + public void register(SecureSettings secureSettings) { + secureSettings. + unregisterContentObserverSync(mSettingObserver); + } + } + """ + ) + .indented(), + *stubs + ) + .issues(RegisterContentObserverSyncViaSettingsProxyDetector.SYNC_WARNING) + .run() + .expect( + """ + src/test/pkg/TestClass.java:6: Warning: Avoid using unregisterContentObserverSync() if calling the API is not required on the main thread. Instead use an appropriate async interface API call for eg. registerContentObserver() or registerContentObserverAsync(). [RegisterContentObserverSyncWarning] + unregisterContentObserverSync(mSettingObserver); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +0 errors, 1 warnings + """ + .trimIndent() + ) + } + + private companion object { + private val SETTINGS_PROXY_STUB = + kotlin( + """ + package com.android.systemui.util.settings + interface SettingsProxy { + fun registerContentObserverSync() {} + fun unregisterContentObserverSync() {} + } + """ + ) + .indented() + + private val USER_SETTINGS_PROXY_STUB = + kotlin( + """ + package com.android.systemui.util.settings + interface UserSettingsProxy : SettingsProxy { + fun registerContentObserverForUserSync() {} + } + """ + ) + .indented() + + private val SECURE_SETTINGS_STUB = + kotlin( + """ + package com.android.systemui.util.settings + interface SecureSettings : UserSettingsProxy {} + """ + ) + .indented() + } + + private val stubs = arrayOf(SETTINGS_PROXY_STUB, USER_SETTINGS_PROXY_STUB, SECURE_SETTINGS_STUB) +} diff --git a/packages/SystemUI/compose/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/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/PredictiveBackHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt index 0105af3377fd..fe16ef75118b 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt @@ -127,16 +127,7 @@ private class PredictiveBackTransition( return coroutineScope .launch(start = CoroutineStart.ATOMIC) { try { - if (currentScene == toScene) { - animatable.animateTo(targetProgress, transformationSpec.progressSpec) - } else { - // If the back gesture is cancelled, the progress is animated back to 0f by - // the system. But we need this animate call anyways because - // PredictiveBackHandler doesn't guarantee that it ends at 0f. Since the - // remaining change in progress is usually very small, the progressSpec is - // omitted and the default spring spec used instead. - animatable.animateTo(targetProgress) - } + animatable.animateTo(targetProgress) } finally { state.finishTransition(this@PredictiveBackTransition, scene) } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt index c414fbe1c2db..0eaecb09e97e 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt @@ -18,8 +18,6 @@ package com.android.compose.animation.scene import androidx.activity.BackEventCompat import androidx.activity.ComponentActivity -import androidx.compose.animation.core.LinearEasing -import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.rememberCoroutineScope @@ -61,23 +59,7 @@ class PredictiveBackHandlerTest { @Test fun testPredictiveBack() { - val transitionFrames = 2 - val layoutState = - rule.runOnUiThread { - MutableSceneTransitionLayoutState( - SceneA, - transitions = - transitions { - from(SceneA, to = SceneB) { - spec = - tween( - durationMillis = transitionFrames * 16, - easing = LinearEasing - ) - } - } - ) - } + val layoutState = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) } rule.setContent { SceneTransitionLayout(layoutState) { scene(SceneA, mapOf(Back to SceneB)) { Box(Modifier.fillMaxSize()) } @@ -106,27 +88,12 @@ class PredictiveBackHandlerTest { assertThat(layoutState.transitionState).hasCurrentScene(SceneA) assertThat(layoutState.transitionState).isIdle() - rule.mainClock.autoAdvance = false - // Start again and commit it. rule.runOnUiThread { dispatcher.dispatchOnBackStarted(backEvent()) dispatcher.dispatchOnBackProgressed(backEvent(progress = 0.4f)) dispatcher.onBackPressed() } - rule.mainClock.advanceTimeByFrame() - rule.waitForIdle() - val transition2 = assertThat(layoutState.transitionState).isTransition() - // verify that transition picks up progress from preview - assertThat(transition2).hasProgress(0.4f, tolerance = 0.0001f) - - rule.mainClock.advanceTimeByFrame() - rule.waitForIdle() - // verify that transition is half way between preview-end-state (0.4f) and target-state (1f) - // after one frame - assertThat(transition2).hasProgress(0.7f, tolerance = 0.0001f) - - rule.mainClock.autoAdvance = true rule.waitForIdle() assertThat(layoutState.transitionState).hasCurrentScene(SceneB) assertThat(layoutState.transitionState).isIdle() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/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/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/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/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/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 8bad97104975..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> @@ -3679,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] --> @@ -3687,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/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..b6fe0df5a514 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java @@ -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/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..096556fed258 100644 --- a/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt +++ b/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt @@ -18,10 +18,10 @@ 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 @@ -42,7 +42,7 @@ import kotlinx.coroutines.SupervisorJob interface ContextualEducationModule { @Binds fun bindContextualEducationRepository( - impl: ContextualEducationRepositoryImpl + impl: UserContextualEducationRepository ): ContextualEducationRepository @Qualifier annotation class EduDataStoreScope 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/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/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/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/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/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..108564ca080e 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java @@ -63,7 +63,9 @@ public class RecordingService extends Service implements ScreenMediaRecorderList protected static final int NOTIF_BASE_ID = 4273; private static final String TAG = "RecordingService"; private static final String CHANNEL_ID = "screen_record"; - private static final String GROUP_KEY = "screen_record_saved"; + @VisibleForTesting static final String GROUP_KEY_SAVED = "screen_record_saved"; + private static final String GROUP_KEY_ERROR_STARTING = "screen_record_error_starting"; + @VisibleForTesting static final String GROUP_KEY_ERROR_SAVING = "screen_record_error_saving"; private static final String EXTRA_RESULT_CODE = "extra_resultCode"; protected static final String EXTRA_PATH = "extra_path"; private static final String EXTRA_AUDIO_SOURCE = "extra_useAudio"; @@ -181,7 +183,7 @@ public class RecordingService extends Service implements ScreenMediaRecorderList mUiEventLogger.log(Events.ScreenRecordEvent.SCREEN_RECORD_START); } else { updateState(false); - createErrorNotification(); + createErrorStartingNotification(currentUser); stopForeground(STOP_FOREGROUND_DETACH); stopSelf(); return Service.START_NOT_STICKY; @@ -272,17 +274,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 +357,7 @@ public class RecordingService extends Service implements ScreenMediaRecorderList .setContentText( strings().getBackgroundProcessingLabel()) .setSmallIcon(R.drawable.ic_screenrecord) - .setGroup(GROUP_KEY) + .setGroup(GROUP_KEY_SAVED) .addExtras(extras); return builder.build(); } @@ -374,7 +394,7 @@ public class RecordingService extends Service implements ScreenMediaRecorderList PendingIntent.FLAG_IMMUTABLE)) .addAction(shareAction) .setAutoCancel(true) - .setGroup(GROUP_KEY) + .setGroup(GROUP_KEY_SAVED) .addExtras(extras); // Add thumbnail if available @@ -389,21 +409,28 @@ public class RecordingService extends Service implements ScreenMediaRecorderList } /** - * Adds a group notification so that save notifications from multiple recordings are - * grouped together, and the foreground service recording notification is not + * Adds a group summary notification for save notifications so that save notifications from + * multiple recordings are grouped together, and the foreground service recording notification + * is not. */ - private void postGroupNotification(UserHandle currentUser) { + private void postGroupSummaryNotificationForSaves(UserHandle currentUser) { + postGroupSummaryNotification(currentUser, strings().getSaveTitle(), GROUP_KEY_SAVED); + } + + /** Posts a group summary notification for the given group. */ + private void postGroupSummaryNotification( + UserHandle currentUser, String notificationContentTitle, String groupKey) { Bundle extras = new Bundle(); extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, strings().getTitle()); Notification groupNotif = new Notification.Builder(this, getChannelId()) .setSmallIcon(R.drawable.ic_screenrecord) - .setContentTitle(strings().getSaveTitle()) - .setGroup(GROUP_KEY) + .setContentTitle(notificationContentTitle) + .setGroup(groupKey) .setGroupSummary(true) .setExtras(extras) .build(); - mNotificationManager.notifyAsUser(getTag(), NOTIF_BASE_ID, groupNotif, currentUser); + mNotificationManager.notifyAsUser(getTag(), mNotificationId, groupNotif, currentUser); } private void stopService() { @@ -414,6 +441,7 @@ public class RecordingService extends Service implements ScreenMediaRecorderList if (userId == USER_ID_NOT_SPECIFIED) { userId = mUserContextTracker.getUserContext().getUserId(); } + UserHandle currentUser = new UserHandle(userId); Log.d(getTag(), "notifying for user " + userId); setTapsVisible(mOriginalShowTaps); try { @@ -427,11 +455,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 +483,7 @@ public class RecordingService extends Service implements ScreenMediaRecorderList Log.d(getTag(), "saving recording"); Notification notification = createSaveNotification( getRecorder() != null ? getRecorder().save() : null); - postGroupNotification(currentUser); + postGroupSummaryNotificationForSaves(currentUser); mNotificationManager.notifyAsUser(null, mNotificationId, notification, currentUser); } catch (IOException | IllegalStateException e) { diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java index 540d4c43c58d..7b802a2a40aa 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java @@ -17,7 +17,6 @@ package com.android.systemui.screenshot; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; -import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT; import static com.android.systemui.Flags.screenshotSaveImageExporter; import static com.android.systemui.screenshot.LogConfig.DEBUG_ANIM; @@ -31,7 +30,6 @@ import static com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_INTERAC import android.animation.Animator; import android.animation.AnimatorListenerAdapter; -import android.annotation.MainThread; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.BroadcastReceiver; @@ -52,17 +50,12 @@ import android.util.DisplayMetrics; import android.util.Log; import android.view.Display; import android.view.ScrollCaptureResponse; -import android.view.View; -import android.view.ViewGroup; import android.view.ViewRootImpl; -import android.view.ViewTreeObserver; -import android.view.WindowInsets; import android.view.WindowManager; import android.widget.Toast; import android.window.WindowContext; import com.android.internal.logging.UiEventLogger; -import com.android.internal.policy.PhoneWindow; import com.android.settingslib.applications.InterestingConfigChanges; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.broadcast.BroadcastSender; @@ -115,11 +108,9 @@ public class ScreenshotController implements InteractiveScreenshotHandler { private final BroadcastDispatcher mBroadcastDispatcher; private final ScreenshotActionsController mActionsController; - private final WindowManager mWindowManager; - private final WindowManager.LayoutParams mWindowLayoutParams; @Nullable private final ScreenshotSoundController mScreenshotSoundController; - private final PhoneWindow mWindow; + private final ScreenshotWindow mWindow; private final Display mDisplay; private final ScrollCaptureExecutor mScrollCaptureExecutor; private final ScreenshotNotificationSmartActionsProvider @@ -135,8 +126,6 @@ public class ScreenshotController implements InteractiveScreenshotHandler { private Bitmap mScreenBitmap; private SaveImageInBackgroundTask mSaveInBgTask; private boolean mScreenshotTakenInPortrait; - private boolean mAttachRequested; - private boolean mDetachRequested; private Animator mScreenshotAnimation; private RequestCallback mCurrentRequestCallback; private String mPackageName = ""; @@ -155,7 +144,7 @@ public class ScreenshotController implements InteractiveScreenshotHandler { @AssistedInject ScreenshotController( Context context, - WindowManager windowManager, + ScreenshotWindow.Factory screenshotWindowFactory, FeatureFlags flags, ScreenshotShelfViewProxy.Factory viewProxyFactory, ScreenshotSmartActions screenshotSmartActions, @@ -195,9 +184,8 @@ public class ScreenshotController implements InteractiveScreenshotHandler { mScreenshotHandler.setDefaultTimeoutMillis(SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS); mDisplay = display; - mWindowManager = windowManager; - final Context displayContext = context.createDisplayContext(display); - mContext = (WindowContext) displayContext.createWindowContext(TYPE_SCREENSHOT, null); + mWindow = screenshotWindowFactory.create(mDisplay); + mContext = mWindow.getContext(); mFlags = flags; mUserManager = userManager; mMessageContainerController = messageContainerController; @@ -213,17 +201,10 @@ public class ScreenshotController implements InteractiveScreenshotHandler { mViewProxy.requestDismissal(SCREENSHOT_INTERACTION_TIMEOUT); }); - // Setup the window that we are going to use - mWindowLayoutParams = FloatingWindowUtil.getFloatingWindowParams(); - mWindowLayoutParams.setTitle("ScreenshotAnimation"); - - mWindow = FloatingWindowUtil.getFloatingWindow(mContext); - mWindow.setWindowManager(mWindowManager, null, null); - mConfigChanges.applyNewConfig(context.getResources()); reloadAssets(); - mActionExecutor = actionExecutorFactory.create(mWindow, mViewProxy, + mActionExecutor = actionExecutorFactory.create(mWindow.getWindow(), mViewProxy, () -> { finishDismiss(); return Unit.INSTANCE; @@ -318,12 +299,12 @@ public class ScreenshotController implements InteractiveScreenshotHandler { } // The window is focusable by default - setWindowFocusable(true); + mWindow.setFocusable(true); mViewProxy.requestFocus(); enqueueScrollCaptureRequest(requestId, screenshot.getUserHandle()); - attachWindow(); + mWindow.attachWindow(); boolean showFlash; if (screenshot.getType() == WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE) { @@ -347,13 +328,10 @@ public class ScreenshotController implements InteractiveScreenshotHandler { mViewProxy.setScreenshot(screenshot); - // ignore system bar insets for the purpose of window layout - mWindow.getDecorView().setOnApplyWindowInsetsListener( - (v, insets) -> WindowInsets.CONSUMED); } void prepareViewForNewScreenshot(@NonNull ScreenshotData screenshot, String oldPackageName) { - withWindowAttached(() -> { + mWindow.whenWindowAttached(() -> { mAnnouncementResolver.getScreenshotAnnouncement( screenshot.getUserHandle().getIdentifier(), announcement -> { @@ -444,7 +422,7 @@ public class ScreenshotController implements InteractiveScreenshotHandler { @Override public void onTouchOutside() { // TODO(159460485): Remove this when focus is handled properly in the system - setWindowFocusable(false); + mWindow.setFocusable(false); } }); @@ -457,9 +435,9 @@ public class ScreenshotController implements InteractiveScreenshotHandler { private void enqueueScrollCaptureRequest(UUID requestId, UserHandle owner) { // Wait until this window is attached to request because it is // the reference used to locate the target window (below). - withWindowAttached(() -> { + mWindow.whenWindowAttached(() -> { requestScrollCapture(requestId, owner); - mWindow.peekDecorView().getViewRootImpl().setActivityConfigCallback( + mWindow.setActivityConfigCallback( new ViewRootImpl.ActivityConfigCallback() { @Override public void onConfigurationChanged(Configuration overrideConfig, @@ -472,8 +450,7 @@ public class ScreenshotController implements InteractiveScreenshotHandler { // to set up in the new orientation. mScreenshotHandler.postDelayed( () -> requestScrollCapture(requestId, owner), 150); - mViewProxy.updateInsets( - mWindowManager.getCurrentWindowMetrics().getWindowInsets()); + mViewProxy.updateInsets(mWindow.getWindowInsets()); // Screenshot animation calculations won't be valid anymore, // so just end if (mScreenshotAnimation != null @@ -489,7 +466,7 @@ public class ScreenshotController implements InteractiveScreenshotHandler { private void requestScrollCapture(UUID requestId, UserHandle owner) { mScrollCaptureExecutor.requestScrollCapture( mDisplay.getDisplayId(), - mWindow.getDecorView().getWindowToken(), + mWindow.getWindowToken(), (response) -> { mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_LONG_SCREENSHOT_IMPRESSION, 0, response.getPackageName()); @@ -528,61 +505,9 @@ public class ScreenshotController implements InteractiveScreenshotHandler { mViewProxy::startLongScreenshotTransition); } - private void withWindowAttached(Runnable action) { - View decorView = mWindow.getDecorView(); - if (decorView.isAttachedToWindow()) { - action.run(); - } else { - decorView.getViewTreeObserver().addOnWindowAttachListener( - new ViewTreeObserver.OnWindowAttachListener() { - @Override - public void onWindowAttached() { - mAttachRequested = false; - decorView.getViewTreeObserver().removeOnWindowAttachListener(this); - action.run(); - } - - @Override - public void onWindowDetached() { - } - }); - - } - } - - @MainThread - private void attachWindow() { - View decorView = mWindow.getDecorView(); - if (decorView.isAttachedToWindow() || mAttachRequested) { - return; - } - if (DEBUG_WINDOW) { - Log.d(TAG, "attachWindow"); - } - mAttachRequested = true; - mWindowManager.addView(decorView, mWindowLayoutParams); - decorView.requestApplyInsets(); - - ViewGroup layout = decorView.requireViewById(android.R.id.content); - layout.setClipChildren(false); - layout.setClipToPadding(false); - } - @Override public void removeWindow() { - final View decorView = mWindow.peekDecorView(); - if (decorView != null && decorView.isAttachedToWindow()) { - if (DEBUG_WINDOW) { - Log.d(TAG, "Removing screenshot window"); - } - mWindowManager.removeViewImmediate(decorView); - mDetachRequested = false; - } - if (mAttachRequested && !mDetachRequested) { - mDetachRequested = true; - withWindowAttached(this::removeWindow); - } - + mWindow.removeWindow(); mViewProxy.stopInputListening(); } @@ -759,33 +684,6 @@ public class ScreenshotController implements InteractiveScreenshotHandler { .getContentResolver(), SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1; } - /** - * Updates the window focusability. If the window is already showing, then it updates the - * window immediately, otherwise the layout params will be applied when the window is next - * shown. - */ - private void setWindowFocusable(boolean focusable) { - if (DEBUG_WINDOW) { - Log.d(TAG, "setWindowFocusable: " + focusable); - } - int flags = mWindowLayoutParams.flags; - if (focusable) { - mWindowLayoutParams.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; - } else { - mWindowLayoutParams.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; - } - if (mWindowLayoutParams.flags == flags) { - if (DEBUG_WINDOW) { - Log.d(TAG, "setWindowFocusable: skipping, already " + focusable); - } - return; - } - final View decorView = mWindow.peekDecorView(); - if (decorView != null && decorView.isAttachedToWindow()) { - mWindowManager.updateViewLayout(decorView, mWindowLayoutParams); - } - } - private Rect getFullScreenRect() { DisplayMetrics displayMetrics = new DisplayMetrics(); mDisplay.getRealMetrics(displayMetrics); @@ -826,6 +724,6 @@ public class ScreenshotController implements InteractiveScreenshotHandler { * * @param display display to capture */ - LegacyScreenshotController create(Display display); + ScreenshotController create(Display display); } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotWindow.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotWindow.kt new file mode 100644 index 000000000000..644e12cba6fc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotWindow.kt @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot + +import android.R +import android.annotation.MainThread +import android.content.Context +import android.graphics.PixelFormat +import android.os.IBinder +import android.util.Log +import android.view.Display +import android.view.View +import android.view.ViewGroup +import android.view.ViewRootImpl +import android.view.ViewTreeObserver.OnWindowAttachListener +import android.view.Window +import android.view.WindowInsets +import android.view.WindowManager +import android.window.WindowContext +import com.android.internal.policy.PhoneWindow +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +/** Creates and manages the window in which the screenshot UI is displayed. */ +class ScreenshotWindow +@AssistedInject +constructor( + private val windowManager: WindowManager, + private val context: Context, + @Assisted private val display: Display, +) { + + val window: PhoneWindow = + PhoneWindow( + context + .createDisplayContext(display) + .createWindowContext(WindowManager.LayoutParams.TYPE_SCREENSHOT, null) + ) + private val params = + WindowManager.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + 0, /* xpos */ + 0, /* ypos */ + WindowManager.LayoutParams.TYPE_SCREENSHOT, + WindowManager.LayoutParams.FLAG_FULLSCREEN or + WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or + WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or + WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH or + WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or + WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM, + PixelFormat.TRANSLUCENT + ) + .apply { + layoutInDisplayCutoutMode = + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS + setFitInsetsTypes(0) + // This is needed to let touches pass through outside the touchable areas + privateFlags = + privateFlags or WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY + title = "ScreenshotUI" + } + private var attachRequested: Boolean = false + private var detachRequested: Boolean = false + + init { + window.requestFeature(Window.FEATURE_NO_TITLE) + window.requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS) + window.setBackgroundDrawableResource(R.color.transparent) + window.setWindowManager(windowManager, null, null) + } + + @MainThread + fun attachWindow() { + val decorView: View = window.getDecorView() + if (decorView.isAttachedToWindow || attachRequested) { + return + } + if (LogConfig.DEBUG_WINDOW) { + Log.d(TAG, "attachWindow") + } + attachRequested = true + windowManager.addView(decorView, params) + + decorView.requestApplyInsets() + decorView.requireViewById<ViewGroup>(R.id.content).apply { + clipChildren = false + clipToPadding = false + // ignore system bar insets for the purpose of window layout + setOnApplyWindowInsetsListener { _, _ -> WindowInsets.CONSUMED } + } + } + + fun whenWindowAttached(action: Runnable) { + val decorView: View = window.getDecorView() + if (decorView.isAttachedToWindow) { + action.run() + } else { + decorView + .getViewTreeObserver() + .addOnWindowAttachListener( + object : OnWindowAttachListener { + override fun onWindowAttached() { + attachRequested = false + decorView.getViewTreeObserver().removeOnWindowAttachListener(this) + action.run() + } + + override fun onWindowDetached() {} + } + ) + } + } + + fun removeWindow() { + val decorView: View? = window.peekDecorView() + if (decorView != null && decorView.isAttachedToWindow) { + if (LogConfig.DEBUG_WINDOW) { + Log.d(TAG, "Removing screenshot window") + } + windowManager.removeViewImmediate(decorView) + detachRequested = false + } + if (attachRequested && !detachRequested) { + detachRequested = true + whenWindowAttached { removeWindow() } + } + } + + /** + * Updates the window focusability. If the window is already showing, then it updates the window + * immediately, otherwise the layout params will be applied when the window is next shown. + */ + fun setFocusable(focusable: Boolean) { + if (LogConfig.DEBUG_WINDOW) { + Log.d(TAG, "setWindowFocusable: $focusable") + } + val flags: Int = params.flags + if (focusable) { + params.flags = params.flags and WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE.inv() + } else { + params.flags = params.flags or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + } + if (params.flags == flags) { + if (LogConfig.DEBUG_WINDOW) { + Log.d(TAG, "setWindowFocusable: skipping, already $focusable") + } + return + } + window.peekDecorView()?.also { + if (it.isAttachedToWindow) { + windowManager.updateViewLayout(it, params) + } + } + } + + fun getContext(): WindowContext = window.context as WindowContext + + fun getWindowToken(): IBinder = window.decorView.windowToken + + fun getWindowInsets(): WindowInsets = windowManager.currentWindowMetrics.windowInsets + + fun setContentView(view: View) { + window.setContentView(view) + } + + fun setActivityConfigCallback(callback: ViewRootImpl.ActivityConfigCallback) { + window.peekDecorView().viewRootImpl.setActivityConfigCallback(callback) + } + + @AssistedFactory + interface Factory { + fun create(display: Display): ScreenshotWindow + } + + companion object { + private const val TAG = "ScreenshotWindow" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/ui/viewModel/BrightnessMirrorViewModel.kt b/packages/SystemUI/src/com/android/systemui/settings/brightness/ui/viewModel/BrightnessMirrorViewModel.kt index 79e8b879288e..7f8c1463ed1f 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/brightness/ui/viewModel/BrightnessMirrorViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/ui/viewModel/BrightnessMirrorViewModel.kt @@ -19,25 +19,25 @@ package com.android.systemui.settings.brightness.ui.viewModel import android.content.res.Resources import android.util.Log import android.view.View -import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.lifecycle.SysUiViewModel import com.android.systemui.res.R import com.android.systemui.settings.brightness.BrightnessSliderController import com.android.systemui.settings.brightness.MirrorController import com.android.systemui.settings.brightness.ToggleSlider import com.android.systemui.settings.brightness.domain.interactor.BrightnessMirrorShowingInteractor -import javax.inject.Inject +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -@SysUISingleton class BrightnessMirrorViewModel -@Inject +@AssistedInject constructor( private val brightnessMirrorShowingInteractor: BrightnessMirrorShowingInteractor, @Main private val resources: Resources, val sliderControllerFactory: BrightnessSliderController.Factory, -) : MirrorController { +) : SysUiViewModel(), MirrorController { private val tempPosition = IntArray(2) @@ -99,6 +99,11 @@ constructor( override fun removeCallback(listener: MirrorController.BrightnessMirrorListener) {} + @AssistedFactory + interface Factory { + fun create(): BrightnessMirrorViewModel + } + companion object { private const val TAG = "BrightnessMirrorViewModel" } diff --git a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt index 05c50fe18c8b..15bbef02196a 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt @@ -293,7 +293,7 @@ constructor( ) containerView.systemGestureExclusionRects = - if (Flags.hubmodeFullscreenVerticalSwipe()) { + if (Flags.hubmodeFullscreenVerticalSwipeFix()) { listOf( // Disable back gestures on the left side of the screen, to avoid // conflicting with scene transitions. diff --git a/packages/SystemUI/src/com/android/systemui/shade/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/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..1027bc98ef47 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt @@ -81,11 +81,15 @@ constructor( * [CallType.Ongoing]. */ val ongoingCallNotification: Flow<ActiveNotificationModel?> = - allRepresentativeNotifications.map { notifMap -> - // Once a call has started, its `whenTime` should stay the same, so we can use it as a - // stable sort value. - notifMap.values.filter { it.callType == CallType.Ongoing }.minByOrNull { it.whenTime } - } + allRepresentativeNotifications + .map { notifMap -> + // Once a call has started, its `whenTime` should stay the same, so we can use it as + // a stable sort value. + notifMap.values + .filter { it.callType == CallType.Ongoing } + .minByOrNull { it.whenTime } + } + .flowOn(backgroundDispatcher) /** Are any notifications being actively presented in the notification stack? */ val areAnyNotificationsPresent: Flow<Boolean> = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/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..b6d58d6a23d9 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() { @@ -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/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 b7531b0e2e0f..44b692fcb786 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt @@ -131,7 +131,7 @@ constructor( val on = context.resources.getString(R.string.zen_mode_on) val off = context.resources.getString(R.string.zen_mode_off) - return mode.rule.triggerDescription ?: if (mode.isActive) on else off + return mode.getDynamicDescription(context) ?: if (mode.isActive) on else off } private fun makeZenModeDialog(): Dialog { diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt index 1b00ae2103f0..5980e1de85b4 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt @@ -38,8 +38,8 @@ fun BackGestureTutorialScreen( TutorialScreenConfig.Strings( titleResId = R.string.touchpad_back_gesture_action_title, bodyResId = R.string.touchpad_back_gesture_guidance, - titleSuccessResId = R.string.touchpad_tutorial_gesture_done, - bodySuccessResId = R.string.touchpad_back_gesture_finished + titleSuccessResId = R.string.touchpad_back_gesture_success_title, + bodySuccessResId = R.string.touchpad_back_gesture_success_body ), animations = TutorialScreenConfig.Animations( diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt index 416c562d212d..9ac2cba2b8d8 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt @@ -213,10 +213,14 @@ fun TutorialAnimation( transitionSpec = { if (initialState == NOT_STARTED && targetState == IN_PROGRESS) { val transitionDurationMillis = 150 - fadeIn( - animationSpec = tween(transitionDurationMillis, easing = LinearEasing) - ) togetherWith - fadeOut(animationSpec = snap(delayMillis = transitionDurationMillis)) + fadeIn(animationSpec = tween(transitionDurationMillis, easing = LinearEasing)) + .togetherWith( + fadeOut(animationSpec = snap(delayMillis = transitionDurationMillis)) + ) + // we explicitly don't want size transform because when targetState + // animation is loaded for the first time, AnimatedContent thinks target + // size is smaller and tries to shrink initial state animation + .using(sizeTransform = null) } else { // empty transition works because all remaining transitions are from IN_PROGRESS // state which shares initial animation frame with both FINISHED and NOT_STARTED diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt 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..ad8ab30daa33 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 @@ -28,6 +28,7 @@ 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.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.BACK_GESTURE import com.android.systemui.touchpad.tutorial.ui.viewmodel.Screen.HOME_GESTURE @@ -78,6 +79,10 @@ 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) }, + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt index 28ac2c0e8283..055671cf32ca 100644 --- a/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt @@ -28,6 +28,7 @@ import javax.inject.Inject import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine @@ -62,7 +63,9 @@ constructor( /** * Collect information for the given [flow], calling [consumer] for each emitted event. Defaults to * [LifeCycle.State.CREATED] to better align with legacy ViewController usage of attaching listeners - * during onViewAttached() and removing during onViewRemoved() + * during onViewAttached() and removing during onViewRemoved(). + * + * @return a disposable handle in order to cancel the flow in the future. */ @JvmOverloads fun <T> collectFlow( @@ -71,8 +74,8 @@ fun <T> collectFlow( consumer: Consumer<T>, coroutineContext: CoroutineContext = EmptyCoroutineContext, state: Lifecycle.State = Lifecycle.State.CREATED, -) { - view.repeatWhenAttached(coroutineContext) { +): DisposableHandle { + return view.repeatWhenAttached(coroutineContext) { repeatOnLifecycle(state) { flow.collect { consumer.accept(it) } } } } diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/GlobalSettingsImpl.java b/packages/SystemUI/src/com/android/systemui/util/settings/GlobalSettingsImpl.java index 816f55db65a0..7fcabe4a4363 100644 --- a/packages/SystemUI/src/com/android/systemui/util/settings/GlobalSettingsImpl.java +++ b/packages/SystemUI/src/com/android/systemui/util/settings/GlobalSettingsImpl.java @@ -42,28 +42,31 @@ class GlobalSettingsImpl implements GlobalSettings { mBgDispatcher = bgDispatcher; } + @NonNull @Override public ContentResolver getContentResolver() { return mContentResolver; } + @NonNull @Override - public Uri getUriFor(String name) { + public Uri getUriFor(@NonNull String name) { return Settings.Global.getUriFor(name); } + @NonNull @Override public CoroutineDispatcher getBackgroundDispatcher() { return mBgDispatcher; } @Override - public String getString(String name) { + public String getString(@NonNull String name) { return Settings.Global.getString(mContentResolver, name); } @Override - public boolean putString(String name, String value) { + public boolean putString(@NonNull String name, String value) { return Settings.Global.putString(mContentResolver, name, value); } diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/SecureSettingsImpl.java b/packages/SystemUI/src/com/android/systemui/util/settings/SecureSettingsImpl.java index f1da27f9cce9..c29648186d54 100644 --- a/packages/SystemUI/src/com/android/systemui/util/settings/SecureSettingsImpl.java +++ b/packages/SystemUI/src/com/android/systemui/util/settings/SecureSettingsImpl.java @@ -16,12 +16,11 @@ package com.android.systemui.util.settings; +import android.annotation.NonNull; import android.content.ContentResolver; import android.net.Uri; import android.provider.Settings; -import androidx.annotation.NonNull; - import com.android.systemui.util.kotlin.SettingsSingleThreadBackground; import kotlinx.coroutines.CoroutineDispatcher; @@ -43,46 +42,50 @@ class SecureSettingsImpl implements SecureSettings { mBgDispatcher = bgDispatcher; } + @NonNull @Override public ContentResolver getContentResolver() { return mContentResolver; } + @NonNull @Override public CurrentUserIdProvider getCurrentUserProvider() { return mCurrentUserProvider; } + @NonNull @Override - public Uri getUriFor(String name) { + public Uri getUriFor(@NonNull String name) { return Settings.Secure.getUriFor(name); } + @NonNull @Override public CoroutineDispatcher getBackgroundDispatcher() { return mBgDispatcher; } @Override - public String getStringForUser(String name, int userHandle) { + public String getStringForUser(@NonNull String name, int userHandle) { return Settings.Secure.getStringForUser(mContentResolver, name, getRealUserHandle(userHandle)); } @Override - public boolean putString(String name, String value, boolean overrideableByRestore) { + public boolean putString(@NonNull String name, String value, boolean overrideableByRestore) { return Settings.Secure.putString(mContentResolver, name, value, overrideableByRestore); } @Override - public boolean putStringForUser(String name, String value, int userHandle) { + public boolean putStringForUser(@NonNull String name, String value, int userHandle) { return Settings.Secure.putStringForUser(mContentResolver, name, value, getRealUserHandle(userHandle)); } @Override - public boolean putStringForUser(String name, String value, String tag, boolean makeDefault, - int userHandle, boolean overrideableByRestore) { + public boolean putStringForUser(@NonNull String name, String value, String tag, + boolean makeDefault, int userHandle, boolean overrideableByRestore) { return Settings.Secure.putStringForUser( mContentResolver, name, value, tag, makeDefault, getRealUserHandle(userHandle), overrideableByRestore); diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.kt b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.kt index 0ee997e4549d..82f41a7fd154 100644 --- a/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.kt +++ b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.kt @@ -346,7 +346,7 @@ interface SettingsProxy { * @param value to associate with the name * @return true if the value was set, false on database errors */ - fun putString(name: String, value: String): Boolean + fun putString(name: String, value: String?): Boolean /** * Store a name/value pair into the database. @@ -377,7 +377,7 @@ interface SettingsProxy { * @return true if the value was set, false on database errors. * @see .resetToDefaults */ - fun putString(name: String, value: String, tag: String, makeDefault: Boolean): Boolean + fun putString(name: String, value: String?, tag: String?, makeDefault: Boolean): Boolean /** * Convenience function for retrieving a single secure settings value as an integer. Note that diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/SystemSettingsImpl.java b/packages/SystemUI/src/com/android/systemui/util/settings/SystemSettingsImpl.java index 1e8035734a36..e670b2c2edd0 100644 --- a/packages/SystemUI/src/com/android/systemui/util/settings/SystemSettingsImpl.java +++ b/packages/SystemUI/src/com/android/systemui/util/settings/SystemSettingsImpl.java @@ -16,12 +16,11 @@ package com.android.systemui.util.settings; +import android.annotation.NonNull; import android.content.ContentResolver; import android.net.Uri; import android.provider.Settings; -import androidx.annotation.NonNull; - import com.android.systemui.util.kotlin.SettingsSingleThreadBackground; import kotlinx.coroutines.CoroutineDispatcher; @@ -42,46 +41,50 @@ class SystemSettingsImpl implements SystemSettings { mBgCoroutineDispatcher = bgDispatcher; } + @NonNull @Override public ContentResolver getContentResolver() { return mContentResolver; } + @NonNull @Override public CurrentUserIdProvider getCurrentUserProvider() { return mCurrentUserProvider; } + @NonNull @Override - public Uri getUriFor(String name) { + public Uri getUriFor(@NonNull String name) { return Settings.System.getUriFor(name); } + @NonNull @Override public CoroutineDispatcher getBackgroundDispatcher() { return mBgCoroutineDispatcher; } @Override - public String getStringForUser(String name, int userHandle) { + public String getStringForUser(@NonNull String name, int userHandle) { return Settings.System.getStringForUser(mContentResolver, name, getRealUserHandle(userHandle)); } @Override - public boolean putString(String name, String value, boolean overrideableByRestore) { + public boolean putString(@NonNull String name, String value, boolean overrideableByRestore) { return Settings.System.putString(mContentResolver, name, value, overrideableByRestore); } @Override - public boolean putStringForUser(String name, String value, int userHandle) { + public boolean putStringForUser(@NonNull String name, String value, int userHandle) { return Settings.System.putStringForUser(mContentResolver, name, value, getRealUserHandle(userHandle)); } @Override - public boolean putStringForUser(String name, String value, String tag, boolean makeDefault, - int userHandle, boolean overrideableByRestore) { + public boolean putStringForUser(@NonNull String name, String value, String tag, + boolean makeDefault, int userHandle, boolean overrideableByRestore) { throw new UnsupportedOperationException( "This method only exists publicly for Settings.Secure and Settings.Global"); } diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt b/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt index 9ae8f03479cf..8e3b813a2a82 100644 --- a/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt +++ b/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt @@ -368,19 +368,19 @@ interface UserSettingsProxy : SettingsProxy { * @param value to associate with the name * @return true if the value was set, false on database errors */ - fun putString(name: String, value: String, overrideableByRestore: Boolean): Boolean + fun putString(name: String, value: String?, overrideableByRestore: Boolean): Boolean - override fun putString(name: String, value: String): Boolean { + override fun putString(name: String, value: String?): Boolean { return putStringForUser(name, value, userId) } /** Similar implementation to [putString] for the specified [userHandle]. */ - fun putStringForUser(name: String, value: String, userHandle: Int): Boolean + fun putStringForUser(name: String, value: String?, userHandle: Int): Boolean /** Similar implementation to [putString] for the specified [userHandle]. */ fun putStringForUser( name: String, - value: String, + value: String?, tag: String?, makeDefault: Boolean, @UserIdInt userHandle: Int, diff --git a/packages/SystemUI/tests/src/com/android/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/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/navigationbar/views/NavigationBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java index a8cbbd4178bd..a52ab0c690a4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java @@ -20,7 +20,6 @@ import static android.app.StatusBarManager.NAVIGATION_HINT_BACK_ALT; import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SHOWN; import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_SHOWN; import static android.inputmethodservice.InputMethodService.BACK_DISPOSITION_DEFAULT; -import static android.inputmethodservice.InputMethodService.IME_INVISIBLE; import static android.inputmethodservice.InputMethodService.IME_VISIBLE; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS; @@ -512,7 +511,7 @@ public class NavigationBarTest extends SysuiTestCase { externalNavBar.setImeWindowStatus(EXTERNAL_DISPLAY_ID, IME_VISIBLE, BACK_DISPOSITION_DEFAULT, true); - defaultNavBar.setImeWindowStatus(DEFAULT_DISPLAY, IME_INVISIBLE, + defaultNavBar.setImeWindowStatus(DEFAULT_DISPLAY, 0 /* vis */, BACK_DISPOSITION_DEFAULT, false); // Verify IME window state will be updated in external NavBar & default NavBar state reset. assertEquals(NAVIGATION_HINT_BACK_ALT | NAVIGATION_HINT_IME_SHOWN diff --git a/packages/SystemUI/tests/src/com/android/systemui/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/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/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/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/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 8afe54ebabc8..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); } + } - final var newMethodMap = userData.mRawInputMethodMap.get().toInputMethodMap( - newAdditionalSubtypeMap, - DirectBootAwareness.AUTO, - mUserManagerInternal.isUserUnlockingOrUnlocked(userId)); + // 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, + userData.mIsUnlockingOrUnlocked.get()); + + final boolean noUpdate = InputMethodMap.areSame(settings.getMethodMap(), newMethodMap); + if (noUpdate && imesToBeDisabled.isEmpty()) { + return; + } - if (InputMethodMap.areSame(settings.getMethodMap(), newMethodMap)) { - // No update in the actual IME map. + // 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(). @@ -3768,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); @@ -3815,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. @@ -4077,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(); } @@ -4086,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. */ @@ -4098,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()); } } @@ -4123,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}) @@ -4160,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); } } @@ -4174,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); @@ -4198,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(); @@ -4345,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 @@ -4416,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); @@ -4567,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(); @@ -4756,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; @@ -4783,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()); } @@ -4793,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); } @@ -4916,72 +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 - setImeVisibilityOnFocusedWindowClient(false, userData, 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(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); - } - } 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); } } @@ -5078,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( @@ -5092,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); } } @@ -5144,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 @@ -5182,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) { @@ -5210,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()) { @@ -5284,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(); } @@ -5292,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; @@ -5632,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); @@ -5642,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 { @@ -5683,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); } /** @@ -5720,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; } @@ -5783,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 = @@ -5803,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; } @@ -5881,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); } } @@ -5966,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 { @@ -5982,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) { @@ -6094,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); } } @@ -6249,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:"); @@ -6281,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=" @@ -6299,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()); @@ -6322,7 +6145,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. }; mUserDataRepository.forAllUserData(userDataDump); - if (mNewInputMethodSwitcherMenuEnabled) { + if (Flags.imeSwitcherRevamp()) { p.println(" menuControllerNew:"); mMenuControllerNew.dump(p, " "); } else { @@ -6373,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) { @@ -6604,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()) { @@ -6650,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; @@ -6745,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); @@ -6765,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; } @@ -6786,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; @@ -6951,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; @@ -6967,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 @@ -6988,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); } @@ -6998,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 @@ -7006,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); } @@ -7019,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); } @@ -7033,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); } @@ -7047,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); } @@ -7057,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 @@ -7065,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); } @@ -7077,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); } @@ -7089,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); } @@ -7099,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(); @@ -7142,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..4dbbfa2be334 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; } } @@ -575,11 +581,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 +597,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 +806,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); } } } 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..17f6561cb757 100644 --- a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java +++ b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java @@ -622,16 +622,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") @@ -4589,7 +4579,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 +4589,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 +4679,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 +4694,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 +4708,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 +4741,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 +4778,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 +4806,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); } } @@ -5313,16 +5295,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); } @@ -6269,18 +6246,8 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { "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. 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..b4459cb2fe92 100644 --- a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java +++ b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java @@ -1194,9 +1194,9 @@ public final class NotificationAttentionHelper { } boolean shouldIgnoreNotification(final NotificationRecord record) { - // Ignore group summaries - return (record.getSbn().isGroup() && record.getSbn().getNotification() - .isGroupSummary()); + // Ignore auto-group summaries => don't count them as app-posted notifications + // for the cooldown budget + return (record.getSbn().isGroup() && GroupHelper.isAggregatedGroup(record)); } /** diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 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/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 829ee27287c2..57d7d79b2392 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -2105,6 +2105,10 @@ public class UserManagerService extends IUserManager.Stub { @Override public void setUserAdmin(@UserIdInt int userId) { checkManageUserAndAcrossUsersFullPermission("set user admin"); + if (Flags.unicornModeRefactoringForHsumReadOnly()) { + checkAdminStatusChangeAllowed(userId); + } + mUserJourneyLogger.logUserJourneyBegin(userId, USER_JOURNEY_GRANT_ADMIN); UserData user; synchronized (mPackagesLock) { @@ -2133,6 +2137,10 @@ public class UserManagerService extends IUserManager.Stub { @Override public void revokeUserAdmin(@UserIdInt int userId) { checkManageUserAndAcrossUsersFullPermission("revoke admin privileges"); + if (Flags.unicornModeRefactoringForHsumReadOnly()) { + checkAdminStatusChangeAllowed(userId); + } + mUserJourneyLogger.logUserJourneyBegin(userId, USER_JOURNEY_REVOKE_ADMIN); UserData user; synchronized (mPackagesLock) { @@ -4065,6 +4073,26 @@ public class UserManagerService extends IUserManager.Stub { } } + /** + * Checks if changing the admin status of a target user is restricted + * due to the DISALLOW_GRANT_ADMIN restriction. If either the calling + * user or the target user has this restriction, a SecurityException + * is thrown. + * + * @param targetUser The user ID of the user whose admin status is being + * considered for change. + * @throws SecurityException if the admin status change is restricted due + * to the DISALLOW_GRANT_ADMIN restriction. + */ + private void checkAdminStatusChangeAllowed(int targetUser) { + if (hasUserRestriction(UserManager.DISALLOW_GRANT_ADMIN, UserHandle.getCallingUserId()) + || hasUserRestriction(UserManager.DISALLOW_GRANT_ADMIN, targetUser)) { + throw new SecurityException( + "Admin status change is restricted. The DISALLOW_GRANT_ADMIN " + + "restriction is applied either on the current or the target user."); + } + } + @GuardedBy({"mPackagesLock"}) private void writeBitmapLP(UserInfo info, Bitmap bitmap) { try { @@ -5443,6 +5471,13 @@ public class UserManagerService extends IUserManager.Stub { enforceUserRestriction(restriction, UserHandle.getCallingUserId(), "Cannot add user"); + if (Flags.unicornModeRefactoringForHsumReadOnly()) { + if ((flags & UserInfo.FLAG_ADMIN) != 0) { + enforceUserRestriction(UserManager.DISALLOW_GRANT_ADMIN, + UserHandle.getCallingUserId(), "Cannot create ADMIN user"); + } + } + return createUserInternalUnchecked(name, userType, flags, parentId, /* preCreate= */ false, disallowedPackages, /* token= */ null); } diff --git a/services/core/java/com/android/server/pm/pkg/PackageState.java b/services/core/java/com/android/server/pm/pkg/PackageState.java index 58761886ecb9..bbc17c83cfac 100644 --- a/services/core/java/com/android/server/pm/pkg/PackageState.java +++ b/services/core/java/com/android/server/pm/pkg/PackageState.java @@ -488,4 +488,10 @@ public interface PackageState { * @hide */ boolean isScannedAsStoppedSystemApp(); + + /** + * see AndroidPackage#isLeavingSharedUser() + * @hide + */ + boolean isLeavingSharedUser(); } diff --git a/services/core/java/com/android/server/policy/ModifierShortcutManager.java b/services/core/java/com/android/server/policy/ModifierShortcutManager.java index cefecbc99bd7..5a4518606ca6 100644 --- a/services/core/java/com/android/server/policy/ModifierShortcutManager.java +++ b/services/core/java/com/android/server/policy/ModifierShortcutManager.java @@ -30,6 +30,7 @@ import android.content.pm.PackageManager; import android.content.res.XmlResourceParser; import android.graphics.drawable.Icon; import android.hardware.input.InputManager; +import android.hardware.input.KeyboardSystemShortcut; import android.os.Handler; import android.os.RemoteException; import android.os.UserHandle; @@ -39,7 +40,6 @@ import android.util.Log; import android.util.LongSparseArray; import android.util.Slog; import android.util.SparseArray; -import android.view.InputDevice; import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.KeyboardShortcutGroup; @@ -49,8 +49,8 @@ import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.policy.IShortcutService; import com.android.internal.util.XmlUtils; -import com.android.server.input.KeyboardMetricsCollector; -import com.android.server.input.KeyboardMetricsCollector.KeyboardLogEvent; +import com.android.server.LocalServices; +import com.android.server.input.InputManagerInternal; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -61,6 +61,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; /** * Manages quick launch shortcuts by: @@ -123,6 +124,7 @@ public class ModifierShortcutManager { private final Context mContext; private final Handler mHandler; + private final InputManagerInternal mInputManagerInternal; private boolean mSearchKeyShortcutPending = false; private boolean mConsumeSearchKeyUp = true; private UserHandle mCurrentUser; @@ -136,6 +138,7 @@ public class ModifierShortcutManager { mRoleIntents.remove(roleName); }, UserHandle.ALL); mCurrentUser = currentUser; + mInputManagerInternal = LocalServices.getService(InputManagerInternal.class); loadShortcuts(); } @@ -473,7 +476,7 @@ public class ModifierShortcutManager { + "keyCode=" + KeyEvent.keyCodeToString(keyCode) + "," + " category=" + category + " role=" + role); } - logKeyboardShortcut(keyEvent, KeyboardLogEvent.getLogEventFromIntent(intent)); + notifyKeyboardShortcutTriggered(keyEvent, getSystemShortcutFromIntent(intent)); return true; } else { return false; @@ -494,22 +497,19 @@ public class ModifierShortcutManager { + "the activity to which it is registered was not found: " + "META+ or SEARCH" + KeyEvent.keyCodeToString(keyCode)); } - logKeyboardShortcut(keyEvent, KeyboardLogEvent.getLogEventFromIntent(shortcutIntent)); + notifyKeyboardShortcutTriggered(keyEvent, getSystemShortcutFromIntent(shortcutIntent)); return true; } return false; } - private void logKeyboardShortcut(KeyEvent event, KeyboardLogEvent logEvent) { - mHandler.post(() -> handleKeyboardLogging(event, logEvent)); - } - - private void handleKeyboardLogging(KeyEvent event, KeyboardLogEvent logEvent) { - final InputManager inputManager = mContext.getSystemService(InputManager.class); - final InputDevice inputDevice = inputManager != null - ? inputManager.getInputDevice(event.getDeviceId()) : null; - KeyboardMetricsCollector.logKeyboardSystemsEventReportedAtom(inputDevice, - logEvent, event.getMetaState(), event.getKeyCode()); + private void notifyKeyboardShortcutTriggered(KeyEvent event, + @KeyboardSystemShortcut.SystemShortcut int systemShortcut) { + if (systemShortcut == KeyboardSystemShortcut.SYSTEM_SHORTCUT_UNSPECIFIED) { + return; + } + mInputManagerInternal.notifyKeyboardShortcutTriggered(event.getDeviceId(), + new int[]{event.getKeyCode()}, event.getMetaState(), systemShortcut); } /** @@ -708,6 +708,97 @@ public class ModifierShortcutManager { return context.getString(resid); }; + + /** + * Find Keyboard shortcut event corresponding to intent filter category. Returns + * {@code SYSTEM_SHORTCUT_UNSPECIFIED if no matching event found} + */ + @KeyboardSystemShortcut.SystemShortcut + private static int getSystemShortcutFromIntent(Intent intent) { + Intent selectorIntent = intent.getSelector(); + if (selectorIntent != null) { + Set<String> selectorCategories = selectorIntent.getCategories(); + if (selectorCategories != null && !selectorCategories.isEmpty()) { + for (String intentCategory : selectorCategories) { + int systemShortcut = getEventFromSelectorCategory(intentCategory); + if (systemShortcut == KeyboardSystemShortcut.SYSTEM_SHORTCUT_UNSPECIFIED) { + continue; + } + return systemShortcut; + } + } + } + + // The shortcut may be targeting a system role rather than using an intent selector, + // so check for that. + String role = intent.getStringExtra(ModifierShortcutManager.EXTRA_ROLE); + if (!TextUtils.isEmpty(role)) { + return getLogEventFromRole(role); + } + + Set<String> intentCategories = intent.getCategories(); + if (intentCategories == null || intentCategories.isEmpty() + || !intentCategories.contains(Intent.CATEGORY_LAUNCHER)) { + return KeyboardSystemShortcut.SYSTEM_SHORTCUT_UNSPECIFIED; + } + if (intent.getComponent() == null) { + return KeyboardSystemShortcut.SYSTEM_SHORTCUT_UNSPECIFIED; + } + + // TODO(b/280423320): Add new field package name associated in the + // KeyboardShortcutEvent atom and log it accordingly. + return KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_APPLICATION_BY_PACKAGE_NAME; + } + + @KeyboardSystemShortcut.SystemShortcut + private static int getEventFromSelectorCategory(String category) { + switch (category) { + case Intent.CATEGORY_APP_BROWSER: + return KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_DEFAULT_BROWSER; + case Intent.CATEGORY_APP_EMAIL: + return KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_DEFAULT_EMAIL; + case Intent.CATEGORY_APP_CONTACTS: + return KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CONTACTS; + case Intent.CATEGORY_APP_CALENDAR: + return KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CALENDAR; + case Intent.CATEGORY_APP_CALCULATOR: + return KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CALCULATOR; + case Intent.CATEGORY_APP_MUSIC: + return KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MUSIC; + case Intent.CATEGORY_APP_MAPS: + return KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MAPS; + case Intent.CATEGORY_APP_MESSAGING: + return KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MESSAGING; + case Intent.CATEGORY_APP_GALLERY: + return KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_DEFAULT_GALLERY; + case Intent.CATEGORY_APP_FILES: + return KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_DEFAULT_FILES; + case Intent.CATEGORY_APP_WEATHER: + return KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_DEFAULT_WEATHER; + case Intent.CATEGORY_APP_FITNESS: + return KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_DEFAULT_FITNESS; + default: + return KeyboardSystemShortcut.SYSTEM_SHORTCUT_UNSPECIFIED; + } + } + + /** + * Find KeyboardLogEvent corresponding to the provide system role name. + * Returns {@code null} if no matching event found. + */ + @KeyboardSystemShortcut.SystemShortcut + private static int getLogEventFromRole(String role) { + if (RoleManager.ROLE_BROWSER.equals(role)) { + return KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_DEFAULT_BROWSER; + } else if (RoleManager.ROLE_SMS.equals(role)) { + return KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MESSAGING; + } else { + Log.w(TAG, "Keyboard shortcut to launch " + + role + " not supported for logging"); + return KeyboardSystemShortcut.SYSTEM_SHORTCUT_UNSPECIFIED; + } + } + void dump(String prefix, PrintWriter pw) { IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " ", prefix); ipw.println("ModifierShortcutManager shortcuts:"); diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 21d6c6457e75..720c1c201158 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -139,6 +139,7 @@ import android.hardware.hdmi.HdmiControlManager; import android.hardware.hdmi.HdmiPlaybackClient; import android.hardware.hdmi.HdmiPlaybackClient.OneTouchPlayCallback; import android.hardware.input.InputManager; +import android.hardware.input.KeyboardSystemShortcut; import android.media.AudioManager; import android.media.AudioManagerInternal; import android.media.AudioSystem; @@ -164,8 +165,6 @@ import android.os.SystemProperties; import android.os.Trace; import android.os.UEventObserver; import android.os.UserHandle; -import android.os.VibrationAttributes; -import android.os.VibrationEffect; import android.os.Vibrator; import android.provider.DeviceConfig; import android.provider.MediaStore; @@ -229,8 +228,6 @@ import com.android.server.LocalServices; import com.android.server.SystemServiceManager; import com.android.server.UiThread; import com.android.server.input.InputManagerInternal; -import com.android.server.input.KeyboardMetricsCollector; -import com.android.server.input.KeyboardMetricsCollector.KeyboardLogEvent; import com.android.server.inputmethod.InputMethodManagerInternal; import com.android.server.pm.UserManagerInternal; import com.android.server.policy.KeyCombinationManager.TwoKeysCombinationRule; @@ -238,8 +235,6 @@ import com.android.server.policy.keyguard.KeyguardServiceDelegate; import com.android.server.policy.keyguard.KeyguardServiceDelegate.DrawnListener; import com.android.server.policy.keyguard.KeyguardStateMonitor.StateCallback; import com.android.server.statusbar.StatusBarManagerInternal; -import com.android.server.vibrator.HapticFeedbackVibrationProvider; -import com.android.server.vibrator.VibratorFrameworkStatsLogger; import com.android.server.vr.VrManagerInternal; import com.android.server.wallpaper.WallpaperManagerInternal; import com.android.server.wm.ActivityTaskManagerInternal; @@ -462,7 +457,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { PackageManager mPackageManager; SideFpsEventHandler mSideFpsEventHandler; LockPatternUtils mLockPatternUtils; - private HapticFeedbackVibrationProvider mHapticFeedbackVibrationProvider; private boolean mHasFeatureAuto; private boolean mHasFeatureWatch; private boolean mHasFeatureLeanback; @@ -737,7 +731,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { private static final int MSG_LAUNCH_ASSIST = 23; private static final int MSG_RINGER_TOGGLE_CHORD = 24; private static final int MSG_SWITCH_KEYBOARD_LAYOUT = 25; - private static final int MSG_LOG_KEYBOARD_SYSTEM_EVENT = 26; private static final int MSG_SET_DEFERRED_KEY_ACTIONS_EXECUTABLE = 27; private class PolicyHandler extends Handler { @@ -825,9 +818,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { handleSwitchKeyboardLayout(object.keyEvent, object.direction, object.focusedToken); break; - case MSG_LOG_KEYBOARD_SYSTEM_EVENT: - handleKeyboardSystemEvent(KeyboardLogEvent.from(msg.arg1), (KeyEvent) msg.obj); - break; case MSG_SET_DEFERRED_KEY_ACTIONS_EXECUTABLE: final int keyCode = msg.arg1; final long downTime = (Long) msg.obj; @@ -1829,7 +1819,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { } private void handleShortPressOnHome(KeyEvent event) { - logKeyboardSystemsEvent(event, KeyboardLogEvent.HOME); + notifyKeyboardShortcutTriggered(event, KeyboardSystemShortcut.SYSTEM_SHORTCUT_HOME); // Turn on the connected TV and switch HDMI input if we're a HDMI playback device. final HdmiControl hdmiControl = getHdmiControl(); @@ -2063,7 +2053,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { } switch (mDoubleTapOnHomeBehavior) { case DOUBLE_TAP_HOME_RECENT_SYSTEM_UI: - logKeyboardSystemsEvent(event, KeyboardLogEvent.APP_SWITCH); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_APP_SWITCH); mHomeConsumed = true; toggleRecentApps(); break; @@ -2091,19 +2082,23 @@ public class PhoneWindowManager implements WindowManagerPolicy { case LONG_PRESS_HOME_ALL_APPS: if (mHasFeatureLeanback) { launchAllAppsAction(); - logKeyboardSystemsEvent(event, KeyboardLogEvent.ALL_APPS); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_ALL_APPS); } else { launchAllAppsViaA11y(); - logKeyboardSystemsEvent(event, KeyboardLogEvent.ACCESSIBILITY_ALL_APPS); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_ACCESSIBILITY_ALL_APPS); } break; case LONG_PRESS_HOME_ASSIST: - logKeyboardSystemsEvent(event, KeyboardLogEvent.LAUNCH_ASSISTANT); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_ASSISTANT); launchAssistAction(null, event.getDeviceId(), event.getEventTime(), AssistUtils.INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS); break; case LONG_PRESS_HOME_NOTIFICATION_PANEL: - logKeyboardSystemsEvent(event, KeyboardLogEvent.TOGGLE_NOTIFICATION_PANEL); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_TOGGLE_NOTIFICATION_PANEL); toggleNotificationPanel(); break; default: @@ -2388,8 +2383,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { mContext.registerReceiver(mMultiuserReceiver, filter); mVibrator = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE); - mHapticFeedbackVibrationProvider = - new HapticFeedbackVibrationProvider(mContext.getResources(), mVibrator); mGlobalKeyManager = new GlobalKeyManager(mContext); @@ -3292,39 +3285,29 @@ public class PhoneWindowManager implements WindowManagerPolicy { WindowManager.LayoutParams.TYPE_SYSTEM_ERROR, }; - /** - * Log the keyboard shortcuts without blocking the current thread. - * - * We won't log keyboard events when the input device is null - * or when it is virtual. - */ - private void handleKeyboardSystemEvent(KeyboardLogEvent keyboardLogEvent, KeyEvent event) { - final InputDevice inputDevice = mInputManager.getInputDevice(event.getDeviceId()); - KeyboardMetricsCollector.logKeyboardSystemsEventReportedAtom(inputDevice, - keyboardLogEvent, event.getMetaState(), event.getKeyCode()); - event.recycle(); - } - - private void logKeyboardSystemsEventOnActionUp(KeyEvent event, - KeyboardLogEvent keyboardSystemEvent) { + private void notifyKeyboardShortcutTriggeredOnActionUp(KeyEvent event, + @KeyboardSystemShortcut.SystemShortcut int systemShortcut) { if (event.getAction() != KeyEvent.ACTION_UP) { return; } - logKeyboardSystemsEvent(event, keyboardSystemEvent); + notifyKeyboardShortcutTriggered(event, systemShortcut); } - private void logKeyboardSystemsEventOnActionDown(KeyEvent event, - KeyboardLogEvent keyboardSystemEvent) { + private void notifyKeyboardShortcutTriggeredOnActionDown(KeyEvent event, + @KeyboardSystemShortcut.SystemShortcut int systemShortcut) { if (event.getAction() != KeyEvent.ACTION_DOWN) { return; } - logKeyboardSystemsEvent(event, keyboardSystemEvent); + notifyKeyboardShortcutTriggered(event, systemShortcut); } - private void logKeyboardSystemsEvent(KeyEvent event, KeyboardLogEvent keyboardSystemEvent) { - KeyEvent eventToLog = KeyEvent.obtain(event); - mHandler.obtainMessage(MSG_LOG_KEYBOARD_SYSTEM_EVENT, keyboardSystemEvent.getIntValue(), 0, - eventToLog).sendToTarget(); + private void notifyKeyboardShortcutTriggered(KeyEvent event, + @KeyboardSystemShortcut.SystemShortcut int systemShortcut) { + if (systemShortcut == KeyboardSystemShortcut.SYSTEM_SHORTCUT_UNSPECIFIED) { + return; + } + mInputManagerInternal.notifyKeyboardShortcutTriggered(event.getDeviceId(), + new int[]{event.getKeyCode()}, event.getMetaState(), systemShortcut); } @Override @@ -3434,7 +3417,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { case KeyEvent.KEYCODE_RECENT_APPS: if (firstDown) { showRecentApps(false /* triggeredFromAltTab */); - logKeyboardSystemsEvent(event, KeyboardLogEvent.RECENT_APPS); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_RECENT_APPS); } return true; case KeyEvent.KEYCODE_APP_SWITCH: @@ -3443,7 +3427,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { preloadRecentApps(); } else if (!down) { toggleRecentApps(); - logKeyboardSystemsEvent(event, KeyboardLogEvent.APP_SWITCH); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_APP_SWITCH); } } return true; @@ -3452,7 +3437,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { launchAssistAction(Intent.EXTRA_ASSIST_INPUT_HINT_KEYBOARD, deviceId, event.getEventTime(), AssistUtils.INVOCATION_TYPE_UNKNOWN); - logKeyboardSystemsEvent(event, KeyboardLogEvent.LAUNCH_ASSISTANT); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_ASSISTANT); return true; } break; @@ -3465,14 +3451,16 @@ public class PhoneWindowManager implements WindowManagerPolicy { case KeyEvent.KEYCODE_I: if (firstDown && event.isMetaPressed()) { showSystemSettings(); - logKeyboardSystemsEvent(event, KeyboardLogEvent.LAUNCH_SYSTEM_SETTINGS); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_SYSTEM_SETTINGS); return true; } break; case KeyEvent.KEYCODE_L: if (firstDown && event.isMetaPressed()) { lockNow(null /* options */); - logKeyboardSystemsEvent(event, KeyboardLogEvent.LOCK_SCREEN); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_LOCK_SCREEN); return true; } break; @@ -3480,10 +3468,12 @@ public class PhoneWindowManager implements WindowManagerPolicy { if (firstDown && event.isMetaPressed()) { if (event.isCtrlPressed()) { sendSystemKeyToStatusBarAsync(event); - logKeyboardSystemsEvent(event, KeyboardLogEvent.OPEN_NOTES); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_OPEN_NOTES); } else { toggleNotificationPanel(); - logKeyboardSystemsEvent(event, KeyboardLogEvent.TOGGLE_NOTIFICATION_PANEL); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_TOGGLE_NOTIFICATION_PANEL); } return true; } @@ -3491,7 +3481,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { case KeyEvent.KEYCODE_S: if (firstDown && event.isMetaPressed() && event.isCtrlPressed()) { interceptScreenshotChord(SCREENSHOT_KEY_OTHER, 0 /*pressDelay*/); - logKeyboardSystemsEvent(event, KeyboardLogEvent.TAKE_SCREENSHOT); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_TAKE_SCREENSHOT); return true; } break; @@ -3504,14 +3495,16 @@ public class PhoneWindowManager implements WindowManagerPolicy { } catch (RemoteException e) { Slog.d(TAG, "Error taking bugreport", e); } - logKeyboardSystemsEvent(event, KeyboardLogEvent.TRIGGER_BUG_REPORT); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_TRIGGER_BUG_REPORT); return true; } } // fall through case KeyEvent.KEYCODE_ESCAPE: if (firstDown && event.isMetaPressed()) { - logKeyboardSystemsEvent(event, KeyboardLogEvent.BACK); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_BACK); injectBackGesture(event.getDownTime()); return true; } @@ -3520,7 +3513,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { StatusBarManagerInternal statusbar = getStatusBarManagerInternal(); if (statusbar != null) { statusbar.moveFocusedTaskToFullscreen(getTargetDisplayIdForKeyEvent(event)); - logKeyboardSystemsEvent(event, KeyboardLogEvent.MULTI_WINDOW_NAVIGATION); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_MULTI_WINDOW_NAVIGATION); return true; } } @@ -3530,7 +3524,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { StatusBarManagerInternal statusbar = getStatusBarManagerInternal(); if (statusbar != null) { statusbar.moveFocusedTaskToDesktop(getTargetDisplayIdForKeyEvent(event)); - logKeyboardSystemsEvent(event, KeyboardLogEvent.DESKTOP_MODE); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_DESKTOP_MODE); return true; } } @@ -3540,12 +3535,15 @@ public class PhoneWindowManager implements WindowManagerPolicy { if (event.isCtrlPressed()) { moveFocusedTaskToStageSplit(getTargetDisplayIdForKeyEvent(event), true /* leftOrTop */); - logKeyboardSystemsEvent(event, KeyboardLogEvent.SPLIT_SCREEN_NAVIGATION); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_SPLIT_SCREEN_NAVIGATION); } else if (event.isAltPressed()) { setSplitscreenFocus(true /* leftOrTop */); - logKeyboardSystemsEvent(event, KeyboardLogEvent.CHANGE_SPLITSCREEN_FOCUS); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_CHANGE_SPLITSCREEN_FOCUS); } else { - logKeyboardSystemsEvent(event, KeyboardLogEvent.BACK); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_BACK); injectBackGesture(event.getDownTime()); } return true; @@ -3556,11 +3554,13 @@ public class PhoneWindowManager implements WindowManagerPolicy { if (event.isCtrlPressed()) { moveFocusedTaskToStageSplit(getTargetDisplayIdForKeyEvent(event), false /* leftOrTop */); - logKeyboardSystemsEvent(event, KeyboardLogEvent.SPLIT_SCREEN_NAVIGATION); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_SPLIT_SCREEN_NAVIGATION); return true; } else if (event.isAltPressed()) { setSplitscreenFocus(false /* leftOrTop */); - logKeyboardSystemsEvent(event, KeyboardLogEvent.CHANGE_SPLITSCREEN_FOCUS); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_CHANGE_SPLITSCREEN_FOCUS); return true; } } @@ -3568,7 +3568,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { case KeyEvent.KEYCODE_SLASH: if (firstDown && event.isMetaPressed() && !keyguardOn) { toggleKeyboardShortcutsMenu(event.getDeviceId()); - logKeyboardSystemsEvent(event, KeyboardLogEvent.OPEN_SHORTCUT_HELPER); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_OPEN_SHORTCUT_HELPER); return true; } break; @@ -3620,25 +3621,32 @@ public class PhoneWindowManager implements WindowManagerPolicy { | Intent.FLAG_ACTIVITY_NO_USER_ACTION); intent.putExtra(EXTRA_FROM_BRIGHTNESS_KEY, true); startActivityAsUser(intent, UserHandle.CURRENT_OR_SELF); - logKeyboardSystemsEvent(event, KeyboardLogEvent.getBrightnessEvent(keyCode)); + + int systemShortcut = keyCode == KeyEvent.KEYCODE_BRIGHTNESS_DOWN + ? KeyboardSystemShortcut.SYSTEM_SHORTCUT_BRIGHTNESS_DOWN + : KeyboardSystemShortcut.SYSTEM_SHORTCUT_BRIGHTNESS_UP; + notifyKeyboardShortcutTriggered(event, systemShortcut); } return true; case KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_DOWN: if (down) { mInputManagerInternal.decrementKeyboardBacklight(event.getDeviceId()); - logKeyboardSystemsEvent(event, KeyboardLogEvent.KEYBOARD_BACKLIGHT_DOWN); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_DOWN); } return true; case KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_UP: if (down) { mInputManagerInternal.incrementKeyboardBacklight(event.getDeviceId()); - logKeyboardSystemsEvent(event, KeyboardLogEvent.KEYBOARD_BACKLIGHT_UP); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_UP); } return true; case KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_TOGGLE: // TODO: Add logic if (!down) { - logKeyboardSystemsEvent(event, KeyboardLogEvent.KEYBOARD_BACKLIGHT_TOGGLE); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_TOGGLE); } return true; case KeyEvent.KEYCODE_VOLUME_UP: @@ -3665,7 +3673,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { if (firstDown && !keyguardOn && isUserSetupComplete()) { if (event.isMetaPressed()) { showRecentApps(false); - logKeyboardSystemsEvent(event, KeyboardLogEvent.RECENT_APPS); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_RECENT_APPS); return true; } else if (mRecentAppsHeldModifiers == 0) { final int shiftlessModifiers = @@ -3674,7 +3683,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { shiftlessModifiers, KeyEvent.META_ALT_ON)) { mRecentAppsHeldModifiers = shiftlessModifiers; showRecentApps(true); - logKeyboardSystemsEvent(event, KeyboardLogEvent.RECENT_APPS); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_RECENT_APPS); return true; } } @@ -3687,17 +3697,20 @@ public class PhoneWindowManager implements WindowManagerPolicy { Message msg = mHandler.obtainMessage(MSG_HANDLE_ALL_APPS); msg.setAsynchronous(true); msg.sendToTarget(); - logKeyboardSystemsEvent(event, KeyboardLogEvent.ALL_APPS); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_ALL_APPS); } else { launchAllAppsViaA11y(); - logKeyboardSystemsEvent(event, KeyboardLogEvent.ACCESSIBILITY_ALL_APPS); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_ACCESSIBILITY_ALL_APPS); } } return true; case KeyEvent.KEYCODE_NOTIFICATION: if (!down) { toggleNotificationPanel(); - logKeyboardSystemsEvent(event, KeyboardLogEvent.TOGGLE_NOTIFICATION_PANEL); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_TOGGLE_NOTIFICATION_PANEL); } return true; case KeyEvent.KEYCODE_SEARCH: @@ -3705,7 +3718,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { switch (mSearchKeyBehavior) { case SEARCH_KEY_BEHAVIOR_TARGET_ACTIVITY: { launchTargetSearchActivity(); - logKeyboardSystemsEvent(event, KeyboardLogEvent.LAUNCH_SEARCH); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_SEARCH); return true; } case SEARCH_KEY_BEHAVIOR_DEFAULT_SEARCH: @@ -3718,7 +3732,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { if (firstDown) { int direction = (metaState & KeyEvent.META_SHIFT_MASK) != 0 ? -1 : 1; sendSwitchKeyboardLayout(event, focusedToken, direction); - logKeyboardSystemsEvent(event, KeyboardLogEvent.LANGUAGE_SWITCH); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_LANGUAGE_SWITCH); return true; } break; @@ -3737,11 +3752,13 @@ public class PhoneWindowManager implements WindowManagerPolicy { if (mPendingCapsLockToggle) { mInputManagerInternal.toggleCapsLock(event.getDeviceId()); mPendingCapsLockToggle = false; - logKeyboardSystemsEvent(event, KeyboardLogEvent.TOGGLE_CAPS_LOCK); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_TOGGLE_CAPS_LOCK); } else if (mPendingMetaAction) { if (!canceled) { launchAllAppsViaA11y(); - logKeyboardSystemsEvent(event, KeyboardLogEvent.ACCESSIBILITY_ALL_APPS); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_ACCESSIBILITY_ALL_APPS); } mPendingMetaAction = false; } @@ -3769,14 +3786,16 @@ public class PhoneWindowManager implements WindowManagerPolicy { if (mPendingCapsLockToggle) { mInputManagerInternal.toggleCapsLock(event.getDeviceId()); mPendingCapsLockToggle = false; - logKeyboardSystemsEvent(event, KeyboardLogEvent.TOGGLE_CAPS_LOCK); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_TOGGLE_CAPS_LOCK); return true; } } break; case KeyEvent.KEYCODE_CAPS_LOCK: if (!down) { - logKeyboardSystemsEvent(event, KeyboardLogEvent.TOGGLE_CAPS_LOCK); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_TOGGLE_CAPS_LOCK); } break; case KeyEvent.KEYCODE_STYLUS_BUTTON_PRIMARY: @@ -3790,10 +3809,12 @@ public class PhoneWindowManager implements WindowManagerPolicy { if (firstDown) { if (mSettingsKeyBehavior == SETTINGS_KEY_BEHAVIOR_NOTIFICATION_PANEL) { toggleNotificationPanel(); - logKeyboardSystemsEvent(event, KeyboardLogEvent.TOGGLE_NOTIFICATION_PANEL); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_TOGGLE_NOTIFICATION_PANEL); } else if (mSettingsKeyBehavior == SETTINGS_KEY_BEHAVIOR_SETTINGS_ACTIVITY) { showSystemSettings(); - logKeyboardSystemsEvent(event, KeyboardLogEvent.LAUNCH_SYSTEM_SETTINGS); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_SYSTEM_SETTINGS); } } return true; @@ -4739,7 +4760,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { // Handle special keys. switch (keyCode) { case KeyEvent.KEYCODE_BACK: { - logKeyboardSystemsEventOnActionUp(event, KeyboardLogEvent.BACK); + notifyKeyboardShortcutTriggeredOnActionUp(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_BACK); if (down) { // There may have other embedded activities on the same Task. Try to move the // focus before processing the back event. @@ -4760,8 +4782,12 @@ public class PhoneWindowManager implements WindowManagerPolicy { case KeyEvent.KEYCODE_VOLUME_DOWN: case KeyEvent.KEYCODE_VOLUME_UP: case KeyEvent.KEYCODE_VOLUME_MUTE: { - logKeyboardSystemsEventOnActionDown(event, - KeyboardLogEvent.getVolumeEvent(keyCode)); + int systemShortcut = keyCode == KEYCODE_VOLUME_DOWN + ? KeyboardSystemShortcut.SYSTEM_SHORTCUT_VOLUME_DOWN + : keyCode == KEYCODE_VOLUME_UP + ? KeyboardSystemShortcut.SYSTEM_SHORTCUT_VOLUME_UP + : KeyboardSystemShortcut.SYSTEM_SHORTCUT_VOLUME_MUTE; + notifyKeyboardShortcutTriggeredOnActionDown(event, systemShortcut); if (down) { sendSystemKeyToStatusBarAsync(event); @@ -4862,7 +4888,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { } case KeyEvent.KEYCODE_TV_POWER: { - logKeyboardSystemsEventOnActionUp(event, KeyboardLogEvent.TOGGLE_POWER); + notifyKeyboardShortcutTriggeredOnActionUp(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_TOGGLE_POWER); result &= ~ACTION_PASS_TO_USER; isWakeKey = false; // wake-up will be handled separately if (down && hdmiControlManager != null) { @@ -4872,7 +4899,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { } case KeyEvent.KEYCODE_POWER: { - logKeyboardSystemsEventOnActionUp(event, KeyboardLogEvent.TOGGLE_POWER); + notifyKeyboardShortcutTriggeredOnActionUp(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_TOGGLE_POWER); EventLogTags.writeInterceptPower( KeyEvent.actionToString(event.getAction()), mPowerKeyHandled ? 1 : 0, @@ -4895,14 +4923,16 @@ public class PhoneWindowManager implements WindowManagerPolicy { case KeyEvent.KEYCODE_SYSTEM_NAVIGATION_LEFT: // fall through case KeyEvent.KEYCODE_SYSTEM_NAVIGATION_RIGHT: { - logKeyboardSystemsEventOnActionUp(event, KeyboardLogEvent.SYSTEM_NAVIGATION); + notifyKeyboardShortcutTriggeredOnActionUp(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_SYSTEM_NAVIGATION); result &= ~ACTION_PASS_TO_USER; interceptSystemNavigationKey(event); break; } case KeyEvent.KEYCODE_SLEEP: { - logKeyboardSystemsEventOnActionUp(event, KeyboardLogEvent.SLEEP); + notifyKeyboardShortcutTriggeredOnActionUp(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_SLEEP); result &= ~ACTION_PASS_TO_USER; isWakeKey = false; if (!mPowerManager.isInteractive()) { @@ -4918,7 +4948,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { } case KeyEvent.KEYCODE_SOFT_SLEEP: { - logKeyboardSystemsEventOnActionUp(event, KeyboardLogEvent.SLEEP); + notifyKeyboardShortcutTriggeredOnActionUp(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_SLEEP); result &= ~ACTION_PASS_TO_USER; isWakeKey = false; if (!down) { @@ -4929,7 +4960,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { } case KeyEvent.KEYCODE_WAKEUP: { - logKeyboardSystemsEventOnActionUp(event, KeyboardLogEvent.WAKEUP); + notifyKeyboardShortcutTriggeredOnActionUp(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_WAKEUP); result &= ~ACTION_PASS_TO_USER; isWakeKey = true; break; @@ -4938,7 +4970,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { case KeyEvent.KEYCODE_MUTE: result &= ~ACTION_PASS_TO_USER; if (down && event.getRepeatCount() == 0) { - logKeyboardSystemsEvent(event, KeyboardLogEvent.SYSTEM_MUTE); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_SYSTEM_MUTE); toggleMicrophoneMuteFromKey(); } break; @@ -4953,7 +4986,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { case KeyEvent.KEYCODE_MEDIA_RECORD: case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: case KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK: { - logKeyboardSystemsEventOnActionUp(event, KeyboardLogEvent.MEDIA_KEY); + notifyKeyboardShortcutTriggeredOnActionUp(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_MEDIA_KEY); if (MediaSessionLegacyHelper.getHelper(mContext).isGlobalPriorityActive()) { // If the global session is active pass all media keys to it // instead of the active window. @@ -4998,7 +5032,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { 0 /* unused */, event.getEventTime() /* eventTime */); msg.setAsynchronous(true); msg.sendToTarget(); - logKeyboardSystemsEvent(event, KeyboardLogEvent.LAUNCH_ASSISTANT); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_ASSISTANT); } result &= ~ACTION_PASS_TO_USER; break; @@ -5009,7 +5044,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { Message msg = mHandler.obtainMessage(MSG_LAUNCH_VOICE_ASSIST_WITH_WAKE_LOCK); msg.setAsynchronous(true); msg.sendToTarget(); - logKeyboardSystemsEvent(event, KeyboardLogEvent.LAUNCH_VOICE_ASSISTANT); + notifyKeyboardShortcutTriggered(event, + KeyboardSystemShortcut.SYSTEM_SHORTCUT_LAUNCH_VOICE_ASSISTANT); } result &= ~ACTION_PASS_TO_USER; break; @@ -5975,10 +6011,10 @@ public class PhoneWindowManager implements WindowManagerPolicy { public void setSafeMode(boolean safeMode) { mSafeMode = safeMode; if (safeMode) { - performHapticFeedback(Process.myUid(), mContext.getOpPackageName(), + performHapticFeedback( HapticFeedbackConstants.SAFE_MODE_ENABLED, - "Safe Mode Enabled", HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING, - 0 /* privFlags */); + "Safe Mode Enabled" /* reason */, + HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING); } } @@ -6447,33 +6483,18 @@ public class PhoneWindowManager implements WindowManagerPolicy { Settings.Global.THEATER_MODE_ON, 0) == 1; } - private boolean performHapticFeedback(int effectId, String reason) { - return performHapticFeedback(Process.myUid(), mContext.getOpPackageName(), - effectId, reason, 0 /* flags */, 0 /* privFlags */); + private void performHapticFeedback(int effectId, String reason) { + performHapticFeedback(effectId, reason, 0 /* flags */); } - @Override - public boolean isGlobalKey(int keyCode) { - return mGlobalKeyManager.shouldHandleGlobalKey(keyCode); + private void performHapticFeedback( + int effectId, String reason, @HapticFeedbackConstants.Flags int flags) { + mVibrator.performHapticFeedback(effectId, reason, flags, 0 /* privFlags */); } @Override - public boolean performHapticFeedback(int uid, String packageName, int effectId, String reason, - int flags, int privFlags) { - if (!mVibrator.hasVibrator()) { - return false; - } - VibrationEffect effect = - mHapticFeedbackVibrationProvider.getVibrationForHapticFeedback(effectId); - if (effect == null) { - return false; - } - VibrationAttributes attrs = - mHapticFeedbackVibrationProvider.getVibrationAttributesForHapticFeedback( - effectId, flags, privFlags); - VibratorFrameworkStatsLogger.logPerformHapticsFeedbackIfKeyboard(uid, effectId); - mVibrator.vibrate(uid, packageName, effect, reason, attrs); - return true; + public boolean isGlobalKey(int keyCode) { + return mGlobalKeyManager.shouldHandleGlobalKey(keyCode); } @@ -6651,7 +6672,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { pw.print(" mLockScreenTimerActive="); pw.println(mLockScreenTimerActive); pw.print(prefix); pw.print("mKidsModeEnabled="); pw.println(mKidsModeEnabled); - mHapticFeedbackVibrationProvider.dump(prefix, pw); mGlobalKeyManager.dump(prefix, pw); mKeyCombinationManager.dump(prefix, pw); mSingleKeyGestureDetector.dump(prefix, pw); diff --git a/services/core/java/com/android/server/policy/WindowManagerPolicy.java b/services/core/java/com/android/server/policy/WindowManagerPolicy.java index 1b394f65c5eb..67f5f27b42eb 100644 --- a/services/core/java/com/android/server/policy/WindowManagerPolicy.java +++ b/services/core/java/com/android/server/policy/WindowManagerPolicy.java @@ -80,7 +80,6 @@ import android.os.RemoteException; import android.util.Slog; import android.util.proto.ProtoOutputStream; import android.view.Display; -import android.view.HapticFeedbackConstants; import android.view.IDisplayFoldListener; import android.view.KeyEvent; import android.view.KeyboardShortcutGroup; @@ -1077,13 +1076,6 @@ public interface WindowManagerPolicy extends WindowManagerPolicyConstants { public void enableScreenAfterBoot(); /** - * Call from application to perform haptic feedback on its window. - */ - public boolean performHapticFeedback(int uid, String packageName, int effectId, - String reason, @HapticFeedbackConstants.Flags int flags, - @HapticFeedbackConstants.PrivateFlags int privFlags); - - /** * Called when we have started keeping the screen on because a window * requesting this has become visible. */ diff --git a/services/core/java/com/android/server/vibrator/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/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..06e665e88b65 100644 --- a/services/core/java/com/android/server/wm/BackNavigationController.java +++ b/services/core/java/com/android/server/wm/BackNavigationController.java @@ -768,6 +768,48 @@ class BackNavigationController { } } + void onAppVisibilityChanged(@NonNull ActivityRecord ar, boolean visible) { + if (!mAnimationHandler.mComposed) { + return; + } + + final boolean openingTransition = mAnimationHandler.mOpenAnimAdaptor + .mPreparedOpenTransition != null; + // Detect if another transition is collecting during predictive back animation. + if (openingTransition && !visible && mAnimationHandler.isTarget(ar, false /* open */) + && ar.mTransitionController.isCollecting(ar)) { + final TransitionController controller = ar.mTransitionController; + boolean collectTask = false; + ActivityRecord changedActivity = null; + for (int i = mAnimationHandler.mOpenActivities.length - 1; i >= 0; --i) { + final ActivityRecord next = mAnimationHandler.mOpenActivities[i]; + if (next.mLaunchTaskBehind) { + // collect previous activity, so shell side can handle the transition. + controller.collect(next); + collectTask = true; + restoreLaunchBehind(next, true /* cancel */, false /* finishTransition */); + changedActivity = next; + } + } + if (collectTask && mAnimationHandler.mOpenAnimAdaptor.mAdaptors[0].mSwitchType + == AnimationHandler.TASK_SWITCH) { + final Task topTask = mAnimationHandler.mOpenAnimAdaptor.mAdaptors[0].getTopTask(); + if (topTask != null) { + WindowContainer parent = mAnimationHandler.mOpenActivities[0].getParent(); + while (parent != topTask && parent.isDescendantOf(topTask)) { + controller.collect(parent); + parent = parent.getParent(); + } + controller.collect(topTask); + } + } + if (changedActivity != null) { + changedActivity.getDisplayContent().ensureActivitiesVisible(null /* starting */, + true /* notifyClients */); + } + } + } + // For shell transition /** * Check whether the transition targets was animated by back gesture animation. @@ -784,7 +826,13 @@ class BackNavigationController { mAnimationHandler.markStartingSurfaceMatch(startTransaction); return; } - if (!isMonitoringFinishTransition() || targets.isEmpty()) { + if (targets.isEmpty()) { + return; + } + final boolean migratePredictToTransition = Flags.migratePredictiveBackTransition(); + if (migratePredictToTransition && !mAnimationHandler.mComposed) { + return; + } else if (!isMonitoringFinishTransition()) { return; } if (mAnimationHandler.hasTargetDetached()) { @@ -808,20 +856,27 @@ class BackNavigationController { mTmpCloseApps.add(wc); } } - final boolean matchAnimationTargets = isWaitBackTransition() + final boolean matchAnimationTargets; + if (migratePredictToTransition) { + matchAnimationTargets = + mAnimationHandler.containsBackAnimationTargets(mTmpOpenApps, mTmpCloseApps); + } else { + matchAnimationTargets = isWaitBackTransition() && (transition.mType == TRANSIT_CLOSE || transition.mType == TRANSIT_TO_BACK) && mAnimationHandler.containsBackAnimationTargets(mTmpOpenApps, mTmpCloseApps); + } ProtoLog.d(WM_DEBUG_BACK_PREVIEW, "onTransactionReady, opening: %s, closing: %s, animating: %s, match: %b", mTmpOpenApps, mTmpCloseApps, mAnimationHandler, matchAnimationTargets); - if (!matchAnimationTargets) { + // Don't cancel transition, let transition handler to handle it + if (!matchAnimationTargets && !migratePredictToTransition) { mNavigationMonitor.onTransitionReadyWhileNavigate(mTmpOpenApps, mTmpCloseApps); } else { if (mAnimationHandler.mPrepareCloseTransition != null) { Slog.e(TAG, "Gesture animation is applied on another transition?"); } mAnimationHandler.mPrepareCloseTransition = transition; - if (!Flags.migratePredictiveBackTransition()) { + if (!migratePredictToTransition) { // Because the target will reparent to transition root, so it cannot be controlled // by animation leash. Hide the close target when transition starts. startTransaction.hide(mAnimationHandler.mCloseAdaptor.mTarget.getSurfaceControl()); @@ -839,7 +894,19 @@ class BackNavigationController { } boolean isMonitorTransitionTarget(WindowContainer wc) { - if ((isWaitBackTransition() && mAnimationHandler.mPrepareCloseTransition != null) + if (Flags.migratePredictiveBackTransition()) { + if (!mAnimationHandler.mComposed) { + return false; + } + if (mAnimationHandler.mSwitchType == AnimationHandler.TASK_SWITCH + && wc.asActivityRecord() != null + || (mAnimationHandler.mSwitchType == AnimationHandler.ACTIVITY_SWITCH + && wc.asTask() != null)) { + return false; + } + return (mAnimationHandler.isTarget(wc, true /* open */) + || mAnimationHandler.isTarget(wc, false /* open */)); + } else if ((isWaitBackTransition() && mAnimationHandler.mPrepareCloseTransition != null) || (mAnimationHandler.mOpenAnimAdaptor != null && mAnimationHandler.mOpenAnimAdaptor.mPreparedOpenTransition != null)) { return mAnimationHandler.isTarget(wc, wc.isVisibleRequested() /* open */); @@ -1840,6 +1907,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. @@ -2015,7 +2115,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 +2126,12 @@ class BackNavigationController { if (!ar.isSnapshotOrientationCompatible(snapshot)) { return false; } + final int appNightMode = ar.getConfiguration().uiMode + & Configuration.UI_MODE_NIGHT_MASK; + final int snapshotNightMode = snapshot.getUiMode() & Configuration.UI_MODE_NIGHT_MASK; + if (appNightMode != snapshotNightMode) { + return false; + } oneComponentMatch |= ar.isSnapshotComponentCompatible(snapshot); } return oneComponentMatch; diff --git a/services/core/java/com/android/server/wm/DesktopModeBoundsCalculator.java b/services/core/java/com/android/server/wm/DesktopModeBoundsCalculator.java index f566df5fd147..8f1828d741c5 100644 --- a/services/core/java/com/android/server/wm/DesktopModeBoundsCalculator.java +++ b/services/core/java/com/android/server/wm/DesktopModeBoundsCalculator.java @@ -109,6 +109,13 @@ public final class DesktopModeBoundsCalculator { if (!DesktopModeFlagsUtil.DYNAMIC_INITIAL_BOUNDS.isEnabled(activity.mWmService.mContext)) { return centerInScreen(idealSize, screenBounds); } + if (activity.mAppCompatController.getAppCompatAspectRatioOverrides() + .hasFullscreenOverride()) { + // If the activity has a fullscreen override applied, it should be treated as + // resizeable and match the device orientation. Thus the ideal size can be + // applied. + return centerInScreen(idealSize, screenBounds); + } // TODO(b/353457301): Replace with app compat aspect ratio method when refactoring complete. float appAspectRatio = calculateAspectRatio(task, activity); final float tdaWidth = stableBounds.width(); diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 9c8c759765bc..fcc6b11d46c5 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -1804,9 +1804,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp return; } final int displayRotation = getRotation(); - final int rotation = ar.isVisible() - ? ar.getWindowConfiguration().getDisplayRotation() - : mDisplayRotation.rotationForOrientation(orientation, displayRotation); + final int rotation = mDisplayRotation.rotationForOrientation(orientation, displayRotation); if (rotation == displayRotation) { return; } @@ -6710,6 +6708,11 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp final boolean rotationChanged = super.setIgnoreOrientationRequest(ignoreOrientationRequest); mWmService.mDisplayWindowSettings.setIgnoreOrientationRequest( this, mSetIgnoreOrientationRequest); + if (ignoreOrientationRequest && mWmService.mFlags.mRespectNonTopVisibleFixedOrientation) { + forAllActivities(r -> { + r.finishFixedRotationTransform(); + }); + } return rotationChanged; } diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java index 8272e1609e0d..a5da5e7cc0de 100644 --- a/services/core/java/com/android/server/wm/DisplayRotation.java +++ b/services/core/java/com/android/server/wm/DisplayRotation.java @@ -1239,7 +1239,6 @@ public class DisplayRotation { * @param lastRotation The most recently used rotation. * @return The surface rotation to use. */ - @VisibleForTesting @Surface.Rotation int rotationForOrientation(@ScreenOrientation int orientation, @Surface.Rotation int lastRotation) { diff --git a/services/core/java/com/android/server/wm/DisplayWindowSettings.java b/services/core/java/com/android/server/wm/DisplayWindowSettings.java index 7a95c2d6d934..2f0ee171b5ba 100644 --- a/services/core/java/com/android/server/wm/DisplayWindowSettings.java +++ b/services/core/java/com/android/server/wm/DisplayWindowSettings.java @@ -129,21 +129,33 @@ class DisplayWindowSettings { @WindowConfiguration.WindowingMode private int getWindowingModeLocked(@NonNull SettingsProvider.SettingsEntry settings, @NonNull DisplayContent dc) { - int windowingMode = settings.mWindowingMode; + final int windowingModeFromDisplaySettings = settings.mWindowingMode; // This display used to be in freeform, but we don't support freeform anymore, so fall // back to fullscreen. - if (windowingMode == WindowConfiguration.WINDOWING_MODE_FREEFORM + if (windowingModeFromDisplaySettings == WindowConfiguration.WINDOWING_MODE_FREEFORM && !mService.mAtmService.mSupportsFreeformWindowManagement) { return WindowConfiguration.WINDOWING_MODE_FULLSCREEN; } + if (windowingModeFromDisplaySettings != WindowConfiguration.WINDOWING_MODE_UNDEFINED) { + return windowingModeFromDisplaySettings; + } // No record is present so use default windowing mode policy. - if (windowingMode == WindowConfiguration.WINDOWING_MODE_UNDEFINED) { - windowingMode = mService.mAtmService.mSupportsFreeformWindowManagement - && (mService.mIsPc || dc.forceDesktopMode()) - ? WindowConfiguration.WINDOWING_MODE_FREEFORM - : WindowConfiguration.WINDOWING_MODE_FULLSCREEN; + final boolean forceFreeForm = mService.mAtmService.mSupportsFreeformWindowManagement + && (mService.mIsPc || dc.forceDesktopMode()); + if (forceFreeForm) { + return WindowConfiguration.WINDOWING_MODE_FREEFORM; + } + final int currentWindowingMode = dc.getDefaultTaskDisplayArea().getWindowingMode(); + if (currentWindowingMode == WindowConfiguration.WINDOWING_MODE_UNDEFINED) { + // No record preset in settings + no mode set via the display area policy. + // Move to fullscreen as a fallback. + return WindowConfiguration.WINDOWING_MODE_FULLSCREEN; + } + if (currentWindowingMode == WindowConfiguration.WINDOWING_MODE_FREEFORM) { + // Freeform was enabled before but disabled now, the TDA should now move to fullscreen. + return WindowConfiguration.WINDOWING_MODE_FULLSCREEN; } - return windowingMode; + return currentWindowingMode; } @WindowConfiguration.WindowingMode diff --git a/services/core/java/com/android/server/wm/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/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..f86d307d97bd 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java @@ -708,17 +708,15 @@ final class PolicyDefinition<V> { } @Nullable - static <V> PolicyKey readPolicyKeyFromXml(TypedXmlPullParser parser) + static PolicyKey readPolicyKeyFromXml(TypedXmlPullParser parser) throws XmlPullParserException, IOException { - // TODO: can we avoid casting? PolicyKey policyKey = PolicyKey.readGenericPolicyKeyFromXml(parser); if (policyKey == null) { Slogf.wtf(TAG, "Error parsing PolicyKey, GenericPolicyKey is null"); return null; } - PolicyDefinition<PolicyValue<V>> genericPolicyDefinition = - (PolicyDefinition<PolicyValue<V>>) POLICY_DEFINITIONS.get( - policyKey.getIdentifier()); + PolicyDefinition<?> genericPolicyDefinition = + POLICY_DEFINITIONS.get(policyKey.getIdentifier()); if (genericPolicyDefinition == null) { Slogf.wtf(TAG, "Error parsing PolicyKey, Unknown generic policy key: " + policyKey); return null; diff --git a/services/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/InputMethodSubtypeSwitchingControllerTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java index dc0373239547..aee7242d4604 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); 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/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/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..14ad15e23791 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java @@ -15,7 +15,9 @@ */ package com.android.server.notification; +import static android.app.Notification.FLAG_AUTOGROUP_SUMMARY; import static android.app.Notification.FLAG_BUBBLE; +import static android.app.Notification.FLAG_GROUP_SUMMARY; import static android.app.Notification.GROUP_ALERT_ALL; import static android.app.Notification.GROUP_ALERT_CHILDREN; import static android.app.Notification.GROUP_ALERT_SUMMARY; @@ -539,6 +541,36 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { return r; } + private NotificationRecord getAutogroupSummaryNotificationRecord(int id, String groupKey, + int groupAlertBehavior, UserHandle userHandle, String packageName) { + final Builder builder = new Builder(getContext()) + .setContentTitle("foo") + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setPriority(Notification.PRIORITY_HIGH) + .setFlag(FLAG_GROUP_SUMMARY | FLAG_AUTOGROUP_SUMMARY, true); + + int defaults = 0; + defaults |= Notification.DEFAULT_SOUND; + mChannel.setSound(Settings.System.DEFAULT_NOTIFICATION_URI, + Notification.AUDIO_ATTRIBUTES_DEFAULT); + + builder.setDefaults(defaults); + builder.setGroup(groupKey); + builder.setGroupAlertBehavior(groupAlertBehavior); + Notification n = builder.build(); + + Context context = spy(getContext()); + PackageManager packageManager = spy(context.getPackageManager()); + when(context.getPackageManager()).thenReturn(packageManager); + when(packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)).thenReturn(false); + + StatusBarNotification sbn = new StatusBarNotification(packageName, packageName, id, mTag, + mUid, mPid, n, userHandle, null, System.currentTimeMillis()); + NotificationRecord r = new NotificationRecord(context, sbn, mChannel); + mService.addNotification(r); + return r; + } + // // Convenience functions for interacting with mocks // @@ -2603,6 +2635,79 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { } @Test + public void testBeepVolume_politeNotif_justSummaries() throws Exception { + mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS); + mSetFlagsRule.disableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS); + TestableFlagResolver flagResolver = new TestableFlagResolver(); + flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME1, 50); + flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME2, 0); + // NOTIFICATION_COOLDOWN_ALL setting is enabled + Settings.System.putInt(getContext().getContentResolver(), + Settings.System.NOTIFICATION_COOLDOWN_ALL, 1); + initAttentionHelper(flagResolver); + + NotificationRecord summary = getBeepyNotificationRecord("a", GROUP_ALERT_ALL); + summary.getNotification().flags |= Notification.FLAG_GROUP_SUMMARY; + + // first update at 100% volume + mAttentionHelper.buzzBeepBlinkLocked(summary, DEFAULT_SIGNALS); + assertNotEquals(-1, summary.getLastAudiblyAlertedMs()); + verifyBeepVolume(1.0f); + Mockito.reset(mRingtonePlayer); + + // update should beep at 50% volume + summary = getBeepyNotificationRecord("a", GROUP_ALERT_ALL); + summary.getNotification().flags |= Notification.FLAG_GROUP_SUMMARY; + mAttentionHelper.buzzBeepBlinkLocked(summary, DEFAULT_SIGNALS); + assertNotEquals(-1, summary.getLastAudiblyAlertedMs()); + verifyBeepVolume(0.5f); + Mockito.reset(mRingtonePlayer); + + // next update at 0% volume + mAttentionHelper.buzzBeepBlinkLocked(summary, DEFAULT_SIGNALS); + assertEquals(-1, summary.getLastAudiblyAlertedMs()); + verifyBeepVolume(0.0f); + + verify(mAccessibilityService, times(3)).sendAccessibilityEvent(any(), anyInt()); + } + + @Test + public void testBeepVolume_politeNotif_autogroupSummary() throws Exception { + mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS); + mSetFlagsRule.disableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS); + TestableFlagResolver flagResolver = new TestableFlagResolver(); + flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME1, 50); + flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME2, 0); + // NOTIFICATION_COOLDOWN_ALL setting is enabled + Settings.System.putInt(getContext().getContentResolver(), + Settings.System.NOTIFICATION_COOLDOWN_ALL, 1); + initAttentionHelper(flagResolver); + + // child should beep at 100% volume + NotificationRecord child = getBeepyNotificationRecord("a", GROUP_ALERT_ALL); + mAttentionHelper.buzzBeepBlinkLocked(child, DEFAULT_SIGNALS); + assertNotEquals(-1, child.getLastAudiblyAlertedMs()); + verifyBeepVolume(1.0f); + Mockito.reset(mRingtonePlayer); + + // summary 0% volume (GROUP_ALERT_CHILDREN) + NotificationRecord summary = getAutogroupSummaryNotificationRecord(mId, "a", + GROUP_ALERT_CHILDREN, mUser, mPkg); + mAttentionHelper.buzzBeepBlinkLocked(summary, DEFAULT_SIGNALS); + verifyNeverBeep(); + assertFalse(summary.isInterruptive()); + assertEquals(-1, summary.getLastAudiblyAlertedMs()); + Mockito.reset(mRingtonePlayer); + + // next update at 50% volume because autogroup summary was ignored + mAttentionHelper.buzzBeepBlinkLocked(child, DEFAULT_SIGNALS); + assertNotEquals(-1, child.getLastAudiblyAlertedMs()); + verifyBeepVolume(0.5f); + + verify(mAccessibilityService, times(3)).sendAccessibilityEvent(any(), anyInt()); + } + + @Test public void testBeepVolume_politeNotif_applyPerApp() throws Exception { mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS); mSetFlagsRule.disableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index 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/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/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/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..ad6db2d07336 100644 --- a/telephony/java/android/telephony/satellite/SatelliteManager.java +++ b/telephony/java/android/telephony/satellite/SatelliteManager.java @@ -412,6 +412,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 +448,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 {} diff --git a/telephony/java/android/telephony/satellite/stub/ISatellite.aidl b/telephony/java/android/telephony/satellite/stub/ISatellite.aidl index b82396e710fd..e66a0824f545 100644 --- a/telephony/java/android/telephony/satellite/stub/ISatellite.aidl +++ b/telephony/java/android/telephony/satellite/stub/ISatellite.aidl @@ -206,77 +206,6 @@ oneway interface ISatellite { void stopSendingSatellitePointingInfo(in IIntegerConsumer resultCallback); /** - * Provision the device with a satellite provider. - * This is needed if the provider allows dynamic registration. - * Once provisioned, ISatelliteListener#onSatelliteProvisionStateChanged should report true. - * - * @param token The token to be used as a unique identifier for provisioning with satellite - * gateway. - * @param provisionData Data from the provisioning app that can be used by provisioning server - * @param resultCallback The callback to receive the error code result of the operation. - * - * Valid result codes returned: - * SatelliteResult:SATELLITE_RESULT_SUCCESS - * SatelliteResult:SATELLITE_RESULT_SERVICE_ERROR - * SatelliteResult:SATELLITE_RESULT_MODEM_ERROR - * SatelliteResult:SATELLITE_RESULT_NETWORK_ERROR - * SatelliteResult:SATELLITE_RESULT_INVALID_MODEM_STATE - * SatelliteResult:SATELLITE_RESULT_INVALID_ARGUMENTS - * SatelliteResult:SATELLITE_RESULT_RADIO_NOT_AVAILABLE - * SatelliteResult:SATELLITE_RESULT_REQUEST_NOT_SUPPORTED - * SatelliteResult:SATELLITE_RESULT_NO_RESOURCES - * SatelliteResult:SATELLITE_RESULT_REQUEST_ABORTED - * SatelliteResult:SATELLITE_RESULT_NETWORK_TIMEOUT - */ - void provisionSatelliteService(in String token, in byte[] provisionData, - in IIntegerConsumer resultCallback); - - /** - * Deprovision the device with the satellite provider. - * This is needed if the provider allows dynamic registration. - * Once deprovisioned, ISatelliteListener#onSatelliteProvisionStateChanged should report false. - * - * @param token The token of the device/subscription to be deprovisioned. - * @param resultCallback The callback to receive the error code result of the operation. - * - * Valid result codes returned: - * SatelliteResult:SATELLITE_RESULT_SUCCESS - * SatelliteResult:SATELLITE_RESULT_SERVICE_ERROR - * SatelliteResult:SATELLITE_RESULT_MODEM_ERROR - * SatelliteResult:SATELLITE_RESULT_NETWORK_ERROR - * SatelliteResult:SATELLITE_RESULT_INVALID_MODEM_STATE - * SatelliteResult:SATELLITE_RESULT_INVALID_ARGUMENTS - * SatelliteResult:SATELLITE_RESULT_RADIO_NOT_AVAILABLE - * SatelliteResult:SATELLITE_RESULT_REQUEST_NOT_SUPPORTED - * SatelliteResult:SATELLITE_RESULT_NO_RESOURCES - * SatelliteResult:SATELLITE_RESULT_REQUEST_ABORTED - * SatelliteResult:SATELLITE_RESULT_NETWORK_TIMEOUT - */ - void deprovisionSatelliteService(in String token, in IIntegerConsumer resultCallback); - - /** - * Request to get whether this device is provisioned with a satellite provider. - * - * @param resultCallback The callback to receive the error code result of the operation. - * This must only be sent when the result is not - * SatelliteResult#SATELLITE_RESULT_SUCCESS. - * @param callback If the result is SatelliteResult#SATELLITE_RESULT_SUCCESS, the callback to - * receive whether this device is provisioned with a satellite provider. - * - * Valid result codes returned: - * SatelliteResult:SATELLITE_RESULT_SUCCESS - * SatelliteResult:SATELLITE_RESULT_SERVICE_ERROR - * SatelliteResult:SATELLITE_RESULT_MODEM_ERROR - * SatelliteResult:SATELLITE_RESULT_INVALID_MODEM_STATE - * SatelliteResult:SATELLITE_RESULT_INVALID_ARGUMENTS - * SatelliteResult:SATELLITE_RESULT_RADIO_NOT_AVAILABLE - * SatelliteResult:SATELLITE_RESULT_REQUEST_NOT_SUPPORTED - * SatelliteResult:SATELLITE_RESULT_NO_RESOURCES - */ - void requestIsSatelliteProvisioned(in IIntegerConsumer resultCallback, - in IBooleanConsumer callback); - - /** * Poll the pending datagrams to be received over satellite. * The satellite service should check if there are any pending datagrams to be received over * satellite and report them via ISatelliteListener#onSatelliteDatagramsReceived. diff --git a/telephony/java/android/telephony/satellite/stub/ISatelliteListener.aidl b/telephony/java/android/telephony/satellite/stub/ISatelliteListener.aidl index b4eb15fde632..3f2fce2b9f1d 100644 --- a/telephony/java/android/telephony/satellite/stub/ISatelliteListener.aidl +++ b/telephony/java/android/telephony/satellite/stub/ISatelliteListener.aidl @@ -28,13 +28,6 @@ import android.telephony.satellite.stub.SatelliteModemState; */ oneway interface ISatelliteListener { /** - * Indicates that the satellite provision state has changed. - * - * @param provisioned True means the service is provisioned and false means it is not. - */ - void onSatelliteProvisionStateChanged(in boolean provisioned); - - /** * Indicates that new datagrams have been received on the device. * * @param datagram New datagram that was received. diff --git a/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java b/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java index d8b4974f23b9..c50e469e83cb 100644 --- a/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java +++ b/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java @@ -142,32 +142,6 @@ public class SatelliteImplBase extends SatelliteService { } @Override - public void provisionSatelliteService(String token, byte[] provisionData, - IIntegerConsumer resultCallback) throws RemoteException { - executeMethodAsync( - () -> SatelliteImplBase.this - .provisionSatelliteService(token, provisionData, resultCallback), - "provisionSatelliteService"); - } - - @Override - public void deprovisionSatelliteService(String token, IIntegerConsumer resultCallback) - throws RemoteException { - executeMethodAsync( - () -> SatelliteImplBase.this.deprovisionSatelliteService(token, resultCallback), - "deprovisionSatelliteService"); - } - - @Override - public void requestIsSatelliteProvisioned(IIntegerConsumer resultCallback, - IBooleanConsumer callback) throws RemoteException { - executeMethodAsync( - () -> SatelliteImplBase.this - .requestIsSatelliteProvisioned(resultCallback, callback), - "requestIsSatelliteProvisioned"); - } - - @Override public void pollPendingSatelliteDatagrams(IIntegerConsumer resultCallback) throws RemoteException { executeMethodAsync( @@ -487,85 +461,6 @@ public class SatelliteImplBase extends SatelliteService { } /** - * Provision the device with a satellite provider. - * This is needed if the provider allows dynamic registration. - * Once provisioned, ISatelliteListener#onSatelliteProvisionStateChanged should report true. - * - * @param token The token to be used as a unique identifier for provisioning with satellite - * gateway. - * @param provisionData Data from the provisioning app that can be used by provisioning - * server - * @param resultCallback The callback to receive the error code result of the operation. - * - * Valid result codes returned: - * SatelliteResult:SATELLITE_RESULT_SUCCESS - * SatelliteResult:SATELLITE_RESULT_SERVICE_ERROR - * SatelliteResult:SATELLITE_RESULT_MODEM_ERROR - * SatelliteResult:SATELLITE_RESULT_NETWORK_ERROR - * SatelliteResult:SATELLITE_RESULT_INVALID_MODEM_STATE - * SatelliteResult:SATELLITE_RESULT_INVALID_ARGUMENTS - * SatelliteResult:SATELLITE_RESULT_RADIO_NOT_AVAILABLE - * SatelliteResult:SATELLITE_RESULT_REQUEST_NOT_SUPPORTED - * SatelliteResult:SATELLITE_RESULT_NO_RESOURCES - * SatelliteResult:SATELLITE_RESULT_REQUEST_ABORTED - * SatelliteResult:SATELLITE_RESULT_NETWORK_TIMEOUT - */ - public void provisionSatelliteService(@NonNull String token, @NonNull byte[] provisionData, - @NonNull IIntegerConsumer resultCallback) { - // stub implementation - } - - /** - * Deprovision the device with the satellite provider. - * This is needed if the provider allows dynamic registration. - * Once deprovisioned, ISatelliteListener#onSatelliteProvisionStateChanged should report false. - * - * @param token The token of the device/subscription to be deprovisioned. - * @param resultCallback The callback to receive the error code result of the operation. - * - * Valid result codes returned: - * SatelliteResult:SATELLITE_RESULT_SUCCESS - * SatelliteResult:SATELLITE_RESULT_SERVICE_ERROR - * SatelliteResult:SATELLITE_RESULT_MODEM_ERROR - * SatelliteResult:SATELLITE_RESULT_NETWORK_ERROR - * SatelliteResult:SATELLITE_RESULT_INVALID_MODEM_STATE - * SatelliteResult:SATELLITE_RESULT_INVALID_ARGUMENTS - * SatelliteResult:SATELLITE_RESULT_RADIO_NOT_AVAILABLE - * SatelliteResult:SATELLITE_RESULT_REQUEST_NOT_SUPPORTED - * SatelliteResult:SATELLITE_RESULT_NO_RESOURCES - * SatelliteResult:SATELLITE_RESULT_REQUEST_ABORTED - * SatelliteResult:SATELLITE_RESULT_NETWORK_TIMEOUT - */ - public void deprovisionSatelliteService(@NonNull String token, - @NonNull IIntegerConsumer resultCallback) { - // stub implementation - } - - /** - * Request to get whether this device is provisioned with a satellite provider. - * - * @param resultCallback The callback to receive the error code result of the operation. - * This must only be sent when the result is not - * SatelliteResult#SATELLITE_RESULT_SUCCESS. - * @param callback If the result is SatelliteResult#SATELLITE_RESULT_SUCCESS, the callback to - * receive whether this device is provisioned with a satellite provider. - * - * Valid result codes returned: - * SatelliteResult:SATELLITE_RESULT_SUCCESS - * SatelliteResult:SATELLITE_RESULT_SERVICE_ERROR - * SatelliteResult:SATELLITE_RESULT_MODEM_ERROR - * SatelliteResult:SATELLITE_RESULT_INVALID_MODEM_STATE - * SatelliteResult:SATELLITE_RESULT_INVALID_ARGUMENTS - * SatelliteResult:SATELLITE_RESULT_RADIO_NOT_AVAILABLE - * SatelliteResult:SATELLITE_RESULT_REQUEST_NOT_SUPPORTED - * SatelliteResult:SATELLITE_RESULT_NO_RESOURCES - */ - public void requestIsSatelliteProvisioned(@NonNull IIntegerConsumer resultCallback, - @NonNull IBooleanConsumer callback) { - // stub implementation - } - - /** * Poll the pending datagrams to be received over satellite. * The satellite service should check if there are any pending datagrams to be received over * satellite and report them via ISatelliteListener#onSatelliteDatagramsReceived. diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl index 3dbda7ae10a3..2f8e95713eba 100644 --- a/telephony/java/com/android/internal/telephony/ITelephony.aidl +++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl @@ -3007,16 +3007,18 @@ interface ITelephony { */ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(" + "android.Manifest.permission.SATELLITE_COMMUNICATION)") - void setDeviceAlignedWithSatellite(int subId, in boolean isAligned); + void setDeviceAlignedWithSatellite(int subId, boolean isAligned); /** * This API can be used by only CTS to update satellite vendor service package name. * * @param servicePackageName The package name of the satellite vendor service. + * @param provisioned Whether satellite should be provisioned or not. + * * @return {@code true} if the satellite vendor service is set successfully, * {@code false} otherwise. */ - boolean setSatelliteServicePackageName(in String servicePackageName); + boolean setSatelliteServicePackageName(in String servicePackageName, in String provisioned); /** * This API can be used by only CTS to update satellite gateway service package name. diff --git a/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..7b227cebe2ae 100644 --- a/tools/aapt2/Android.bp +++ b/tools/aapt2/Android.bp @@ -189,6 +189,7 @@ cc_test_host { "integration-tests/CommandTests/**/*", "integration-tests/ConvertTest/**/*", "integration-tests/DumpTest/**/*", + ":resource-flagging-test-app-apk", ], } 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..88cc4dab2a96 100644 --- a/tools/aapt2/cmd/Link.cpp +++ b/tools/aapt2/cmd/Link.cpp @@ -1840,11 +1840,51 @@ 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); + } + } + } + } + } + const bool keep_raw_values = (context_->GetPackageType() == PackageType::kStaticLib) || options_.keep_raw_values; bool result = FlattenXml(context_, *manifest, kAndroidManifestPath, keep_raw_values, diff --git a/tools/aapt2/integration-tests/FlaggedResourcesTest/Android.bp b/tools/aapt2/integration-tests/FlaggedResourcesTest/Android.bp new file mode 100644 index 000000000000..efa8e0494856 --- /dev/null +++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/Android.bp @@ -0,0 +1,65 @@ +// 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)", +} + +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..8d0146511d1d 100644 --- a/core/tests/resourceflaggingtests/flagged_resources_res/values/bools.xml +++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/bools.xml 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/FlaggedResources_test.cpp b/tools/aapt2/link/FlaggedResources_test.cpp new file mode 100644 index 000000000000..03c4ac9c83a2 --- /dev/null +++ b/tools/aapt2/link/FlaggedResources_test.cpp @@ -0,0 +1,54 @@ +/* + * 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(); +} + +TEST_F(FlaggedResourcesTest, DisabledStringRemoved) { + 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); +} + +} // 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 73cfd3f7ceec..36bfbefdb086 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt @@ -58,17 +58,19 @@ class HostStubGen(val options: HostStubGenOptions) { // Dump the classes, if specified. options.inputJarDumpFile.ifSet { - PrintWriter(it).use { pw -> allClasses.dump(pw) } - log.i("Dump file created at $it") + log.iTime("Dump file created at $it") { + PrintWriter(it).use { pw -> allClasses.dump(pw) } + } } options.inputJarAsKeepAllFile.ifSet { - PrintWriter(it).use { - pw -> allClasses.forEach { - classNode -> printAsTextPolicy(pw, classNode) + log.iTime("Dump file created at $it") { + PrintWriter(it).use { pw -> + allClasses.forEach { classNode -> + printAsTextPolicy(pw, classNode) + } } } - log.i("Dump file created at $it") } // Build the filters. @@ -85,20 +87,24 @@ class HostStubGen(val options: HostStubGenOptions) { 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") } } @@ -213,35 +219,54 @@ class HostStubGen(val options: HostStubGenOptions) { 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, + 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) + 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 { 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/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. */ |