diff options
345 files changed, 13834 insertions, 3406 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index 3620a11fe036..7d9e95bb12ee 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -1108,6 +1108,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/api/Android.bp b/api/Android.bp index d931df165a8f..341be3d53844 100644 --- a/api/Android.bp +++ b/api/Android.bp @@ -284,7 +284,7 @@ packages_to_document = [ // These are libs from framework-internal-utils that are required (i.e. being referenced) // from framework-non-updatable-sources. Add more here when there's a need. // DO NOT add the entire framework-internal-utils. It might cause unnecessary circular -// dependencies gets bigger. +// dependencies when the list gets bigger. android_non_updatable_stubs_libs = [ "android.hardware.cas-V1.2-java", "android.hardware.health-V1.0-java-constants", @@ -384,6 +384,11 @@ non_updatable_api_deps_on_modules = [ "sdk_system_current_android", ] +java_defaults { + name: "module-classpath-java-defaults", + libs: non_updatable_api_deps_on_modules, +} + // Defaults with module APIs in the classpath (mostly from prebuilts). // Suitable for compiling android-non-updatable. stubs_defaults { diff --git a/api/StubLibraries.bp b/api/StubLibraries.bp index 8dfddf0e13c8..d991da59f167 100644 --- a/api/StubLibraries.bp +++ b/api/StubLibraries.bp @@ -563,8 +563,12 @@ java_library { java_defaults { name: "android-non-updatable_from_text_defaults", + defaults: ["android-non-updatable-stubs-libs-defaults"], static_libs: ["framework-res-package-jar"], libs: ["stub-annotations"], + sdk_version: "none", + system_modules: "none", + previous_api: ":android.api.public.latest", } java_defaults { @@ -582,10 +586,10 @@ java_api_library { "api-stubs-docs-non-updatable.api.contribution", ], defaults: ["android-non-updatable_everything_from_text_defaults"], - full_api_surface_stub: "android_stubs_current.from-text", // Use full Android API not just the non-updatable API as the latter is incomplete // and can result in incorrect behavior. previous_api: ":android.api.combined.public.latest", + libs: ["all-modules-public-stubs"], } java_api_library { @@ -596,10 +600,10 @@ java_api_library { "system-api-stubs-docs-non-updatable.api.contribution", ], defaults: ["android-non-updatable_everything_from_text_defaults"], - full_api_surface_stub: "android_system_stubs_current.from-text", // Use full Android API not just the non-updatable API as the latter is incomplete // and can result in incorrect behavior. previous_api: ":android.api.combined.system.latest", + libs: ["all-modules-system-stubs"], } java_api_library { @@ -611,10 +615,10 @@ java_api_library { "test-api-stubs-docs-non-updatable.api.contribution", ], defaults: ["android-non-updatable_everything_from_text_defaults"], - full_api_surface_stub: "android_test_stubs_current.from-text", // Use full Android API not just the non-updatable API as the latter is incomplete // and can result in incorrect behavior. previous_api: ":android.api.combined.test.latest", + libs: ["all-modules-system-stubs"], } java_api_library { @@ -625,8 +629,10 @@ java_api_library { "system-api-stubs-docs-non-updatable.api.contribution", "module-lib-api-stubs-docs-non-updatable.api.contribution", ], - defaults: ["android-non-updatable_everything_from_text_defaults"], - full_api_surface_stub: "android_module_lib_stubs_current_full.from-text", + defaults: [ + "module-classpath-java-defaults", + "android-non-updatable_everything_from_text_defaults", + ], // Use full Android API not just the non-updatable API as the latter is incomplete // and can result in incorrect behavior. previous_api: ":android.api.combined.module-lib.latest", @@ -644,14 +650,16 @@ java_api_library { "test-api-stubs-docs-non-updatable.api.contribution", "module-lib-api-stubs-docs-non-updatable.api.contribution", ], - defaults: ["android-non-updatable_everything_from_text_defaults"], - full_api_surface_stub: "android_test_module_lib_stubs_current.from-text", + defaults: [ + "module-classpath-java-defaults", + "android-non-updatable_everything_from_text_defaults", + ], // No need to specify previous_api as this is not used for compiling against. - // This module is only used for hiddenapi, and other modules should not // depend on this module. visibility: ["//visibility:private"], + libs: ["all-modules-system-stubs"], } java_defaults { @@ -665,7 +673,7 @@ java_defaults { } java_library { - name: "android_stubs_current.from-source", + name: "android_stubs_current", static_libs: [ "all-modules-public-stubs", "android-non-updatable.stubs", @@ -675,7 +683,7 @@ java_library { } java_library { - name: "android_stubs_current_exportable.from-source", + name: "android_stubs_current_exportable", static_libs: [ "all-modules-public-stubs-exportable", "android-non-updatable.stubs.exportable", @@ -685,7 +693,7 @@ java_library { } java_library { - name: "android_system_stubs_current.from-source", + name: "android_system_stubs_current", static_libs: [ "all-modules-system-stubs", "android-non-updatable.stubs.system", @@ -698,7 +706,7 @@ java_library { } java_library { - name: "android_system_stubs_current_exportable.from-source", + name: "android_system_stubs_current_exportable", static_libs: [ "all-modules-system-stubs-exportable", "android-non-updatable.stubs.exportable.system", @@ -722,7 +730,7 @@ java_library { } java_library { - name: "android_test_stubs_current.from-source", + name: "android_test_stubs_current", static_libs: [ // Updatable modules do not have test APIs, but we want to include their SystemApis, like we // include the SystemApi of framework-non-updatable-sources. @@ -739,7 +747,7 @@ java_library { } java_library { - name: "android_test_stubs_current_exportable.from-source", + name: "android_test_stubs_current_exportable", static_libs: [ // Updatable modules do not have test APIs, but we want to include their SystemApis, like we // include the SystemApi of framework-non-updatable-sources. @@ -760,7 +768,7 @@ java_library { // This module does not need to be copied to dist java_library { - name: "android_test_frameworks_core_stubs_current.from-source", + name: "android_test_frameworks_core_stubs_current", static_libs: [ "all-updatable-modules-system-stubs", "android-non-updatable.stubs.test", @@ -772,7 +780,7 @@ java_library { } java_library { - name: "android_module_lib_stubs_current.from-source", + name: "android_module_lib_stubs_current", defaults: [ "android.jar_defaults", ], @@ -785,7 +793,7 @@ java_library { } java_library { - name: "android_module_lib_stubs_current_exportable.from-source", + name: "android_module_lib_stubs_current_exportable", defaults: [ "android.jar_defaults", "android_stubs_dists_default", @@ -801,20 +809,20 @@ java_library { } java_library { - name: "android_system_server_stubs_current.from-source", + name: "android_system_server_stubs_current", defaults: [ "android.jar_defaults", ], srcs: [":services-non-updatable-stubs"], installable: false, static_libs: [ - "android_module_lib_stubs_current.from-source", + "android_module_lib_stubs_current", ], visibility: ["//frameworks/base/services"], } java_library { - name: "android_system_server_stubs_current_exportable.from-source", + name: "android_system_server_stubs_current_exportable", defaults: [ "android.jar_defaults", "android_stubs_dists_default", @@ -822,7 +830,7 @@ java_library { srcs: [":services-non-updatable-stubs{.exportable}"], installable: false, static_libs: [ - "android_module_lib_stubs_current_exportable.from-source", + "android_module_lib_stubs_current_exportable", ], dist: { dir: "apistubs/android/system-server", @@ -897,215 +905,6 @@ java_genrule { }, } -// -// Java API defaults and libraries for single tree build -// - -java_defaults { - name: "stub-annotation-defaults", - libs: [ - "stub-annotations", - ], - static_libs: [ - // stub annotations do not contribute to the API surfaces but are statically - // linked in the stubs for API surfaces (see frameworks/base/StubLibraries.bp). - // This is because annotation processors insist on loading the classes for any - // annotations found, thus should exist inside android.jar. - "private-stub-annotations-jar", - ], - is_stubs_module: true, -} - -// Listing of API domains contribution and dependencies per API surfaces -java_defaults { - name: "android_test_stubs_current_contributions", - api_surface: "test", - api_contributions: [ - "framework-virtualization.stubs.source.test.api.contribution", - "framework-location.stubs.source.test.api.contribution", - ], -} - -java_defaults { - name: "android_test_frameworks_core_stubs_current_contributions", - api_surface: "test", - api_contributions: [ - "test-api-stubs-docs-non-updatable.api.contribution", - ], -} - -java_defaults { - name: "android_module_lib_stubs_current_contributions", - api_surface: "module-lib", - api_contributions: [ - "api-stubs-docs-non-updatable.api.contribution", - "system-api-stubs-docs-non-updatable.api.contribution", - "module-lib-api-stubs-docs-non-updatable.api.contribution", - "art.module.public.api.stubs.source.api.contribution", - "art.module.public.api.stubs.source.system.api.contribution", - "art.module.public.api.stubs.source.module_lib.api.contribution", - "i18n.module.public.api.stubs.source.api.contribution", - "i18n.module.public.api.stubs.source.system.api.contribution", - "i18n.module.public.api.stubs.source.module_lib.api.contribution", - ], - previous_api: ":android.api.combined.module-lib.latest", -} - -// Java API library definitions per API surface -java_api_library { - name: "android_stubs_current.from-text", - api_surface: "public", - defaults: [ - // This module is dynamically created at frameworks/base/api/api.go - // instead of being written out, in order to minimize edits in the codebase - // when there is a change in the list of modules. - // that contributes to an api surface. - "android_stubs_current_contributions", - "stub-annotation-defaults", - ], - api_contributions: [ - "api-stubs-docs-non-updatable.api.contribution", - ], - visibility: ["//visibility:public"], - enable_validation: false, - stubs_type: "everything", -} - -java_api_library { - name: "android_system_stubs_current.from-text", - api_surface: "system", - defaults: [ - "android_stubs_current_contributions", - "android_system_stubs_current_contributions", - "stub-annotation-defaults", - ], - api_contributions: [ - "api-stubs-docs-non-updatable.api.contribution", - "system-api-stubs-docs-non-updatable.api.contribution", - ], - visibility: ["//visibility:public"], - enable_validation: false, - stubs_type: "everything", -} - -java_api_library { - name: "android_test_stubs_current.from-text", - api_surface: "test", - defaults: [ - "android_stubs_current_contributions", - "android_system_stubs_current_contributions", - "android_test_stubs_current_contributions", - "stub-annotation-defaults", - ], - api_contributions: [ - "api-stubs-docs-non-updatable.api.contribution", - "system-api-stubs-docs-non-updatable.api.contribution", - "test-api-stubs-docs-non-updatable.api.contribution", - ], - visibility: ["//visibility:public"], - enable_validation: false, - stubs_type: "everything", -} - -java_api_library { - name: "android_test_frameworks_core_stubs_current.from-text", - api_surface: "test", - defaults: [ - "android_stubs_current_contributions", - "android_system_stubs_current_contributions", - "android_test_frameworks_core_stubs_current_contributions", - ], - libs: [ - "stub-annotations", - ], - api_contributions: [ - "api-stubs-docs-non-updatable.api.contribution", - "system-api-stubs-docs-non-updatable.api.contribution", - ], - enable_validation: false, - stubs_type: "everything", -} - -java_api_library { - name: "android_module_lib_stubs_current_full.from-text", - api_surface: "module-lib", - defaults: [ - "android_stubs_current_contributions", - "android_system_stubs_current_contributions", - "android_module_lib_stubs_current_contributions_full", - ], - libs: [ - "stub-annotations", - ], - api_contributions: [ - "api-stubs-docs-non-updatable.api.contribution", - "system-api-stubs-docs-non-updatable.api.contribution", - "module-lib-api-stubs-docs-non-updatable.api.contribution", - ], - visibility: ["//visibility:public"], - enable_validation: false, - stubs_type: "everything", -} - -java_api_library { - name: "android_module_lib_stubs_current.from-text", - api_surface: "module-lib", - defaults: [ - "android_module_lib_stubs_current_contributions", - ], - libs: [ - "android_module_lib_stubs_current_full.from-text", - "stub-annotations", - ], - visibility: ["//visibility:public"], - enable_validation: false, - stubs_type: "everything", -} - -java_api_library { - name: "android_test_module_lib_stubs_current.from-text", - api_surface: "module-lib", - defaults: [ - "android_stubs_current_contributions", - "android_system_stubs_current_contributions", - "android_test_stubs_current_contributions", - "android_module_lib_stubs_current_contributions", - ], - libs: [ - "android_module_lib_stubs_current_full.from-text", - "stub-annotations", - ], - api_contributions: [ - "test-api-stubs-docs-non-updatable.api.contribution", - ], - - // This module is only used to build android-non-updatable.stubs.test_module_lib - // and other modules should not depend on this module. - visibility: [ - "//visibility:private", - ], - enable_validation: false, - stubs_type: "everything", -} - -java_api_library { - name: "android_system_server_stubs_current.from-text", - api_surface: "system-server", - api_contributions: [ - "services-non-updatable-stubs.api.contribution", - ], - libs: [ - "android_module_lib_stubs_current.from-text", - "stub-annotations", - ], - static_libs: [ - "android_module_lib_stubs_current.from-text", - ], - visibility: ["//visibility:public"], - enable_validation: false, - stubs_type: "everything", -} - //////////////////////////////////////////////////////////////////////// // api-versions.xml generation, for public and system. This API database // also contains the android.test.* APIs. diff --git a/api/api.go b/api/api.go index b6b1a7e44510..5b7f534443fb 100644 --- a/api/api.go +++ b/api/api.go @@ -15,9 +15,7 @@ package api import ( - "fmt" "sort" - "strings" "github.com/google/blueprint/proptools" @@ -464,79 +462,6 @@ func createMergedTxts(ctx android.LoadHookContext, bootclasspath, system_server_ } } -func createApiContributionDefaults(ctx android.LoadHookContext, modules []string) { - defaultsSdkKinds := []android.SdkKind{ - android.SdkPublic, android.SdkSystem, android.SdkModule, - } - for _, sdkKind := range defaultsSdkKinds { - props := defaultsProps{} - props.Name = proptools.StringPtr( - sdkKind.DefaultJavaLibraryName() + "_contributions") - if sdkKind == android.SdkModule { - props.Name = proptools.StringPtr( - sdkKind.DefaultJavaLibraryName() + "_contributions_full") - } - props.Api_surface = proptools.StringPtr(sdkKind.String()) - apiSuffix := "" - if sdkKind != android.SdkPublic { - apiSuffix = "." + strings.ReplaceAll(sdkKind.String(), "-", "_") - } - props.Api_contributions = transformArray( - modules, "", fmt.Sprintf(".stubs.source%s.api.contribution", apiSuffix)) - props.Defaults_visibility = []string{"//visibility:public"} - props.Previous_api = proptools.StringPtr(":android.api.combined." + sdkKind.String() + ".latest") - ctx.CreateModule(java.DefaultsFactory, &props) - } -} - -func createFullApiLibraries(ctx android.LoadHookContext) { - javaLibraryNames := []string{ - "android_stubs_current", - "android_system_stubs_current", - "android_test_stubs_current", - "android_test_frameworks_core_stubs_current", - "android_module_lib_stubs_current", - "android_system_server_stubs_current", - } - - for _, libraryName := range javaLibraryNames { - props := libraryProps{} - props.Name = proptools.StringPtr(libraryName) - staticLib := libraryName + ".from-source" - if ctx.Config().BuildFromTextStub() { - staticLib = libraryName + ".from-text" - } - props.Static_libs = []string{staticLib} - props.Defaults = []string{"android.jar_defaults"} - props.Visibility = []string{"//visibility:public"} - props.Is_stubs_module = proptools.BoolPtr(true) - - ctx.CreateModule(java.LibraryFactory, &props) - } -} - -func createFullExportableApiLibraries(ctx android.LoadHookContext) { - javaLibraryNames := []string{ - "android_stubs_current_exportable", - "android_system_stubs_current_exportable", - "android_test_stubs_current_exportable", - "android_module_lib_stubs_current_exportable", - "android_system_server_stubs_current_exportable", - } - - for _, libraryName := range javaLibraryNames { - props := libraryProps{} - props.Name = proptools.StringPtr(libraryName) - staticLib := libraryName + ".from-source" - props.Static_libs = []string{staticLib} - props.Defaults = []string{"android.jar_defaults"} - props.Visibility = []string{"//visibility:public"} - props.Is_stubs_module = proptools.BoolPtr(true) - - ctx.CreateModule(java.LibraryFactory, &props) - } -} - func (a *CombinedApis) createInternalModules(ctx android.LoadHookContext) { bootclasspath := a.bootclasspath(ctx) system_server_classpath := a.systemServerClasspath(ctx) @@ -562,12 +487,6 @@ func (a *CombinedApis) createInternalModules(ctx android.LoadHookContext) { createMergedAnnotationsFilegroups(ctx, bootclasspath, system_server_classpath) createPublicStubsSourceFilegroup(ctx, bootclasspath) - - createApiContributionDefaults(ctx, bootclasspath) - - createFullApiLibraries(ctx) - - createFullExportableApiLibraries(ctx) } func combinedApisModuleFactory() android.Module { diff --git a/api/api_test.go b/api/api_test.go index 47d167093b39..fb26f821eec1 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -52,6 +52,12 @@ func gatherRequiredDepsForTest() string { "core.current.stubs", "ext", "framework", + "android_stubs_current", + "android_system_stubs_current", + "android_test_stubs_current", + "android_test_frameworks_core_stubs_current", + "android_module_lib_stubs_current", + "android_system_server_stubs_current", "android_stubs_current.from-text", "android_system_stubs_current.from-text", "android_test_stubs_current.from-text", @@ -190,61 +196,60 @@ func TestCombinedApisDefaults(t *testing.T) { } }), ).RunTestWithBp(t, ` - java_sdk_library { - name: "framework-foo", - srcs: ["a.java"], - public: { - enabled: true, - }, - system: { - enabled: true, - }, - test: { - enabled: true, - }, - module_lib: { - enabled: true, - }, - api_packages: [ - "foo", - ], - sdk_version: "core_current", - annotations_enabled: true, - } + java_sdk_library { + name: "framework-foo", + srcs: ["a.java"], + public: { + enabled: true, + }, + system: { + enabled: true, + }, + test: { + enabled: true, + }, + module_lib: { + enabled: true, + }, + api_packages: [ + "foo", + ], + sdk_version: "core_current", + annotations_enabled: true, + } + java_sdk_library { + name: "framework-bar", + srcs: ["a.java"], + public: { + enabled: true, + }, + system: { + enabled: true, + }, + test: { + enabled: true, + }, + module_lib: { + enabled: true, + }, + api_packages: [ + "foo", + ], + sdk_version: "core_current", + annotations_enabled: true, + } - java_sdk_library { - name: "framework-bar", - srcs: ["a.java"], - public: { - enabled: true, - }, - system: { - enabled: true, - }, - test: { - enabled: true, - }, - module_lib: { - enabled: true, - }, - api_packages: [ - "foo", + combined_apis { + name: "foo", + bootclasspath: [ + "framework-bar", + ] + select(boolean_var_for_testing(), { + true: [ + "framework-foo", ], - sdk_version: "core_current", - annotations_enabled: true, - } - - combined_apis { - name: "foo", - bootclasspath: [ - "framework-bar", - ] + select(boolean_var_for_testing(), { - true: [ - "framework-foo", - ], - default: [], - }), - } + default: [], + }), + } `) subModuleDependsOnSelectAppendedModule := java.CheckModuleHasDependency(t, diff --git a/core/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/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/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/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl index 1767d6438999..98e11375f077 100644 --- a/core/java/android/hardware/input/IInputManager.aidl +++ b/core/java/android/hardware/input/IInputManager.aidl @@ -25,6 +25,7 @@ import android.hardware.input.IInputDeviceBatteryListener; import android.hardware.input.IInputDeviceBatteryState; import android.hardware.input.IKeyboardBacklightListener; import android.hardware.input.IKeyboardBacklightState; +import android.hardware.input.IKeyboardSystemShortcutListener; import android.hardware.input.IStickyModifierStateListener; import android.hardware.input.ITabletModeChangedListener; import android.hardware.input.KeyboardLayoutSelectionResult; @@ -239,4 +240,14 @@ interface IInputManager { void unregisterStickyModifierStateListener(IStickyModifierStateListener listener); KeyGlyphMap getKeyGlyphMap(int deviceId); + + @EnforcePermission("MONITOR_KEYBOARD_SYSTEM_SHORTCUTS") + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " + + "android.Manifest.permission.MONITOR_KEYBOARD_SYSTEM_SHORTCUTS)") + void registerKeyboardSystemShortcutListener(IKeyboardSystemShortcutListener listener); + + @EnforcePermission("MONITOR_KEYBOARD_SYSTEM_SHORTCUTS") + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " + + "android.Manifest.permission.MONITOR_KEYBOARD_SYSTEM_SHORTCUTS)") + void unregisterKeyboardSystemShortcutListener(IKeyboardSystemShortcutListener listener); } diff --git a/core/java/android/hardware/input/IKeyboardSystemShortcutListener.aidl b/core/java/android/hardware/input/IKeyboardSystemShortcutListener.aidl new file mode 100644 index 000000000000..8d44917845f4 --- /dev/null +++ b/core/java/android/hardware/input/IKeyboardSystemShortcutListener.aidl @@ -0,0 +1,27 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.hardware.input; + +/** @hide */ +oneway interface IKeyboardSystemShortcutListener { + + /** + * Called when the keyboard system shortcut is triggered. + */ + void onKeyboardSystemShortcutTriggered(int deviceId, in int[] keycodes, int modifierState, + int shortcut); +} diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java index d7952eb26f7e..6bc522b2b386 100644 --- a/core/java/android/hardware/input/InputManager.java +++ b/core/java/android/hardware/input/InputManager.java @@ -1378,6 +1378,36 @@ public final class InputManager { } /** + * Registers a keyboard system shortcut listener for {@link KeyboardSystemShortcut} being + * triggered. + * + * @param executor an executor on which the callback will be called + * @param listener the {@link KeyboardSystemShortcutListener} + * @throws IllegalArgumentException if {@code listener} has already been registered previously. + * @throws NullPointerException if {@code listener} or {@code executor} is null. + * @hide + * @see #unregisterKeyboardSystemShortcutListener(KeyboardSystemShortcutListener) + */ + @RequiresPermission(Manifest.permission.MONITOR_KEYBOARD_SYSTEM_SHORTCUTS) + public void registerKeyboardSystemShortcutListener(@NonNull Executor executor, + @NonNull KeyboardSystemShortcutListener listener) throws IllegalArgumentException { + mGlobal.registerKeyboardSystemShortcutListener(executor, listener); + } + + /** + * Unregisters a previously added keyboard system shortcut listener. + * + * @param listener the {@link KeyboardSystemShortcutListener} + * @hide + * @see #registerKeyboardSystemShortcutListener(Executor, KeyboardSystemShortcutListener) + */ + @RequiresPermission(Manifest.permission.MONITOR_KEYBOARD_SYSTEM_SHORTCUTS) + public void unregisterKeyboardSystemShortcutListener( + @NonNull KeyboardSystemShortcutListener listener) { + mGlobal.unregisterKeyboardSystemShortcutListener(listener); + } + + /** * A callback used to be notified about battery state changes for an input device. The * {@link #onBatteryStateChanged(int, long, BatteryState)} method will be called once after the * listener is successfully registered to provide the initial battery state of the device. @@ -1478,4 +1508,21 @@ public final class InputManager { */ void onStickyModifierStateChanged(@NonNull StickyModifierState state); } + + /** + * A callback used to be notified about keyboard system shortcuts being triggered. + * + * @see #registerKeyboardSystemShortcutListener(Executor, KeyboardSystemShortcutListener) + * @see #unregisterKeyboardSystemShortcutListener(KeyboardSystemShortcutListener) + * @hide + */ + public interface KeyboardSystemShortcutListener { + /** + * Called when a keyboard system shortcut is triggered. + * + * @param systemShortcut the shortcut info about the shortcut that was triggered. + */ + void onKeyboardSystemShortcutTriggered(int deviceId, + @NonNull KeyboardSystemShortcut systemShortcut); + } } diff --git a/core/java/android/hardware/input/InputManagerGlobal.java b/core/java/android/hardware/input/InputManagerGlobal.java index 7b471806cfc1..f7fa5577a047 100644 --- a/core/java/android/hardware/input/InputManagerGlobal.java +++ b/core/java/android/hardware/input/InputManagerGlobal.java @@ -26,6 +26,7 @@ import android.hardware.SensorManager; import android.hardware.input.InputManager.InputDeviceBatteryListener; import android.hardware.input.InputManager.InputDeviceListener; import android.hardware.input.InputManager.KeyboardBacklightListener; +import android.hardware.input.InputManager.KeyboardSystemShortcutListener; import android.hardware.input.InputManager.OnTabletModeChangedListener; import android.hardware.input.InputManager.StickyModifierStateListener; import android.hardware.lights.Light; @@ -110,6 +111,14 @@ public final class InputManagerGlobal { @Nullable private IStickyModifierStateListener mStickyModifierStateListener; + private final Object mKeyboardSystemShortcutListenerLock = new Object(); + @GuardedBy("mKeyboardSystemShortcutListenerLock") + @Nullable + private ArrayList<KeyboardSystemShortcutListenerDelegate> mKeyboardSystemShortcutListeners; + @GuardedBy("mKeyboardSystemShortcutListenerLock") + @Nullable + private IKeyboardSystemShortcutListener mKeyboardSystemShortcutListener; + // InputDeviceSensorManager gets notified synchronously from the binder thread when input // devices change, so it must be synchronized with the input device listeners. @GuardedBy("mInputDeviceListeners") @@ -1055,6 +1064,98 @@ public final class InputManagerGlobal { } } + private static final class KeyboardSystemShortcutListenerDelegate { + final KeyboardSystemShortcutListener mListener; + final Executor mExecutor; + + KeyboardSystemShortcutListenerDelegate(KeyboardSystemShortcutListener listener, + Executor executor) { + mListener = listener; + mExecutor = executor; + } + + void onKeyboardSystemShortcutTriggered(int deviceId, + KeyboardSystemShortcut systemShortcut) { + mExecutor.execute(() -> + mListener.onKeyboardSystemShortcutTriggered(deviceId, systemShortcut)); + } + } + + private class LocalKeyboardSystemShortcutListener extends IKeyboardSystemShortcutListener.Stub { + + @Override + public void onKeyboardSystemShortcutTriggered(int deviceId, int[] keycodes, + int modifierState, int shortcut) { + synchronized (mKeyboardSystemShortcutListenerLock) { + if (mKeyboardSystemShortcutListeners == null) return; + final int numListeners = mKeyboardSystemShortcutListeners.size(); + for (int i = 0; i < numListeners; i++) { + mKeyboardSystemShortcutListeners.get(i) + .onKeyboardSystemShortcutTriggered(deviceId, + new KeyboardSystemShortcut(keycodes, modifierState, shortcut)); + } + } + } + } + + /** + * @see InputManager#registerKeyboardSystemShortcutListener(Executor, + * KeyboardSystemShortcutListener) + */ + @RequiresPermission(Manifest.permission.MONITOR_KEYBOARD_SYSTEM_SHORTCUTS) + void registerKeyboardSystemShortcutListener(@NonNull Executor executor, + @NonNull KeyboardSystemShortcutListener listener) throws IllegalArgumentException { + Objects.requireNonNull(executor, "executor should not be null"); + Objects.requireNonNull(listener, "listener should not be null"); + + synchronized (mKeyboardSystemShortcutListenerLock) { + if (mKeyboardSystemShortcutListener == null) { + mKeyboardSystemShortcutListeners = new ArrayList<>(); + mKeyboardSystemShortcutListener = new LocalKeyboardSystemShortcutListener(); + + try { + mIm.registerKeyboardSystemShortcutListener(mKeyboardSystemShortcutListener); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + final int numListeners = mKeyboardSystemShortcutListeners.size(); + for (int i = 0; i < numListeners; i++) { + if (mKeyboardSystemShortcutListeners.get(i).mListener == listener) { + throw new IllegalArgumentException("Listener has already been registered!"); + } + } + KeyboardSystemShortcutListenerDelegate delegate = + new KeyboardSystemShortcutListenerDelegate(listener, executor); + mKeyboardSystemShortcutListeners.add(delegate); + } + } + + /** + * @see InputManager#unregisterKeyboardSystemShortcutListener(KeyboardSystemShortcutListener) + */ + @RequiresPermission(Manifest.permission.MONITOR_KEYBOARD_SYSTEM_SHORTCUTS) + void unregisterKeyboardSystemShortcutListener( + @NonNull KeyboardSystemShortcutListener listener) { + Objects.requireNonNull(listener, "listener should not be null"); + + synchronized (mKeyboardSystemShortcutListenerLock) { + if (mKeyboardSystemShortcutListeners == null) { + return; + } + mKeyboardSystemShortcutListeners.removeIf((delegate) -> delegate.mListener == listener); + if (mKeyboardSystemShortcutListeners.isEmpty()) { + try { + mIm.unregisterKeyboardSystemShortcutListener(mKeyboardSystemShortcutListener); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + mKeyboardSystemShortcutListeners = null; + mKeyboardSystemShortcutListener = null; + } + } + } + /** * TODO(b/330517633): Cleanup the unsupported API */ diff --git a/core/java/android/hardware/input/KeyboardSystemShortcut.java b/core/java/android/hardware/input/KeyboardSystemShortcut.java new file mode 100644 index 000000000000..89cf877c3aa8 --- /dev/null +++ b/core/java/android/hardware/input/KeyboardSystemShortcut.java @@ -0,0 +1,522 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.hardware.input; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; + +import com.android.internal.util.DataClass; +import com.android.internal.util.FrameworkStatsLog; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Provides information about the keyboard shortcut being triggered by an external keyboard. + * + * @hide + */ +@DataClass(genToString = true, genEqualsHashCode = true) +public class KeyboardSystemShortcut { + + private static final String TAG = "KeyboardSystemShortcut"; + + @NonNull + private final int[] mKeycodes; + private final int mModifierState; + @SystemShortcut + private final int mSystemShortcut; + + + public static final int SYSTEM_SHORTCUT_UNSPECIFIED = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__UNSPECIFIED; + public static final int SYSTEM_SHORTCUT_HOME = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__HOME; + public static final int SYSTEM_SHORTCUT_RECENT_APPS = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__RECENT_APPS; + public static final int SYSTEM_SHORTCUT_BACK = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__BACK; + public static final int SYSTEM_SHORTCUT_APP_SWITCH = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__APP_SWITCH; + public static final int SYSTEM_SHORTCUT_LAUNCH_ASSISTANT = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_ASSISTANT; + public static final int SYSTEM_SHORTCUT_LAUNCH_VOICE_ASSISTANT = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_VOICE_ASSISTANT; + public static final int SYSTEM_SHORTCUT_LAUNCH_SYSTEM_SETTINGS = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_SYSTEM_SETTINGS; + public static final int SYSTEM_SHORTCUT_TOGGLE_NOTIFICATION_PANEL = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__TOGGLE_NOTIFICATION_PANEL; + public static final int SYSTEM_SHORTCUT_TOGGLE_TASKBAR = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__TOGGLE_TASKBAR; + public static final int SYSTEM_SHORTCUT_TAKE_SCREENSHOT = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__TAKE_SCREENSHOT; + public static final int SYSTEM_SHORTCUT_OPEN_SHORTCUT_HELPER = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__OPEN_SHORTCUT_HELPER; + public static final int SYSTEM_SHORTCUT_BRIGHTNESS_UP = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__BRIGHTNESS_UP; + public static final int SYSTEM_SHORTCUT_BRIGHTNESS_DOWN = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__BRIGHTNESS_DOWN; + public static final int SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_UP = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__KEYBOARD_BACKLIGHT_UP; + public static final int SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_DOWN = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__KEYBOARD_BACKLIGHT_DOWN; + public static final int SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_TOGGLE = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__KEYBOARD_BACKLIGHT_TOGGLE; + public static final int SYSTEM_SHORTCUT_VOLUME_UP = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__VOLUME_UP; + public static final int SYSTEM_SHORTCUT_VOLUME_DOWN = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__VOLUME_DOWN; + public static final int SYSTEM_SHORTCUT_VOLUME_MUTE = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__VOLUME_MUTE; + public static final int SYSTEM_SHORTCUT_ALL_APPS = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__ALL_APPS; + public static final int SYSTEM_SHORTCUT_LAUNCH_SEARCH = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_SEARCH; + public static final int SYSTEM_SHORTCUT_LANGUAGE_SWITCH = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LANGUAGE_SWITCH; + public static final int SYSTEM_SHORTCUT_ACCESSIBILITY_ALL_APPS = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__ACCESSIBILITY_ALL_APPS; + public static final int SYSTEM_SHORTCUT_TOGGLE_CAPS_LOCK = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__TOGGLE_CAPS_LOCK; + public static final int SYSTEM_SHORTCUT_SYSTEM_MUTE = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__SYSTEM_MUTE; + public static final int SYSTEM_SHORTCUT_SPLIT_SCREEN_NAVIGATION = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__SPLIT_SCREEN_NAVIGATION; + public static final int SYSTEM_SHORTCUT_CHANGE_SPLITSCREEN_FOCUS = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__CHANGE_SPLITSCREEN_FOCUS; + public static final int SYSTEM_SHORTCUT_TRIGGER_BUG_REPORT = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__TRIGGER_BUG_REPORT; + public static final int SYSTEM_SHORTCUT_LOCK_SCREEN = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LOCK_SCREEN; + public static final int SYSTEM_SHORTCUT_OPEN_NOTES = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__OPEN_NOTES; + public static final int SYSTEM_SHORTCUT_TOGGLE_POWER = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__TOGGLE_POWER; + public static final int SYSTEM_SHORTCUT_SYSTEM_NAVIGATION = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__SYSTEM_NAVIGATION; + public static final int SYSTEM_SHORTCUT_SLEEP = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__SLEEP; + public static final int SYSTEM_SHORTCUT_WAKEUP = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__WAKEUP; + public static final int SYSTEM_SHORTCUT_MEDIA_KEY = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__MEDIA_KEY; + public static final int SYSTEM_SHORTCUT_LAUNCH_DEFAULT_BROWSER = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_BROWSER; + public static final int SYSTEM_SHORTCUT_LAUNCH_DEFAULT_EMAIL = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_EMAIL; + public static final int SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CONTACTS = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_CONTACTS; + public static final int SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CALENDAR = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_CALENDAR; + public static final int SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CALCULATOR = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_CALCULATOR; + public static final int SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MUSIC = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_MUSIC; + public static final int SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MAPS = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_MAPS; + public static final int SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MESSAGING = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_MESSAGING; + public static final int SYSTEM_SHORTCUT_LAUNCH_DEFAULT_GALLERY = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_GALLERY; + public static final int SYSTEM_SHORTCUT_LAUNCH_DEFAULT_FILES = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_FILES; + public static final int SYSTEM_SHORTCUT_LAUNCH_DEFAULT_WEATHER = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_WEATHER; + public static final int SYSTEM_SHORTCUT_LAUNCH_DEFAULT_FITNESS = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_FITNESS; + public static final int SYSTEM_SHORTCUT_LAUNCH_APPLICATION_BY_PACKAGE_NAME = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_APPLICATION_BY_PACKAGE_NAME; + public static final int SYSTEM_SHORTCUT_DESKTOP_MODE = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__DESKTOP_MODE; + public static final int SYSTEM_SHORTCUT_MULTI_WINDOW_NAVIGATION = + FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__MULTI_WINDOW_NAVIGATION; + + + + // Code below generated by codegen v1.0.23. + // + // DO NOT MODIFY! + // CHECKSTYLE:OFF Generated code + // + // To regenerate run: + // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/hardware/input/KeyboardSystemShortcut.java + // + // To exclude the generated code from IntelliJ auto-formatting enable (one-time): + // Settings > Editor > Code Style > Formatter Control + //@formatter:off + + + @IntDef(prefix = "SYSTEM_SHORTCUT_", value = { + SYSTEM_SHORTCUT_UNSPECIFIED, + SYSTEM_SHORTCUT_HOME, + SYSTEM_SHORTCUT_RECENT_APPS, + SYSTEM_SHORTCUT_BACK, + SYSTEM_SHORTCUT_APP_SWITCH, + SYSTEM_SHORTCUT_LAUNCH_ASSISTANT, + SYSTEM_SHORTCUT_LAUNCH_VOICE_ASSISTANT, + SYSTEM_SHORTCUT_LAUNCH_SYSTEM_SETTINGS, + SYSTEM_SHORTCUT_TOGGLE_NOTIFICATION_PANEL, + SYSTEM_SHORTCUT_TOGGLE_TASKBAR, + SYSTEM_SHORTCUT_TAKE_SCREENSHOT, + SYSTEM_SHORTCUT_OPEN_SHORTCUT_HELPER, + SYSTEM_SHORTCUT_BRIGHTNESS_UP, + SYSTEM_SHORTCUT_BRIGHTNESS_DOWN, + SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_UP, + SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_DOWN, + SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_TOGGLE, + SYSTEM_SHORTCUT_VOLUME_UP, + SYSTEM_SHORTCUT_VOLUME_DOWN, + SYSTEM_SHORTCUT_VOLUME_MUTE, + SYSTEM_SHORTCUT_ALL_APPS, + SYSTEM_SHORTCUT_LAUNCH_SEARCH, + SYSTEM_SHORTCUT_LANGUAGE_SWITCH, + SYSTEM_SHORTCUT_ACCESSIBILITY_ALL_APPS, + SYSTEM_SHORTCUT_TOGGLE_CAPS_LOCK, + SYSTEM_SHORTCUT_SYSTEM_MUTE, + SYSTEM_SHORTCUT_SPLIT_SCREEN_NAVIGATION, + SYSTEM_SHORTCUT_CHANGE_SPLITSCREEN_FOCUS, + SYSTEM_SHORTCUT_TRIGGER_BUG_REPORT, + SYSTEM_SHORTCUT_LOCK_SCREEN, + SYSTEM_SHORTCUT_OPEN_NOTES, + SYSTEM_SHORTCUT_TOGGLE_POWER, + SYSTEM_SHORTCUT_SYSTEM_NAVIGATION, + SYSTEM_SHORTCUT_SLEEP, + SYSTEM_SHORTCUT_WAKEUP, + SYSTEM_SHORTCUT_MEDIA_KEY, + SYSTEM_SHORTCUT_LAUNCH_DEFAULT_BROWSER, + SYSTEM_SHORTCUT_LAUNCH_DEFAULT_EMAIL, + SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CONTACTS, + SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CALENDAR, + SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CALCULATOR, + SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MUSIC, + SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MAPS, + SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MESSAGING, + SYSTEM_SHORTCUT_LAUNCH_DEFAULT_GALLERY, + SYSTEM_SHORTCUT_LAUNCH_DEFAULT_FILES, + SYSTEM_SHORTCUT_LAUNCH_DEFAULT_WEATHER, + SYSTEM_SHORTCUT_LAUNCH_DEFAULT_FITNESS, + SYSTEM_SHORTCUT_LAUNCH_APPLICATION_BY_PACKAGE_NAME, + SYSTEM_SHORTCUT_DESKTOP_MODE, + SYSTEM_SHORTCUT_MULTI_WINDOW_NAVIGATION + }) + @Retention(RetentionPolicy.SOURCE) + @DataClass.Generated.Member + public @interface SystemShortcut {} + + @DataClass.Generated.Member + public static String systemShortcutToString(@SystemShortcut int value) { + switch (value) { + case SYSTEM_SHORTCUT_UNSPECIFIED: + return "SYSTEM_SHORTCUT_UNSPECIFIED"; + case SYSTEM_SHORTCUT_HOME: + return "SYSTEM_SHORTCUT_HOME"; + case SYSTEM_SHORTCUT_RECENT_APPS: + return "SYSTEM_SHORTCUT_RECENT_APPS"; + case SYSTEM_SHORTCUT_BACK: + return "SYSTEM_SHORTCUT_BACK"; + case SYSTEM_SHORTCUT_APP_SWITCH: + return "SYSTEM_SHORTCUT_APP_SWITCH"; + case SYSTEM_SHORTCUT_LAUNCH_ASSISTANT: + return "SYSTEM_SHORTCUT_LAUNCH_ASSISTANT"; + case SYSTEM_SHORTCUT_LAUNCH_VOICE_ASSISTANT: + return "SYSTEM_SHORTCUT_LAUNCH_VOICE_ASSISTANT"; + case SYSTEM_SHORTCUT_LAUNCH_SYSTEM_SETTINGS: + return "SYSTEM_SHORTCUT_LAUNCH_SYSTEM_SETTINGS"; + case SYSTEM_SHORTCUT_TOGGLE_NOTIFICATION_PANEL: + return "SYSTEM_SHORTCUT_TOGGLE_NOTIFICATION_PANEL"; + case SYSTEM_SHORTCUT_TOGGLE_TASKBAR: + return "SYSTEM_SHORTCUT_TOGGLE_TASKBAR"; + case SYSTEM_SHORTCUT_TAKE_SCREENSHOT: + return "SYSTEM_SHORTCUT_TAKE_SCREENSHOT"; + case SYSTEM_SHORTCUT_OPEN_SHORTCUT_HELPER: + return "SYSTEM_SHORTCUT_OPEN_SHORTCUT_HELPER"; + case SYSTEM_SHORTCUT_BRIGHTNESS_UP: + return "SYSTEM_SHORTCUT_BRIGHTNESS_UP"; + case SYSTEM_SHORTCUT_BRIGHTNESS_DOWN: + return "SYSTEM_SHORTCUT_BRIGHTNESS_DOWN"; + case SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_UP: + return "SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_UP"; + case SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_DOWN: + return "SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_DOWN"; + case SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_TOGGLE: + return "SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_TOGGLE"; + case SYSTEM_SHORTCUT_VOLUME_UP: + return "SYSTEM_SHORTCUT_VOLUME_UP"; + case SYSTEM_SHORTCUT_VOLUME_DOWN: + return "SYSTEM_SHORTCUT_VOLUME_DOWN"; + case SYSTEM_SHORTCUT_VOLUME_MUTE: + return "SYSTEM_SHORTCUT_VOLUME_MUTE"; + case SYSTEM_SHORTCUT_ALL_APPS: + return "SYSTEM_SHORTCUT_ALL_APPS"; + case SYSTEM_SHORTCUT_LAUNCH_SEARCH: + return "SYSTEM_SHORTCUT_LAUNCH_SEARCH"; + case SYSTEM_SHORTCUT_LANGUAGE_SWITCH: + return "SYSTEM_SHORTCUT_LANGUAGE_SWITCH"; + case SYSTEM_SHORTCUT_ACCESSIBILITY_ALL_APPS: + return "SYSTEM_SHORTCUT_ACCESSIBILITY_ALL_APPS"; + case SYSTEM_SHORTCUT_TOGGLE_CAPS_LOCK: + return "SYSTEM_SHORTCUT_TOGGLE_CAPS_LOCK"; + case SYSTEM_SHORTCUT_SYSTEM_MUTE: + return "SYSTEM_SHORTCUT_SYSTEM_MUTE"; + case SYSTEM_SHORTCUT_SPLIT_SCREEN_NAVIGATION: + return "SYSTEM_SHORTCUT_SPLIT_SCREEN_NAVIGATION"; + case SYSTEM_SHORTCUT_CHANGE_SPLITSCREEN_FOCUS: + return "SYSTEM_SHORTCUT_CHANGE_SPLITSCREEN_FOCUS"; + case SYSTEM_SHORTCUT_TRIGGER_BUG_REPORT: + return "SYSTEM_SHORTCUT_TRIGGER_BUG_REPORT"; + case SYSTEM_SHORTCUT_LOCK_SCREEN: + return "SYSTEM_SHORTCUT_LOCK_SCREEN"; + case SYSTEM_SHORTCUT_OPEN_NOTES: + return "SYSTEM_SHORTCUT_OPEN_NOTES"; + case SYSTEM_SHORTCUT_TOGGLE_POWER: + return "SYSTEM_SHORTCUT_TOGGLE_POWER"; + case SYSTEM_SHORTCUT_SYSTEM_NAVIGATION: + return "SYSTEM_SHORTCUT_SYSTEM_NAVIGATION"; + case SYSTEM_SHORTCUT_SLEEP: + return "SYSTEM_SHORTCUT_SLEEP"; + case SYSTEM_SHORTCUT_WAKEUP: + return "SYSTEM_SHORTCUT_WAKEUP"; + case SYSTEM_SHORTCUT_MEDIA_KEY: + return "SYSTEM_SHORTCUT_MEDIA_KEY"; + case SYSTEM_SHORTCUT_LAUNCH_DEFAULT_BROWSER: + return "SYSTEM_SHORTCUT_LAUNCH_DEFAULT_BROWSER"; + case SYSTEM_SHORTCUT_LAUNCH_DEFAULT_EMAIL: + return "SYSTEM_SHORTCUT_LAUNCH_DEFAULT_EMAIL"; + case SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CONTACTS: + return "SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CONTACTS"; + case SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CALENDAR: + return "SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CALENDAR"; + case SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CALCULATOR: + return "SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CALCULATOR"; + case SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MUSIC: + return "SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MUSIC"; + case SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MAPS: + return "SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MAPS"; + case SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MESSAGING: + return "SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MESSAGING"; + case SYSTEM_SHORTCUT_LAUNCH_DEFAULT_GALLERY: + return "SYSTEM_SHORTCUT_LAUNCH_DEFAULT_GALLERY"; + case SYSTEM_SHORTCUT_LAUNCH_DEFAULT_FILES: + return "SYSTEM_SHORTCUT_LAUNCH_DEFAULT_FILES"; + case SYSTEM_SHORTCUT_LAUNCH_DEFAULT_WEATHER: + return "SYSTEM_SHORTCUT_LAUNCH_DEFAULT_WEATHER"; + case SYSTEM_SHORTCUT_LAUNCH_DEFAULT_FITNESS: + return "SYSTEM_SHORTCUT_LAUNCH_DEFAULT_FITNESS"; + case SYSTEM_SHORTCUT_LAUNCH_APPLICATION_BY_PACKAGE_NAME: + return "SYSTEM_SHORTCUT_LAUNCH_APPLICATION_BY_PACKAGE_NAME"; + case SYSTEM_SHORTCUT_DESKTOP_MODE: + return "SYSTEM_SHORTCUT_DESKTOP_MODE"; + case SYSTEM_SHORTCUT_MULTI_WINDOW_NAVIGATION: + return "SYSTEM_SHORTCUT_MULTI_WINDOW_NAVIGATION"; + default: return Integer.toHexString(value); + } + } + + @DataClass.Generated.Member + public KeyboardSystemShortcut( + @NonNull int[] keycodes, + int modifierState, + @SystemShortcut int systemShortcut) { + this.mKeycodes = keycodes; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mKeycodes); + this.mModifierState = modifierState; + this.mSystemShortcut = systemShortcut; + + if (!(mSystemShortcut == SYSTEM_SHORTCUT_UNSPECIFIED) + && !(mSystemShortcut == SYSTEM_SHORTCUT_HOME) + && !(mSystemShortcut == SYSTEM_SHORTCUT_RECENT_APPS) + && !(mSystemShortcut == SYSTEM_SHORTCUT_BACK) + && !(mSystemShortcut == SYSTEM_SHORTCUT_APP_SWITCH) + && !(mSystemShortcut == SYSTEM_SHORTCUT_LAUNCH_ASSISTANT) + && !(mSystemShortcut == SYSTEM_SHORTCUT_LAUNCH_VOICE_ASSISTANT) + && !(mSystemShortcut == SYSTEM_SHORTCUT_LAUNCH_SYSTEM_SETTINGS) + && !(mSystemShortcut == SYSTEM_SHORTCUT_TOGGLE_NOTIFICATION_PANEL) + && !(mSystemShortcut == SYSTEM_SHORTCUT_TOGGLE_TASKBAR) + && !(mSystemShortcut == SYSTEM_SHORTCUT_TAKE_SCREENSHOT) + && !(mSystemShortcut == SYSTEM_SHORTCUT_OPEN_SHORTCUT_HELPER) + && !(mSystemShortcut == SYSTEM_SHORTCUT_BRIGHTNESS_UP) + && !(mSystemShortcut == SYSTEM_SHORTCUT_BRIGHTNESS_DOWN) + && !(mSystemShortcut == SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_UP) + && !(mSystemShortcut == SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_DOWN) + && !(mSystemShortcut == SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_TOGGLE) + && !(mSystemShortcut == SYSTEM_SHORTCUT_VOLUME_UP) + && !(mSystemShortcut == SYSTEM_SHORTCUT_VOLUME_DOWN) + && !(mSystemShortcut == SYSTEM_SHORTCUT_VOLUME_MUTE) + && !(mSystemShortcut == SYSTEM_SHORTCUT_ALL_APPS) + && !(mSystemShortcut == SYSTEM_SHORTCUT_LAUNCH_SEARCH) + && !(mSystemShortcut == SYSTEM_SHORTCUT_LANGUAGE_SWITCH) + && !(mSystemShortcut == SYSTEM_SHORTCUT_ACCESSIBILITY_ALL_APPS) + && !(mSystemShortcut == SYSTEM_SHORTCUT_TOGGLE_CAPS_LOCK) + && !(mSystemShortcut == SYSTEM_SHORTCUT_SYSTEM_MUTE) + && !(mSystemShortcut == SYSTEM_SHORTCUT_SPLIT_SCREEN_NAVIGATION) + && !(mSystemShortcut == SYSTEM_SHORTCUT_CHANGE_SPLITSCREEN_FOCUS) + && !(mSystemShortcut == SYSTEM_SHORTCUT_TRIGGER_BUG_REPORT) + && !(mSystemShortcut == SYSTEM_SHORTCUT_LOCK_SCREEN) + && !(mSystemShortcut == SYSTEM_SHORTCUT_OPEN_NOTES) + && !(mSystemShortcut == SYSTEM_SHORTCUT_TOGGLE_POWER) + && !(mSystemShortcut == SYSTEM_SHORTCUT_SYSTEM_NAVIGATION) + && !(mSystemShortcut == SYSTEM_SHORTCUT_SLEEP) + && !(mSystemShortcut == SYSTEM_SHORTCUT_WAKEUP) + && !(mSystemShortcut == SYSTEM_SHORTCUT_MEDIA_KEY) + && !(mSystemShortcut == SYSTEM_SHORTCUT_LAUNCH_DEFAULT_BROWSER) + && !(mSystemShortcut == SYSTEM_SHORTCUT_LAUNCH_DEFAULT_EMAIL) + && !(mSystemShortcut == SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CONTACTS) + && !(mSystemShortcut == SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CALENDAR) + && !(mSystemShortcut == SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CALCULATOR) + && !(mSystemShortcut == SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MUSIC) + && !(mSystemShortcut == SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MAPS) + && !(mSystemShortcut == SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MESSAGING) + && !(mSystemShortcut == SYSTEM_SHORTCUT_LAUNCH_DEFAULT_GALLERY) + && !(mSystemShortcut == SYSTEM_SHORTCUT_LAUNCH_DEFAULT_FILES) + && !(mSystemShortcut == SYSTEM_SHORTCUT_LAUNCH_DEFAULT_WEATHER) + && !(mSystemShortcut == SYSTEM_SHORTCUT_LAUNCH_DEFAULT_FITNESS) + && !(mSystemShortcut == SYSTEM_SHORTCUT_LAUNCH_APPLICATION_BY_PACKAGE_NAME) + && !(mSystemShortcut == SYSTEM_SHORTCUT_DESKTOP_MODE) + && !(mSystemShortcut == SYSTEM_SHORTCUT_MULTI_WINDOW_NAVIGATION)) { + throw new java.lang.IllegalArgumentException( + "systemShortcut was " + mSystemShortcut + " but must be one of: " + + "SYSTEM_SHORTCUT_UNSPECIFIED(" + SYSTEM_SHORTCUT_UNSPECIFIED + "), " + + "SYSTEM_SHORTCUT_HOME(" + SYSTEM_SHORTCUT_HOME + "), " + + "SYSTEM_SHORTCUT_RECENT_APPS(" + SYSTEM_SHORTCUT_RECENT_APPS + "), " + + "SYSTEM_SHORTCUT_BACK(" + SYSTEM_SHORTCUT_BACK + "), " + + "SYSTEM_SHORTCUT_APP_SWITCH(" + SYSTEM_SHORTCUT_APP_SWITCH + "), " + + "SYSTEM_SHORTCUT_LAUNCH_ASSISTANT(" + SYSTEM_SHORTCUT_LAUNCH_ASSISTANT + "), " + + "SYSTEM_SHORTCUT_LAUNCH_VOICE_ASSISTANT(" + SYSTEM_SHORTCUT_LAUNCH_VOICE_ASSISTANT + "), " + + "SYSTEM_SHORTCUT_LAUNCH_SYSTEM_SETTINGS(" + SYSTEM_SHORTCUT_LAUNCH_SYSTEM_SETTINGS + "), " + + "SYSTEM_SHORTCUT_TOGGLE_NOTIFICATION_PANEL(" + SYSTEM_SHORTCUT_TOGGLE_NOTIFICATION_PANEL + "), " + + "SYSTEM_SHORTCUT_TOGGLE_TASKBAR(" + SYSTEM_SHORTCUT_TOGGLE_TASKBAR + "), " + + "SYSTEM_SHORTCUT_TAKE_SCREENSHOT(" + SYSTEM_SHORTCUT_TAKE_SCREENSHOT + "), " + + "SYSTEM_SHORTCUT_OPEN_SHORTCUT_HELPER(" + SYSTEM_SHORTCUT_OPEN_SHORTCUT_HELPER + "), " + + "SYSTEM_SHORTCUT_BRIGHTNESS_UP(" + SYSTEM_SHORTCUT_BRIGHTNESS_UP + "), " + + "SYSTEM_SHORTCUT_BRIGHTNESS_DOWN(" + SYSTEM_SHORTCUT_BRIGHTNESS_DOWN + "), " + + "SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_UP(" + SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_UP + "), " + + "SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_DOWN(" + SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_DOWN + "), " + + "SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_TOGGLE(" + SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_TOGGLE + "), " + + "SYSTEM_SHORTCUT_VOLUME_UP(" + SYSTEM_SHORTCUT_VOLUME_UP + "), " + + "SYSTEM_SHORTCUT_VOLUME_DOWN(" + SYSTEM_SHORTCUT_VOLUME_DOWN + "), " + + "SYSTEM_SHORTCUT_VOLUME_MUTE(" + SYSTEM_SHORTCUT_VOLUME_MUTE + "), " + + "SYSTEM_SHORTCUT_ALL_APPS(" + SYSTEM_SHORTCUT_ALL_APPS + "), " + + "SYSTEM_SHORTCUT_LAUNCH_SEARCH(" + SYSTEM_SHORTCUT_LAUNCH_SEARCH + "), " + + "SYSTEM_SHORTCUT_LANGUAGE_SWITCH(" + SYSTEM_SHORTCUT_LANGUAGE_SWITCH + "), " + + "SYSTEM_SHORTCUT_ACCESSIBILITY_ALL_APPS(" + SYSTEM_SHORTCUT_ACCESSIBILITY_ALL_APPS + "), " + + "SYSTEM_SHORTCUT_TOGGLE_CAPS_LOCK(" + SYSTEM_SHORTCUT_TOGGLE_CAPS_LOCK + "), " + + "SYSTEM_SHORTCUT_SYSTEM_MUTE(" + SYSTEM_SHORTCUT_SYSTEM_MUTE + "), " + + "SYSTEM_SHORTCUT_SPLIT_SCREEN_NAVIGATION(" + SYSTEM_SHORTCUT_SPLIT_SCREEN_NAVIGATION + "), " + + "SYSTEM_SHORTCUT_CHANGE_SPLITSCREEN_FOCUS(" + SYSTEM_SHORTCUT_CHANGE_SPLITSCREEN_FOCUS + "), " + + "SYSTEM_SHORTCUT_TRIGGER_BUG_REPORT(" + SYSTEM_SHORTCUT_TRIGGER_BUG_REPORT + "), " + + "SYSTEM_SHORTCUT_LOCK_SCREEN(" + SYSTEM_SHORTCUT_LOCK_SCREEN + "), " + + "SYSTEM_SHORTCUT_OPEN_NOTES(" + SYSTEM_SHORTCUT_OPEN_NOTES + "), " + + "SYSTEM_SHORTCUT_TOGGLE_POWER(" + SYSTEM_SHORTCUT_TOGGLE_POWER + "), " + + "SYSTEM_SHORTCUT_SYSTEM_NAVIGATION(" + SYSTEM_SHORTCUT_SYSTEM_NAVIGATION + "), " + + "SYSTEM_SHORTCUT_SLEEP(" + SYSTEM_SHORTCUT_SLEEP + "), " + + "SYSTEM_SHORTCUT_WAKEUP(" + SYSTEM_SHORTCUT_WAKEUP + "), " + + "SYSTEM_SHORTCUT_MEDIA_KEY(" + SYSTEM_SHORTCUT_MEDIA_KEY + "), " + + "SYSTEM_SHORTCUT_LAUNCH_DEFAULT_BROWSER(" + SYSTEM_SHORTCUT_LAUNCH_DEFAULT_BROWSER + "), " + + "SYSTEM_SHORTCUT_LAUNCH_DEFAULT_EMAIL(" + SYSTEM_SHORTCUT_LAUNCH_DEFAULT_EMAIL + "), " + + "SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CONTACTS(" + SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CONTACTS + "), " + + "SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CALENDAR(" + SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CALENDAR + "), " + + "SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CALCULATOR(" + SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CALCULATOR + "), " + + "SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MUSIC(" + SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MUSIC + "), " + + "SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MAPS(" + SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MAPS + "), " + + "SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MESSAGING(" + SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MESSAGING + "), " + + "SYSTEM_SHORTCUT_LAUNCH_DEFAULT_GALLERY(" + SYSTEM_SHORTCUT_LAUNCH_DEFAULT_GALLERY + "), " + + "SYSTEM_SHORTCUT_LAUNCH_DEFAULT_FILES(" + SYSTEM_SHORTCUT_LAUNCH_DEFAULT_FILES + "), " + + "SYSTEM_SHORTCUT_LAUNCH_DEFAULT_WEATHER(" + SYSTEM_SHORTCUT_LAUNCH_DEFAULT_WEATHER + "), " + + "SYSTEM_SHORTCUT_LAUNCH_DEFAULT_FITNESS(" + SYSTEM_SHORTCUT_LAUNCH_DEFAULT_FITNESS + "), " + + "SYSTEM_SHORTCUT_LAUNCH_APPLICATION_BY_PACKAGE_NAME(" + SYSTEM_SHORTCUT_LAUNCH_APPLICATION_BY_PACKAGE_NAME + "), " + + "SYSTEM_SHORTCUT_DESKTOP_MODE(" + SYSTEM_SHORTCUT_DESKTOP_MODE + "), " + + "SYSTEM_SHORTCUT_MULTI_WINDOW_NAVIGATION(" + SYSTEM_SHORTCUT_MULTI_WINDOW_NAVIGATION + ")"); + } + + + // onConstructed(); // You can define this method to get a callback + } + + @DataClass.Generated.Member + public @NonNull int[] getKeycodes() { + return mKeycodes; + } + + @DataClass.Generated.Member + public int getModifierState() { + return mModifierState; + } + + @DataClass.Generated.Member + public @SystemShortcut int getSystemShortcut() { + return mSystemShortcut; + } + + @Override + @DataClass.Generated.Member + public String toString() { + // You can override field toString logic by defining methods like: + // String fieldNameToString() { ... } + + return "KeyboardSystemShortcut { " + + "keycodes = " + java.util.Arrays.toString(mKeycodes) + ", " + + "modifierState = " + mModifierState + ", " + + "systemShortcut = " + systemShortcutToString(mSystemShortcut) + + " }"; + } + + @Override + @DataClass.Generated.Member + public boolean equals(@Nullable Object o) { + // You can override field equality logic by defining either of the methods like: + // boolean fieldNameEquals(KeyboardSystemShortcut other) { ... } + // boolean fieldNameEquals(FieldType otherValue) { ... } + + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + @SuppressWarnings("unchecked") + KeyboardSystemShortcut that = (KeyboardSystemShortcut) o; + //noinspection PointlessBooleanExpression + return true + && java.util.Arrays.equals(mKeycodes, that.mKeycodes) + && mModifierState == that.mModifierState + && mSystemShortcut == that.mSystemShortcut; + } + + @Override + @DataClass.Generated.Member + public int hashCode() { + // You can override field hashCode logic by defining methods like: + // int fieldNameHashCode() { ... } + + int _hash = 1; + _hash = 31 * _hash + java.util.Arrays.hashCode(mKeycodes); + _hash = 31 * _hash + mModifierState; + _hash = 31 * _hash + mSystemShortcut; + return _hash; + } + + @DataClass.Generated( + time = 1722890917041L, + codegenVersion = "1.0.23", + sourceFile = "frameworks/base/core/java/android/hardware/input/KeyboardSystemShortcut.java", + inputSignatures = "private static final java.lang.String TAG\nprivate final @android.annotation.NonNull int[] mKeycodes\nprivate final int mModifierState\nprivate final @android.hardware.input.KeyboardSystemShortcut.SystemShortcut int mSystemShortcut\npublic static final int SYSTEM_SHORTCUT_UNSPECIFIED\npublic static final int SYSTEM_SHORTCUT_HOME\npublic static final int SYSTEM_SHORTCUT_RECENT_APPS\npublic static final int SYSTEM_SHORTCUT_BACK\npublic static final int SYSTEM_SHORTCUT_APP_SWITCH\npublic static final int SYSTEM_SHORTCUT_LAUNCH_ASSISTANT\npublic static final int SYSTEM_SHORTCUT_LAUNCH_VOICE_ASSISTANT\npublic static final int SYSTEM_SHORTCUT_LAUNCH_SYSTEM_SETTINGS\npublic static final int SYSTEM_SHORTCUT_TOGGLE_NOTIFICATION_PANEL\npublic static final int SYSTEM_SHORTCUT_TOGGLE_TASKBAR\npublic static final int SYSTEM_SHORTCUT_TAKE_SCREENSHOT\npublic static final int SYSTEM_SHORTCUT_OPEN_SHORTCUT_HELPER\npublic static final int SYSTEM_SHORTCUT_BRIGHTNESS_UP\npublic static final int SYSTEM_SHORTCUT_BRIGHTNESS_DOWN\npublic static final int SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_UP\npublic static final int SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_DOWN\npublic static final int SYSTEM_SHORTCUT_KEYBOARD_BACKLIGHT_TOGGLE\npublic static final int SYSTEM_SHORTCUT_VOLUME_UP\npublic static final int SYSTEM_SHORTCUT_VOLUME_DOWN\npublic static final int SYSTEM_SHORTCUT_VOLUME_MUTE\npublic static final int SYSTEM_SHORTCUT_ALL_APPS\npublic static final int SYSTEM_SHORTCUT_LAUNCH_SEARCH\npublic static final int SYSTEM_SHORTCUT_LANGUAGE_SWITCH\npublic static final int SYSTEM_SHORTCUT_ACCESSIBILITY_ALL_APPS\npublic static final int SYSTEM_SHORTCUT_TOGGLE_CAPS_LOCK\npublic static final int SYSTEM_SHORTCUT_SYSTEM_MUTE\npublic static final int SYSTEM_SHORTCUT_SPLIT_SCREEN_NAVIGATION\npublic static final int SYSTEM_SHORTCUT_CHANGE_SPLITSCREEN_FOCUS\npublic static final int SYSTEM_SHORTCUT_TRIGGER_BUG_REPORT\npublic static final int SYSTEM_SHORTCUT_LOCK_SCREEN\npublic static final int SYSTEM_SHORTCUT_OPEN_NOTES\npublic static final int SYSTEM_SHORTCUT_TOGGLE_POWER\npublic static final int SYSTEM_SHORTCUT_SYSTEM_NAVIGATION\npublic static final int SYSTEM_SHORTCUT_SLEEP\npublic static final int SYSTEM_SHORTCUT_WAKEUP\npublic static final int SYSTEM_SHORTCUT_MEDIA_KEY\npublic static final int SYSTEM_SHORTCUT_LAUNCH_DEFAULT_BROWSER\npublic static final int SYSTEM_SHORTCUT_LAUNCH_DEFAULT_EMAIL\npublic static final int SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CONTACTS\npublic static final int SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CALENDAR\npublic static final int SYSTEM_SHORTCUT_LAUNCH_DEFAULT_CALCULATOR\npublic static final int SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MUSIC\npublic static final int SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MAPS\npublic static final int SYSTEM_SHORTCUT_LAUNCH_DEFAULT_MESSAGING\npublic static final int SYSTEM_SHORTCUT_LAUNCH_DEFAULT_GALLERY\npublic static final int SYSTEM_SHORTCUT_LAUNCH_DEFAULT_FILES\npublic static final int SYSTEM_SHORTCUT_LAUNCH_DEFAULT_WEATHER\npublic static final int SYSTEM_SHORTCUT_LAUNCH_DEFAULT_FITNESS\npublic static final int SYSTEM_SHORTCUT_LAUNCH_APPLICATION_BY_PACKAGE_NAME\npublic static final int SYSTEM_SHORTCUT_DESKTOP_MODE\npublic static final int SYSTEM_SHORTCUT_MULTI_WINDOW_NAVIGATION\nclass KeyboardSystemShortcut extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genToString=true, genEqualsHashCode=true)") + @Deprecated + private void __metadata() {} + + + //@formatter:on + // End of generated code + +} diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java index e79b8f3f52e6..de3984756416 100644 --- a/core/java/android/inputmethodservice/InputMethodService.java +++ b/core/java/android/inputmethodservice/InputMethodService.java @@ -524,19 +524,12 @@ public class InputMethodService extends AbstractInputMethodService { /** * @hide - * The IME is active and ready with views but set invisible. - * This flag cannot be combined with {@link #IME_VISIBLE}. - */ - public static final int IME_INVISIBLE = 0x4; - - /** - * @hide * The IME is visible, but not yet perceptible to the user (e.g. fading in) * by {@link android.view.WindowInsetsController}. * * @see InputMethodManager#reportPerceptible */ - public static final int IME_VISIBLE_IMPERCEPTIBLE = 0x8; + public static final int IME_VISIBLE_IMPERCEPTIBLE = 0x4; // Min and max values for back disposition. private static final int BACK_DISPOSITION_MIN = BACK_DISPOSITION_DEFAULT; @@ -3125,7 +3118,7 @@ public class InputMethodService extends AbstractInputMethodService { mInShowWindow = true; final int previousImeWindowStatus = (mDecorViewVisible ? IME_ACTIVE : 0) | (isInputViewShown() - ? (!mWindowVisible ? IME_INVISIBLE : IME_VISIBLE) : 0); + ? (!mWindowVisible ? -1 : IME_VISIBLE) : 0); startViews(prepareWindow(showInput)); final int nextImeWindowStatus = mapToImeWindowStatus(); if (previousImeWindowStatus != nextImeWindowStatus) { diff --git a/core/java/android/os/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/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/flags.aconfig b/core/java/android/os/vibrator/flags.aconfig index 3fad8dd1da9c..1a19bb28fc71 100644 --- a/core/java/android/os/vibrator/flags.aconfig +++ b/core/java/android/os/vibrator/flags.aconfig @@ -3,13 +3,6 @@ container: "system" flag { namespace: "haptics" - name: "use_vibrator_haptic_feedback" - description: "Enables performHapticFeedback to directly use the vibrator service instead of going through the window session" - bug: "295459081" -} - -flag { - namespace: "haptics" name: "haptic_feedback_vibration_oem_customization_enabled" description: "Enables OEMs/devices to customize vibrations for haptic feedback" # Make read only. This is because the flag is used only once, and this could happen before @@ -88,3 +81,25 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + namespace: "haptics" + name: "load_haptic_feedback_vibration_customization_from_resources" + description: "Load haptic feedback vibrations customization from resources." + is_fixed_read_only: true + bug: "295142743" + metadata { + purpose: PURPOSE_FEATURE + } +} + +flag { + namespace: "haptics" + name: "haptic_feedback_input_source_customization_enabled" + description: "Enabled the extended haptic feedback customization by input source." + bug: "331819348" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_FEATURE + } +} diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 5703f693792c..791b306f5f47 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -12543,6 +12543,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 diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java index 5e15e0160be4..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; @@ -454,7 +456,7 @@ public class ZenModeConfig implements Parcelable { newRule.conditionId = Uri.EMPTY; newRule.allowManualInvocation = true; newRule.zenPolicy = getDefaultZenPolicy(); - newRule.pkg = "android"; + newRule.pkg = PACKAGE_ANDROID; manualRule = newRule; } } @@ -957,15 +959,9 @@ public class ZenModeConfig implements Parcelable { rt.user = safeInt(parser, ZEN_ATT_USER, rt.user); boolean readSuppressedEffects = false; boolean readManualRule = false; + boolean readManualRuleWithoutPolicy = false; while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) { tag = parser.getName(); - if (type == XmlPullParser.END_TAG && ZEN_TAG.equals(tag)) { - if (Flags.modesUi() && !readManualRule) { - // migrate from fields on config into manual rule - rt.manualRule.zenPolicy = rt.toZenPolicy(); - } - return rt; - } if (type == XmlPullParser.START_TAG) { if (ALLOW_TAG.equals(tag)) { rt.allowCalls = safeBoolean(parser, ALLOW_ATT_CALLS, @@ -1034,9 +1030,17 @@ public class ZenModeConfig implements Parcelable { rt.suppressedVisualEffects = safeInt(parser, DISALLOW_ATT_VISUAL_EFFECTS, DEFAULT_SUPPRESSED_VISUAL_EFFECTS); } else if (MANUAL_TAG.equals(tag)) { - rt.manualRule = readRuleXml(parser); - if (rt.manualRule != null) { + ZenRule manualRule = readRuleXml(parser); + if (manualRule != null) { + rt.manualRule = manualRule; + + // Manual rule may be present prior to modes_ui if it were on, but in that + // case it would not have a set policy, so make note of the need to set + // it up later. readManualRule = true; + if (rt.manualRule.zenPolicy == null) { + readManualRuleWithoutPolicy = true; + } } } else if (AUTOMATIC_TAG.equals(tag) || (Flags.modesApi() && AUTOMATIC_DELETED_TAG.equals(tag))) { @@ -1058,6 +1062,23 @@ public class ZenModeConfig implements Parcelable { STATE_ATT_CHANNELS_BYPASSING_DND, DEFAULT_CHANNELS_BYPASSING_DND); } } + if (type == XmlPullParser.END_TAG && ZEN_TAG.equals(tag)) { + if (Flags.modesUi() && (!readManualRule || readManualRuleWithoutPolicy)) { + // migrate from fields on config into manual rule + rt.manualRule.zenPolicy = rt.toZenPolicy(); + if (readManualRuleWithoutPolicy) { + // indicates that the xml represents a pre-modes_ui XML with an enabled + // manual rule; set rule active, and fill in other fields as would be done + // in ensureManualZenRule() and setManualZenMode(). + rt.manualRule.pkg = PACKAGE_ANDROID; + rt.manualRule.type = AutomaticZenRule.TYPE_OTHER; + rt.manualRule.condition = new Condition( + rt.manualRule.conditionId != null ? rt.manualRule.conditionId + : Uri.EMPTY, "", STATE_TRUE); + } + } + return rt; + } } throw new IllegalStateException("Failed to reach END_DOCUMENT"); } diff --git a/core/java/android/text/flags/flags.aconfig b/core/java/android/text/flags/flags.aconfig index 88a1b9c562d3..1b85191015a1 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." diff --git a/core/java/android/text/method/InsertModeTransformationMethod.java b/core/java/android/text/method/InsertModeTransformationMethod.java index 59b80f3a6468..6c6576f8888e 100644 --- a/core/java/android/text/method/InsertModeTransformationMethod.java +++ b/core/java/android/text/method/InsertModeTransformationMethod.java @@ -36,6 +36,7 @@ import android.view.View; import com.android.internal.util.ArrayUtils; import com.android.internal.util.Preconditions; +import com.android.text.flags.Flags; import java.lang.reflect.Array; @@ -171,9 +172,15 @@ public class InsertModeTransformationMethod implements TransformationMethod, Tex // The text change is before the highlight start, move the highlight start. mStart += diff; } else { - // The text change covers the highlight start. Extend the highlight start to the - // change start. This should be a rare case. - mStart = start; + if (Flags.insertModeHighlightRange()) { + // The text change covers the highlight start. Don't change the start except + // when it's out of range. + mStart = Math.min(mStart, s.length()); + } else { + // The text change covers the highlight start. Extend the highlight start to the + // change start. This should be a rare case. + mStart = start; + } } } @@ -181,9 +188,15 @@ public class InsertModeTransformationMethod implements TransformationMethod, Tex // The text change is before the highlight end, move the highlight end. mEnd += diff; } else if (start < mEnd) { - // The text change covers the highlight end. Extend the highlight end to the - // change end. This should be a rare case. - mEnd = start + count; + if (Flags.insertModeHighlightRange()) { + // The text change covers the highlight end. Don't change the end except when it's + // out of range. + mEnd = Math.min(mEnd, s.length()); + } else { + // The text change covers the highlight end. Extend the highlight end to the + // change end. This should be a rare case. + mEnd = start + count; + } } } diff --git a/core/java/android/text/style/AccessibilityClickableSpan.java b/core/java/android/text/style/AccessibilityClickableSpan.java index 534ce63349e0..ee8d156f9aac 100644 --- a/core/java/android/text/style/AccessibilityClickableSpan.java +++ b/core/java/android/text/style/AccessibilityClickableSpan.java @@ -156,4 +156,12 @@ public class AccessibilityClickableSpan extends ClickableSpan return new AccessibilityClickableSpan[size]; } }; + + /** + * @return the ID of the original clickable span that this is applied to. + * @hide + */ + public int getOriginalClickableSpanId() { + return mOriginalClickableSpanId; + } } diff --git a/core/java/android/text/style/BulletSpan.java b/core/java/android/text/style/BulletSpan.java index b3e7bda07413..f70e6c56b5c9 100644 --- a/core/java/android/text/style/BulletSpan.java +++ b/core/java/android/text/style/BulletSpan.java @@ -119,7 +119,10 @@ public class BulletSpan implements LeadingMarginSpan, ParcelableSpan { this(gapWidth, color, true, bulletRadius); } - private BulletSpan(int gapWidth, @ColorInt int color, boolean wantColor, + /** + * @hide + */ + public BulletSpan(int gapWidth, @ColorInt int color, boolean wantColor, @IntRange(from = 0) int bulletRadius) { mGapWidth = gapWidth; mBulletRadius = bulletRadius; @@ -199,6 +202,14 @@ public class BulletSpan implements LeadingMarginSpan, ParcelableSpan { return mColor; } + /** + * @return true if the bullet should apply the color. + * @hide + */ + public boolean getWantColor() { + return mWantColor; + } + @Override public void drawLeadingMargin(@NonNull Canvas canvas, @NonNull Paint paint, int x, int dir, int top, int baseline, int bottom, diff --git a/core/java/android/text/style/SuggestionSpan.java b/core/java/android/text/style/SuggestionSpan.java index ad044af91d59..0cf96f617f4a 100644 --- a/core/java/android/text/style/SuggestionSpan.java +++ b/core/java/android/text/style/SuggestionSpan.java @@ -248,21 +248,42 @@ public class SuggestionSpan extends CharacterStyle implements ParcelableSpan { } public SuggestionSpan(Parcel src) { - mSuggestions = src.readStringArray(); - mFlags = src.readInt(); - mLocaleStringForCompatibility = src.readString(); - mLanguageTag = src.readString(); - mHashCode = src.readInt(); - mEasyCorrectUnderlineColor = src.readInt(); - mEasyCorrectUnderlineThickness = src.readFloat(); - mMisspelledUnderlineColor = src.readInt(); - mMisspelledUnderlineThickness = src.readFloat(); - mAutoCorrectionUnderlineColor = src.readInt(); - mAutoCorrectionUnderlineThickness = src.readFloat(); - mGrammarErrorUnderlineColor = src.readInt(); - mGrammarErrorUnderlineThickness = src.readFloat(); + this(/* suggestions= */ src.readStringArray(), /* flags= */ src.readInt(), + /* localStringForCompatibility= */ src.readString(), + /* languageTag= */ src.readString(), /* hashCode= */ src.readInt(), + /* easyCorrectUnderlineColor= */ src.readInt(), + /* easyCorrectUnderlineThickness= */ src.readFloat(), + /* misspelledUnderlineColor= */ src.readInt(), + /* misspelledUnderlineThickness= */ src.readFloat(), + /* autoCorrectionUnderlineColor= */ src.readInt(), + /* autoCorrectionUnderlineThickness= */ src.readFloat(), + /* grammarErrorUnderlineColor= */ src.readInt(), + /* grammarErrorUnderlineThickness= */ src.readFloat()); } + /** @hide */ + public SuggestionSpan(String[] suggestions, int flags, String localeStringForCompatibility, + String languageTag, int hashCode, int easyCorrectUnderlineColor, + float easyCorrectUnderlineThickness, int misspelledUnderlineColor, + float misspelledUnderlineThickness, int autoCorrectionUnderlineColor, + float autoCorrectionUnderlineThickness, int grammarErrorUnderlineColor, + float grammarErrorUnderlineThickness) { + mSuggestions = suggestions; + mFlags = flags; + mLocaleStringForCompatibility = localeStringForCompatibility; + mLanguageTag = languageTag; + mHashCode = hashCode; + mEasyCorrectUnderlineColor = easyCorrectUnderlineColor; + mEasyCorrectUnderlineThickness = easyCorrectUnderlineThickness; + mMisspelledUnderlineColor = misspelledUnderlineColor; + mMisspelledUnderlineThickness = misspelledUnderlineThickness; + mAutoCorrectionUnderlineColor = autoCorrectionUnderlineColor; + mAutoCorrectionUnderlineThickness = autoCorrectionUnderlineThickness; + mGrammarErrorUnderlineColor = grammarErrorUnderlineColor; + mGrammarErrorUnderlineThickness = grammarErrorUnderlineThickness; + } + + /** * @return an array of suggestion texts for this span */ @@ -452,4 +473,44 @@ public class SuggestionSpan extends CharacterStyle implements ParcelableSpan { public void notifySelection(Context context, String original, int index) { Log.w(TAG, "notifySelection() is deprecated. Does nothing."); } + + /** @hide */ + public float getEasyCorrectUnderlineThickness() { + return mEasyCorrectUnderlineThickness; + } + + /** @hide */ + public int getEasyCorrectUnderlineColor() { + return mEasyCorrectUnderlineColor; + } + + /** @hide */ + public float getMisspelledUnderlineThickness() { + return mMisspelledUnderlineThickness; + } + + /** @hide */ + public int getMisspelledUnderlineColor() { + return mMisspelledUnderlineColor; + } + + /** @hide */ + public float getAutoCorrectionUnderlineThickness() { + return mAutoCorrectionUnderlineThickness; + } + + /** @hide */ + public int getAutoCorrectionUnderlineColor() { + return mAutoCorrectionUnderlineColor; + } + + /** @hide */ + public float getGrammarErrorUnderlineThickness() { + return mGrammarErrorUnderlineThickness; + } + + /** @hide */ + public int getGrammarErrorUnderlineColor() { + return mGrammarErrorUnderlineColor; + } } diff --git a/core/java/android/text/style/TextAppearanceSpan.java b/core/java/android/text/style/TextAppearanceSpan.java index d61228b295af..245a9dbc9f6c 100644 --- a/core/java/android/text/style/TextAppearanceSpan.java +++ b/core/java/android/text/style/TextAppearanceSpan.java @@ -233,36 +233,59 @@ public class TextAppearanceSpan extends MetricAffectingSpan implements Parcelabl } public TextAppearanceSpan(Parcel src) { - mFamilyName = src.readString(); - mStyle = src.readInt(); - mTextSize = src.readInt(); - if (src.readInt() != 0) { - mTextColor = ColorStateList.CREATOR.createFromParcel(src); - } else { - mTextColor = null; - } - if (src.readInt() != 0) { - mTextColorLink = ColorStateList.CREATOR.createFromParcel(src); - } else { - mTextColorLink = null; - } - mTypeface = LeakyTypefaceStorage.readTypefaceFromParcel(src); + this(/* familyName= */ src.readString(), + /* style= */ src.readInt(), + /* textSize= */ src.readInt(), + /* textColor= */ (src.readInt() != 0) + ? ColorStateList.CREATOR.createFromParcel(src) : null, + /* textColorLink= */ (src.readInt() != 0) + ? ColorStateList.CREATOR.createFromParcel(src) : null, + /* typeface= */ LeakyTypefaceStorage.readTypefaceFromParcel(src), + /* textFontWeight= */ src.readInt(), + /* textLocales= */ + src.readParcelable(LocaleList.class.getClassLoader(), LocaleList.class), + /* shadowRadius= */ src.readFloat(), + /* shadowDx= */ src.readFloat(), + /* shadowDy= */ src.readFloat(), + /* shadowColor= */ src.readInt(), + /* hasElegantTextHeight= */ src.readBoolean(), + /* elegantTextHeight= */ src.readBoolean(), + /* hasLetterSpacing= */ src.readBoolean(), + /* letterSpacing= */ src.readFloat(), + /* fontFeatureSettings= */ src.readString(), + /* fontVariationSettings= */ src.readString()); + } - mTextFontWeight = src.readInt(); - mTextLocales = src.readParcelable(LocaleList.class.getClassLoader(), android.os.LocaleList.class); + /** @hide */ + public TextAppearanceSpan(@Nullable String familyName, int style, int textSize, + @Nullable ColorStateList textColor, @Nullable ColorStateList textColorLink, + @Nullable Typeface typeface, + int textFontWeight, @Nullable LocaleList textLocales, float shadowRadius, + float shadowDx, float shadowDy, int shadowColor, boolean hasElegantTextHeight, + boolean elegantTextHeight, boolean hasLetterSpacing, float letterSpacing, + @Nullable String fontFeatureSettings, @Nullable String fontVariationSettings) { + mFamilyName = familyName; + mStyle = style; + mTextSize = textSize; + mTextColor = textColor; + mTextColorLink = textColorLink; + mTypeface = typeface; - mShadowRadius = src.readFloat(); - mShadowDx = src.readFloat(); - mShadowDy = src.readFloat(); - mShadowColor = src.readInt(); + mTextFontWeight = textFontWeight; + mTextLocales = textLocales; - mHasElegantTextHeight = src.readBoolean(); - mElegantTextHeight = src.readBoolean(); - mHasLetterSpacing = src.readBoolean(); - mLetterSpacing = src.readFloat(); + mShadowRadius = shadowRadius; + mShadowDx = shadowDx; + mShadowDy = shadowDy; + mShadowColor = shadowColor; - mFontFeatureSettings = src.readString(); - mFontVariationSettings = src.readString(); + mHasElegantTextHeight = hasElegantTextHeight; + mElegantTextHeight = elegantTextHeight; + mHasLetterSpacing = hasLetterSpacing; + mLetterSpacing = letterSpacing; + + mFontFeatureSettings = fontFeatureSettings; + mFontVariationSettings = fontVariationSettings; } public int getSpanTypeId() { @@ -564,4 +587,14 @@ public class TextAppearanceSpan extends MetricAffectingSpan implements Parcelabl + ", fontVariationSettings='" + getFontVariationSettings() + '\'' + '}'; } + + /** @hide */ + public boolean hasElegantTextHeight() { + return mHasElegantTextHeight; + } + + /** @hide */ + public boolean hasLetterSpacing() { + return mHasLetterSpacing; + } } diff --git a/core/java/android/view/DisplayInfo.java b/core/java/android/view/DisplayInfo.java index 12dbc5afd0a3..157cec8a4d0f 100644 --- a/core/java/android/view/DisplayInfo.java +++ b/core/java/android/view/DisplayInfo.java @@ -708,7 +708,7 @@ public final class DisplayInfo implements Parcelable { */ @Nullable public Display.Mode findDefaultModeByRefreshRate(float refreshRate) { - Display.Mode[] modes = supportedModes; + Display.Mode[] modes = appsSupportedModes; Display.Mode defaultMode = getDefaultMode(); for (int i = 0; i < modes.length; i++) { if (modes[i].matches( @@ -723,7 +723,7 @@ public final class DisplayInfo implements Parcelable { * Returns the list of supported refresh rates in the default mode. */ public float[] getDefaultRefreshRates() { - Display.Mode[] modes = supportedModes; + Display.Mode[] modes = appsSupportedModes; ArraySet<Float> rates = new ArraySet<>(); Display.Mode defaultMode = getDefaultMode(); for (int i = 0; i < modes.length; i++) { diff --git a/core/java/android/view/HapticScrollFeedbackProvider.java b/core/java/android/view/HapticScrollFeedbackProvider.java index f370256391bb..0001176220b5 100644 --- a/core/java/android/view/HapticScrollFeedbackProvider.java +++ b/core/java/android/view/HapticScrollFeedbackProvider.java @@ -100,8 +100,12 @@ public class HapticScrollFeedbackProvider implements ScrollFeedbackProvider { if (Math.abs(mTotalScrollPixels) >= mTickIntervalPixels) { mTotalScrollPixels %= mTickIntervalPixels; - // TODO(b/239594271): create a new `performHapticFeedbackForDevice` and use that here. - mView.performHapticFeedback(HapticFeedbackConstants.SCROLL_TICK); + if (android.os.vibrator.Flags.hapticFeedbackInputSourceCustomizationEnabled()) { + mView.performHapticFeedbackForInputDevice( + HapticFeedbackConstants.SCROLL_TICK, inputDeviceId, source, /* flags= */ 0); + } else { + mView.performHapticFeedback(HapticFeedbackConstants.SCROLL_TICK); + } } } @@ -115,9 +119,12 @@ public class HapticScrollFeedbackProvider implements ScrollFeedbackProvider { if (!mCanPlayLimitFeedback) { return; } - - // TODO(b/239594271): create a new `performHapticFeedbackForDevice` and use that here. - mView.performHapticFeedback(HapticFeedbackConstants.SCROLL_LIMIT); + if (android.os.vibrator.Flags.hapticFeedbackInputSourceCustomizationEnabled()) { + mView.performHapticFeedbackForInputDevice( + HapticFeedbackConstants.SCROLL_LIMIT, inputDeviceId, source, /* flags= */ 0); + } else { + mView.performHapticFeedback(HapticFeedbackConstants.SCROLL_LIMIT); + } mCanPlayLimitFeedback = false; } @@ -128,8 +135,13 @@ public class HapticScrollFeedbackProvider implements ScrollFeedbackProvider { if (!mHapticScrollFeedbackEnabled) { return; } - // TODO(b/239594271): create a new `performHapticFeedbackForDevice` and use that here. - mView.performHapticFeedback(HapticFeedbackConstants.SCROLL_ITEM_FOCUS); + if (android.os.vibrator.Flags.hapticFeedbackInputSourceCustomizationEnabled()) { + mView.performHapticFeedbackForInputDevice( + HapticFeedbackConstants.SCROLL_ITEM_FOCUS, inputDeviceId, source, + /* flags= */ 0); + } else { + mView.performHapticFeedback(HapticFeedbackConstants.SCROLL_ITEM_FOCUS); + } mCanPlayLimitFeedback = true; } diff --git a/core/java/android/view/IWindowSession.aidl b/core/java/android/view/IWindowSession.aidl index 762a302e8170..11a3168daa0e 100644 --- a/core/java/android/view/IWindowSession.aidl +++ b/core/java/android/view/IWindowSession.aidl @@ -140,15 +140,6 @@ interface IWindowSession { oneway void finishDrawing(IWindow window, in SurfaceControl.Transaction postDrawTransaction, int seqId); - @UnsupportedAppUsage - boolean performHapticFeedback(int effectId, int flags, int privFlags); - - /** - * Called by attached views to perform predefined haptic feedback without requiring VIBRATE - * permission. - */ - oneway void performHapticFeedbackAsync(int effectId, int flags, int privFlags); - /** * Initiate the drag operation itself * diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java index d83f34436b1b..7896cbde678a 100644 --- a/core/java/android/view/InsetsController.java +++ b/core/java/android/view/InsetsController.java @@ -685,9 +685,6 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation */ private @InsetsType int mCancelledForNewAnimationTypes; - private final Runnable mInvokeControllableInsetsChangedListeners = - this::invokeControllableInsetsChangedListeners; - private final InsetsState.OnTraverseCallbacks mRemoveGoneSources = new InsetsState.OnTraverseCallbacks() { @@ -2206,7 +2203,6 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation * @return The types that are now animating due to a listener invoking control/show/hide */ private @InsetsType int invokeControllableInsetsChangedListeners() { - mHandler.removeCallbacks(mInvokeControllableInsetsChangedListeners); mLastStartedAnimTypes = 0; @InsetsType int types = calculateControllableTypes(); int size = mControllableInsetsChangedListeners.size(); diff --git a/core/java/android/view/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..b8a885e64acb 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; @@ -9236,6 +9234,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 +9667,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/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/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/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/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/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/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 6bb969bf49ad..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.MaxHeightFrameLayout> + <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> + + </LinearLayout> <LinearLayout style="?android:attr/buttonBarStyle" diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index bdcba9daa5ad..0d16e9c939d9 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -5462,7 +5462,9 @@ <java-symbol type="bool" name="config_enable_a11y_fullscreen_magnification_overscroll_handler" /> <java-symbol type="dimen" name="accessibility_fullscreen_magnification_gesture_edge_slop" /> + <!-- For HapticFeedbackConstants configurability defined at HapticFeedbackCustomization --> <java-symbol type="string" name="config_hapticFeedbackCustomizationFile" /> + <java-symbol type="xml" name="haptic_feedback_customization" /> <!-- For ActivityManager PSS profiling configurability --> <java-symbol type="bool" name="config_am_disablePssProfiling" /> diff --git a/core/res/res/xml/haptic_feedback_customization.xml b/core/res/res/xml/haptic_feedback_customization.xml new file mode 100644 index 000000000000..7ac0787ab7a0 --- /dev/null +++ b/core/res/res/xml/haptic_feedback_customization.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<haptic-feedback-constants/> diff --git a/core/tests/coretests/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/text/method/InsertModeTransformationMethodTest.java b/core/tests/coretests/src/android/text/method/InsertModeTransformationMethodTest.java index 2f336ab692f6..e2c19024a840 100644 --- a/core/tests/coretests/src/android/text/method/InsertModeTransformationMethodTest.java +++ b/core/tests/coretests/src/android/text/method/InsertModeTransformationMethodTest.java @@ -20,6 +20,10 @@ import static com.google.common.truth.Truth.assertThat; import android.content.Context; import android.platform.test.annotations.Presubmit; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.text.SpannableString; import android.text.SpannableStringBuilder; import android.text.Spanned; @@ -30,7 +34,10 @@ import androidx.test.InstrumentationRegistry; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.text.flags.Flags; + import org.junit.BeforeClass; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -41,6 +48,9 @@ public class InsertModeTransformationMethodTest { private static View sView; private static final String TEXT = "abc def"; + @Rule + public CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + @BeforeClass public static void setupClass() { final Context context = InstrumentationRegistry.getTargetContext(); @@ -76,11 +86,13 @@ public class InsertModeTransformationMethodTest { } @Test + @RequiresFlagsDisabled(Flags.FLAG_INSERT_MODE_HIGHLIGHT_RANGE) public void transformedText_charAt_editing() { transformedText_charAt_editing(false, "\n\n"); } @Test + @RequiresFlagsDisabled(Flags.FLAG_INSERT_MODE_HIGHLIGHT_RANGE) public void transformedText_charAt_singleLine_editing() { transformedText_charAt_editing(true, "\uFFFD"); } @@ -132,6 +144,64 @@ public class InsertModeTransformationMethodTest { } @Test + @RequiresFlagsEnabled(Flags.FLAG_INSERT_MODE_HIGHLIGHT_RANGE) + public void transformedText_charAt_editing_stickyHighlightRange() { + transformedText_charAt_editing_stickyHighlightRange(false, "\n\n"); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_INSERT_MODE_HIGHLIGHT_RANGE) + public void transformedText_charAt_singleLine_editing_stickyHighlightRange() { + transformedText_charAt_editing_stickyHighlightRange(true, "\uFFFD"); + } + + private void transformedText_charAt_editing_stickyHighlightRange(boolean singleLine, + String placeholder) { + final SpannableStringBuilder text = new SpannableStringBuilder(TEXT); + final InsertModeTransformationMethod transformationMethod = + new InsertModeTransformationMethod(3, singleLine, null); + final CharSequence transformedText = transformationMethod.getTransformation(text, sView); + // TransformationMethod is set on the original text as a TextWatcher in the TextView. + text.setSpan(transformationMethod, 0, text.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + assertCharSequence(transformedText, "abc" + placeholder + " def"); + + // original text is "abcxx def" after insertion. + text.insert(3, "xx"); + assertCharSequence(transformedText, "abcxx" + placeholder + " def"); + + // original text is "abcxx vvdef" after insertion. + text.insert(6, "vv"); + assertCharSequence(transformedText, "abcxx" + placeholder + " vvdef"); + + // original text is "abc vvdef" after deletion. + text.delete(3, 5); + assertCharSequence(transformedText, "abc" + placeholder + " vvdef"); + + // original text is "abc def" after deletion. + text.delete(4, 6); + assertCharSequence(transformedText, "abc" + placeholder + " def"); + + // original text is "abdef" after deletion. + // deletion range covers the placeholder's insertion point. It'll try to stay the same, + // which is still at index 3. + text.delete(2, 4); + assertCharSequence(transformedText, "abd" + placeholder + "ef"); + + // original text is "axxdef" after replace. + // this time the replaced range is ahead of the placeholder's insertion point. It updates to + // index 4. + text.replace(1, 2, "xx"); + assertCharSequence(transformedText, "axxd" + placeholder + "ef"); + + // original text is "ax" after replace. + // the deleted range covers the placeholder's insertion point. It tries to stay at index 4. + // However, 4 out of bounds now. So placeholder is inserted at the end of the string. + text.delete(2, 6); + assertCharSequence(transformedText, "ax" + placeholder); + } + + @Test public void transformedText_subSequence() { for (int offset = 0; offset < TEXT.length(); ++offset) { final InsertModeTransformationMethod transformationMethod = @@ -697,7 +767,7 @@ public class InsertModeTransformationMethodTest { } @Test - public void transformedText_getHighlightStartAndEnd_insertion_singleLine() { + public void transformedText_getHighlightStartAndEnd_singleLine_insertion() { transformedText_getHighlightStartAndEnd_insertion(true, "\uFDDD"); } @@ -751,16 +821,18 @@ public class InsertModeTransformationMethodTest { } @Test + @RequiresFlagsDisabled(Flags.FLAG_INSERT_MODE_HIGHLIGHT_RANGE) public void transformedText_getHighlightStartAndEnd_deletion() { transformedText_getHighlightStartAndEnd_deletion(false, "\n\n"); } @Test - public void transformedText_getHighlightStartAndEnd_insertion_deletion() { + @RequiresFlagsDisabled(Flags.FLAG_INSERT_MODE_HIGHLIGHT_RANGE) + public void transformedText_getHighlightStartAndEnd_singleLine_deletion() { transformedText_getHighlightStartAndEnd_deletion(true, "\uFDDD"); } - public void transformedText_getHighlightStartAndEnd_deletion(boolean singleLine, + private void transformedText_getHighlightStartAndEnd_deletion(boolean singleLine, String placeholder) { final SpannableStringBuilder text = new SpannableStringBuilder(TEXT); final InsertModeTransformationMethod transformationMethod = @@ -816,14 +888,93 @@ public class InsertModeTransformationMethodTest { assertThat(transformedText.getHighlightEnd()).isEqualTo(1 + placeholder.length()); } + @Test + @RequiresFlagsEnabled(Flags.FLAG_INSERT_MODE_HIGHLIGHT_RANGE) + public void transformedText_getHighlightStartAndEnd_deletion_stickyHighlightRange() { + transformedText_getHighlightStartAndEnd_deletion_stickyHighlightRange(false, "\n\n"); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_INSERT_MODE_HIGHLIGHT_RANGE) + public void transformedText_getHighlightStartAndEnd_singleLine_deletion_stickyHighlightRange() { + transformedText_getHighlightStartAndEnd_deletion_stickyHighlightRange(true, "\uFDDD"); + } + + private void transformedText_getHighlightStartAndEnd_deletion_stickyHighlightRange( + boolean singleLine, String placeholder) { + final SpannableStringBuilder text = new SpannableStringBuilder(TEXT); + final InsertModeTransformationMethod transformationMethod = + new InsertModeTransformationMethod(3, singleLine, null); + final InsertModeTransformationMethod.TransformedText transformedText = + (InsertModeTransformationMethod.TransformedText) transformationMethod + .getTransformation(text, sView); + // TransformationMethod is set on the original text as a TextWatcher in the TextView. + text.setSpan(transformationMethod, 0, text.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + // note: the placeholder text is also highlighted. + assertThat(transformedText.getHighlightStart()).isEqualTo(3); + assertThat(transformedText.getHighlightEnd()).isEqualTo(3 + placeholder.length()); + + // original text is "abcxxxxxx def" after insertion. + // the placeholder is now inserted at index 9. + // the highlight start is still 3. + // the highlight end now is 9 + placeholder.length(). + text.insert(3, "xxxxxx"); + assertThat(transformedText.getHighlightStart()).isEqualTo(3); + assertThat(transformedText.getHighlightEnd()).isEqualTo(9 + placeholder.length()); + + // original text is "abxxxxxx def" after deletion. + // the placeholder is now inserted at index 6. + // the highlight start is 2, since the deletion happens before the highlight range. + // the highlight end now is 8 + placeholder.length(). + text.delete(2, 3); + assertThat(transformedText.getHighlightStart()).isEqualTo(2); + assertThat(transformedText.getHighlightEnd()).isEqualTo(8 + placeholder.length()); + + // original text is "abxxx def" after deletion. + // the placeholder is now inserted at index 5. + // the highlight start is still 2, since the deletion happens in the highlight range. + // the highlight end now is 5 + placeholder.length(). + text.delete(2, 5); + assertThat(transformedText.getHighlightStart()).isEqualTo(2); + assertThat(transformedText.getHighlightEnd()).isEqualTo(5 + placeholder.length()); + + // original text is "abxxx d" after deletion. + // the placeholder is now inserted at index 5. + // the highlight start is still 2, since the deletion happens after the highlight range. + // the highlight end now is still 5 + placeholder.length(). + text.delete(7, 9); + assertThat(transformedText.getHighlightStart()).isEqualTo(2); + assertThat(transformedText.getHighlightEnd()).isEqualTo(5 + placeholder.length()); + + // original text is "axx d" after deletion. + // the placeholder is now inserted at index 3. + // the highlight start is at 2, since the deletion range covers the start. + // the highlight end is 3 + placeholder.length(). + text.delete(1, 3); + assertThat(transformedText.getHighlightStart()).isEqualTo(2); + assertThat(transformedText.getHighlightEnd()).isEqualTo(3 + placeholder.length()); + + // original text is "ax" after deletion. + // the placeholder is now inserted at index 2. + // the highlight start is at 2. + // the highlight end is 2 + placeholder.length(). It wants to stay at 3, but it'll be out + // of bounds, so it'll be 2 instead. + text.delete(2, 5); + assertThat(transformedText.getHighlightStart()).isEqualTo(2); + assertThat(transformedText.getHighlightEnd()).isEqualTo(2 + placeholder.length()); + } + @Test + @RequiresFlagsDisabled(Flags.FLAG_INSERT_MODE_HIGHLIGHT_RANGE) public void transformedText_getHighlightStartAndEnd_replace() { transformedText_getHighlightStartAndEnd_replace(false, "\n\n"); } @Test - public void transformedText_getHighlightStartAndEnd_insertion__replace() { + @RequiresFlagsDisabled(Flags.FLAG_INSERT_MODE_HIGHLIGHT_RANGE) + public void transformedText_getHighlightStartAndEnd_singleLine_replace() { transformedText_getHighlightStartAndEnd_replace(true, "\uFDDD"); } @@ -908,6 +1059,99 @@ public class InsertModeTransformationMethodTest { assertThat(transformedText.getHighlightEnd()).isEqualTo(3 + placeholder.length()); } + @Test + @RequiresFlagsEnabled(Flags.FLAG_INSERT_MODE_HIGHLIGHT_RANGE) + public void transformedText_getHighlightStartAndEnd_replace_stickyHighlightRange() { + transformedText_getHighlightStartAndEnd_replace_stickyHighlightRange(false, "\n\n"); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_INSERT_MODE_HIGHLIGHT_RANGE) + public void transformedText_getHighlightStartAndEnd_singleLine_replace_stickyHighlightRange() { + transformedText_getHighlightStartAndEnd_replace_stickyHighlightRange(true, "\uFDDD"); + } + + private void transformedText_getHighlightStartAndEnd_replace_stickyHighlightRange( + boolean singleLine, String placeholder) { + final SpannableStringBuilder text = new SpannableStringBuilder(TEXT); + final InsertModeTransformationMethod transformationMethod = + new InsertModeTransformationMethod(3, singleLine, null); + final InsertModeTransformationMethod.TransformedText transformedText = + (InsertModeTransformationMethod.TransformedText) transformationMethod + .getTransformation(text, sView); + // TransformationMethod is set on the original text as a TextWatcher in the TextView. + text.setSpan(transformationMethod, 0, text.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + // note: the placeholder text is also highlighted. + assertThat(transformedText.getHighlightStart()).isEqualTo(3); + assertThat(transformedText.getHighlightEnd()).isEqualTo(3 + placeholder.length()); + + // original text is "abcxxxxxx def" after insertion. + // the placeholder is now inserted at index 9. + // the highlight start is still 3. + // the highlight end now is 9 + placeholder.length(). + text.insert(3, "xxxxxx"); + assertThat(transformedText.getHighlightStart()).isEqualTo(3); + assertThat(transformedText.getHighlightEnd()).isEqualTo(9 + placeholder.length()); + + // original text is "abvvxxxxxx def" after replace. + // the replacement happens before the highlight range; highlight range is offset by 1 + // the placeholder is now inserted at index 10, + // the highlight start is 4. + // the highlight end is 10 + placeholder.length(). + text.replace(2, 3, "vv"); + assertThat(transformedText.getHighlightStart()).isEqualTo(4); + assertThat(transformedText.getHighlightEnd()).isEqualTo(10 + placeholder.length()); + + // original text is "abvvxxx def" after replace. + // the replacement happens in the highlight range; highlight end is offset by -3 + // the placeholder is now inserted at index 7, + // the highlight start is still 4. + // the highlight end is 7 + placeholder.length(). + text.replace(5, 9, "x"); + assertThat(transformedText.getHighlightStart()).isEqualTo(4); + assertThat(transformedText.getHighlightEnd()).isEqualTo(7 + placeholder.length()); + + // original text is "abvvxxxvvv" after replace. + // the replacement happens after the highlight range; highlight is not changed + // the placeholder is now inserted at index 7, + // the highlight start is still 4. + // the highlight end is 7 + placeholder.length(). + text.replace(7, 11, "vvv"); + assertThat(transformedText.getHighlightStart()).isEqualTo(4); + assertThat(transformedText.getHighlightEnd()).isEqualTo(7 + placeholder.length()); + + // original text is "abxxxxvvv" after replace. + // the replacement covers the highlight start; highlight start stays the same; + // highlight end is offset by -1 + // the placeholder is now inserted at index 6, + // the highlight start is 4. + // the highlight end is 6 + placeholder.length(). + text.replace(2, 5, "xx"); + assertThat(transformedText.getHighlightStart()).isEqualTo(4); + assertThat(transformedText.getHighlightEnd()).isEqualTo(6 + placeholder.length()); + + // original text is "abxxxxxvv" after replace. + // the replacement covers the highlight end; highlight end stays the same; + // highlight start stays the same + // the placeholder is now inserted at index 6, + // the highlight start is 2. + // the highlight end is 6 + placeholder.length(). + text.replace(5, 7, "xx"); + assertThat(transformedText.getHighlightStart()).isEqualTo(4); + assertThat(transformedText.getHighlightEnd()).isEqualTo(6 + placeholder.length()); + + // original text is "axxv" after replace. + // the replacement covers the highlight range; highlight start stays the same. + // highlight end shrink to the text length. + // the placeholder is now inserted at index 3, + // the highlight start is 2. + // the highlight end is 4 + placeholder.length(). + text.replace(1, 8, "xx"); + assertThat(transformedText.getHighlightStart()).isEqualTo(4); + assertThat(transformedText.getHighlightEnd()).isEqualTo(4 + placeholder.length()); + } + private static <T> void assertNextSpanTransition(Spanned spanned, int[] transitions, Class<T> type) { int currentTransition = 0; diff --git a/core/tests/coretests/src/android/view/HapticScrollFeedbackProviderTest.java b/core/tests/coretests/src/android/view/HapticScrollFeedbackProviderTest.java index b9a9557df0bc..ac6c19e79fcb 100644 --- a/core/tests/coretests/src/android/view/HapticScrollFeedbackProviderTest.java +++ b/core/tests/coretests/src/android/view/HapticScrollFeedbackProviderTest.java @@ -16,16 +16,20 @@ package android.view; +import static android.os.vibrator.Flags.FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED; import static android.view.HapticFeedbackConstants.SCROLL_ITEM_FOCUS; import static android.view.HapticFeedbackConstants.SCROLL_LIMIT; import static android.view.HapticFeedbackConstants.SCROLL_TICK; + import static com.google.common.truth.Truth.assertThat; + import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import android.content.Context; import android.platform.test.annotations.Presubmit; +import android.platform.test.flag.junit.SetFlagsRule; import android.view.flags.FeatureFlags; import androidx.test.InstrumentationRegistry; @@ -33,17 +37,24 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.Objects; +// TODO(b/353625893): update old tests to use new infra like those with "inputDeviceCustomized". @SmallTest @RunWith(AndroidJUnit4.class) @Presubmit public final class HapticScrollFeedbackProviderTest { + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); private static final int INPUT_DEVICE_1 = 1; private static final int INPUT_DEVICE_2 = 2; @@ -64,6 +75,7 @@ public final class HapticScrollFeedbackProviderTest { mView = new TestView(InstrumentationRegistry.getContext()); mProvider = new HapticScrollFeedbackProvider(mView, mMockViewConfig, /* disabledIfViewPlaysScrollHaptics= */ true); + mSetFlagsRule.disableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); } @Test @@ -85,6 +97,26 @@ public final class HapticScrollFeedbackProviderTest { } @Test + public void testRotaryEncoder_inputDeviceCustomized_noFeedbackWhenViewBasedFeedbackIsEnabled() { + mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); + + when(mMockViewConfig.isViewBasedRotaryEncoderHapticScrollFeedbackEnabled()) + .thenReturn(true); + setHapticScrollTickInterval(5); + + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ 10); + mProvider.onSnapToItem( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ true); + + assertThat(mView.mHapticFeedbackRequests).hasSize(0); + } + + @Test public void testRotaryEncoder_feedbackWhenDisregardingViewBasedScrollHaptics() { mProvider = new HapticScrollFeedbackProvider(mView, mMockViewConfig, /* disabledIfViewPlaysScrollHaptics= */ false); @@ -107,6 +139,35 @@ public final class HapticScrollFeedbackProviderTest { } @Test + public void testRotaryEncoder_inputDeviceCustomized_feedbackWhenDisregardingViewBasedScrollHaptics() { + mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); + List<HapticFeedbackRequest> requests = new ArrayList<>(); + + mProvider = new HapticScrollFeedbackProvider(mView, mMockViewConfig, + /* disabledIfViewPlaysScrollHaptics= */ false); + when(mMockViewConfig.isViewBasedRotaryEncoderHapticScrollFeedbackEnabled()) + .thenReturn(true); + setHapticScrollTickInterval(5); + + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ 10); + requests.add(new HapticFeedbackRequest( + SCROLL_TICK, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + mProvider.onSnapToItem( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); + requests.add(new HapticFeedbackRequest( + SCROLL_ITEM_FOCUS, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ true); + requests.add(new HapticFeedbackRequest( + SCROLL_LIMIT, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + + assertThat(mView.mHapticFeedbackRequests).containsExactlyElementsIn(requests).inOrder(); + } + + @Test public void testNoFeedbackWhenFeedbackIsDisabled() { setHapticScrollFeedbackEnabled(false); // Call different types scroll feedback methods; non of them should produce feedback because @@ -130,6 +191,31 @@ public final class HapticScrollFeedbackProviderTest { } @Test + public void testNoFeedbackWhenFeedbackIsDisabled_inputDeviceCustomized() { + mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); + + setHapticScrollFeedbackEnabled(false); + // Call different types scroll feedback methods; non of them should produce feedback because + // feedback has been disabled. + mProvider.onSnapToItem( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ true); + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ 300); + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ -300); + + assertThat(mView.mHapticFeedbackRequests).hasSize(0); + } + + @Test public void testSnapToItem() { mProvider.onSnapToItem( INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); @@ -138,6 +224,25 @@ public final class HapticScrollFeedbackProviderTest { } @Test + public void testSnapToItem_inputDeviceCustomized() { + mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); + List<HapticFeedbackRequest> requests = new ArrayList<>(); + + mProvider.onSnapToItem( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); + requests.add(new HapticFeedbackRequest( + SCROLL_ITEM_FOCUS, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + + mProvider.onSnapToItem( + INPUT_DEVICE_2, InputDevice.SOURCE_TOUCHSCREEN, MotionEvent.AXIS_SCROLL); + requests.add( + new HapticFeedbackRequest( + SCROLL_ITEM_FOCUS, INPUT_DEVICE_2, InputDevice.SOURCE_TOUCHSCREEN)); + + assertThat(mView.mHapticFeedbackRequests).containsExactlyElementsIn(requests).inOrder(); + } + + @Test public void testScrollLimit_start() { mProvider.onSnapToItem( INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); @@ -150,6 +255,24 @@ public final class HapticScrollFeedbackProviderTest { } @Test + public void testScrollLimit_inputDeviceCustomized_start() { + mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); + List<HapticFeedbackRequest> requests = new ArrayList<>(); + + mProvider.onSnapToItem( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); + requests.add(new HapticFeedbackRequest( + SCROLL_ITEM_FOCUS, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ true); + requests.add(new HapticFeedbackRequest( + SCROLL_LIMIT, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + + assertThat(mView.mHapticFeedbackRequests).containsExactlyElementsIn(requests).inOrder(); + } + + @Test public void testScrollLimit_stop() { mProvider.onSnapToItem( INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); @@ -162,6 +285,24 @@ public final class HapticScrollFeedbackProviderTest { } @Test + public void testScrollLimit_inputDeviceCustomized_stop() { + mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); + List<HapticFeedbackRequest> requests = new ArrayList<>(); + + mProvider.onSnapToItem( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); + requests.add(new HapticFeedbackRequest( + SCROLL_ITEM_FOCUS, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + requests.add(new HapticFeedbackRequest( + SCROLL_LIMIT, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + + assertThat(mView.mHapticFeedbackRequests).containsExactlyElementsIn(requests).inOrder(); + } + + @Test public void testScrollProgress_zeroTickInterval() { setHapticScrollTickInterval(0); @@ -176,6 +317,22 @@ public final class HapticScrollFeedbackProviderTest { } @Test + public void testScrollProgress_inputDeviceCustomized_zeroTickInterval() { + mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); + + setHapticScrollTickInterval(0); + + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ 30); + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ 20); + + assertThat(mView.mHapticFeedbackRequests).hasSize(0); + } + + @Test public void testScrollProgress_progressEqualsOrExceedsPositiveThreshold() { setHapticScrollTickInterval(100); mProvider.onScrollProgress( @@ -198,6 +355,32 @@ public final class HapticScrollFeedbackProviderTest { } @Test + public void testScrollProgress_inputDeviceCustomized_progressEqualsOrExceedsPositiveThreshold() { + mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); + List<HapticFeedbackRequest> requests = new ArrayList<>(); + setHapticScrollTickInterval(100); + + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ 20); + + assertThat(mView.mHapticFeedbackRequests).hasSize(0); + + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ 80); + requests.add(new HapticFeedbackRequest( + SCROLL_TICK, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ 120); + requests.add(new HapticFeedbackRequest( + SCROLL_TICK, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + + assertThat(mView.mHapticFeedbackRequests).containsExactlyElementsIn(requests).inOrder(); + } + + @Test public void testScrollProgress_progressEqualsOrExceedsNegativeThreshold() { setHapticScrollTickInterval(100); @@ -224,6 +407,35 @@ public final class HapticScrollFeedbackProviderTest { } @Test + public void testScrollProgress_inputDeviceCustomized_progressEqualsOrExceedsNegativeThreshold() { + mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); + List<HapticFeedbackRequest> requests = new ArrayList<>(); + setHapticScrollTickInterval(100); + + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ -20); + + assertThat(mView.mHapticFeedbackRequests).hasSize(0); + + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ -80); + requests.add(new HapticFeedbackRequest( + SCROLL_TICK, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ -70); + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ -40); + requests.add(new HapticFeedbackRequest( + SCROLL_TICK, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + + assertThat(mView.mHapticFeedbackRequests).containsExactlyElementsIn(requests).inOrder(); + } + + @Test public void testScrollProgress_positiveAndNegativeProgresses() { setHapticScrollTickInterval(100); @@ -262,6 +474,54 @@ public final class HapticScrollFeedbackProviderTest { } @Test + public void testScrollProgress_inputDeviceCustomized_positiveAndNegativeProgresses() { + mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); + List<HapticFeedbackRequest> requests = new ArrayList<>(); + setHapticScrollTickInterval(100); + + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ 20); + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ -90); + + // total pixel abs = 70 + assertThat(mView.mHapticFeedbackRequests).hasSize(0); + + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ 10); + + // total pixel abs = 60 + assertThat(mView.mHapticFeedbackRequests).hasSize(0); + + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ -50); + // total pixel abs = 110. Passed threshold. total pixel reduced to -10. + requests.add(new HapticFeedbackRequest( + SCROLL_TICK, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + + assertThat(mView.mHapticFeedbackRequests).containsExactlyElementsIn(requests).inOrder(); + + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ 40); + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ 50); + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ 60); + // total pixel abs = 140. Passed threshold. total pixel reduced to 40. + requests.add(new HapticFeedbackRequest( + SCROLL_TICK, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + + assertThat(mView.mHapticFeedbackRequests).containsExactlyElementsIn(requests).inOrder(); + } + + @Test public void testScrollProgress_singleProgressExceedsThreshold() { setHapticScrollTickInterval(100); @@ -273,6 +533,21 @@ public final class HapticScrollFeedbackProviderTest { } @Test + public void testScrollProgress_inputDeviceCustomized_singleProgressExceedsThreshold() { + mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); + List<HapticFeedbackRequest> requests = new ArrayList<>(); + setHapticScrollTickInterval(100); + + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ 1000); + requests.add(new HapticFeedbackRequest( + SCROLL_TICK, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + + assertThat(mView.mHapticFeedbackRequests).containsExactlyElementsIn(requests).inOrder(); + } + + @Test public void testScrollLimit_startAndEndLimit_playsOnlyOneFeedback() { mProvider.onSnapToItem( INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); @@ -288,6 +563,29 @@ public final class HapticScrollFeedbackProviderTest { } @Test + public void testScrollLimit_startAndEndLimit_inputDeviceCustomized_playsOnlyOneFeedback() { + mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); + List<HapticFeedbackRequest> requests = new ArrayList<>(); + + mProvider.onSnapToItem( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); + requests.add(new HapticFeedbackRequest( + SCROLL_ITEM_FOCUS, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + // end played. + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + requests.add(new HapticFeedbackRequest( + SCROLL_LIMIT, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + // start after end NOT played. + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ true); + + assertThat(mView.mHapticFeedbackRequests).containsExactlyElementsIn(requests).inOrder(); + } + + @Test public void testScrollLimit_doubleStartLimit_playsOnlyOneFeedback() { mProvider.onSnapToItem( INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); @@ -303,6 +601,29 @@ public final class HapticScrollFeedbackProviderTest { } @Test + public void testScrollLimit_doubleStartLimit_inputDeviceCustomized_playsOnlyOneFeedback() { + mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); + List<HapticFeedbackRequest> requests = new ArrayList<>(); + + mProvider.onSnapToItem( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); + requests.add(new HapticFeedbackRequest( + SCROLL_ITEM_FOCUS, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + // 1st start played. + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ true); + requests.add(new HapticFeedbackRequest( + SCROLL_LIMIT, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + // 2nd start NOT played. + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ true); + + assertThat(mView.mHapticFeedbackRequests).containsExactlyElementsIn(requests).inOrder(); + } + + @Test public void testScrollLimit_doubleEndLimit_playsOnlyOneFeedback() { mProvider.onSnapToItem( INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); @@ -318,6 +639,29 @@ public final class HapticScrollFeedbackProviderTest { } @Test + public void testScrollLimit_doubleEndLimit_inputDeviceCustomized_playsOnlyOneFeedback() { + mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); + List<HapticFeedbackRequest> requests = new ArrayList<>(); + + mProvider.onSnapToItem( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); + requests.add(new HapticFeedbackRequest( + SCROLL_ITEM_FOCUS, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + // 1st end played. + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + requests.add(new HapticFeedbackRequest( + SCROLL_LIMIT, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + // 2nd end NOT played. + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + + assertThat(mView.mHapticFeedbackRequests).containsExactlyElementsIn(requests).inOrder(); + } + + @Test public void testScrollLimit_notEnabledWithZeroProgress() { mProvider.onSnapToItem( INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); @@ -339,6 +683,36 @@ public final class HapticScrollFeedbackProviderTest { } @Test + public void testScrollLimit_inputDeviceCustomized_notEnabledWithZeroProgress() { + mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); + List<HapticFeedbackRequest> requests = new ArrayList<>(); + + mProvider.onSnapToItem( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); + requests.add(new HapticFeedbackRequest( + SCROLL_ITEM_FOCUS, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + // end played. + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + requests.add(new HapticFeedbackRequest( + SCROLL_LIMIT, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + + // progress 0. scroll not started. + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ 0); + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ true); + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + + assertThat(mView.mHapticFeedbackRequests).containsExactlyElementsIn(requests).inOrder(); + } + + @Test public void testScrollLimit_enabledWithProgress() { mProvider.onSnapToItem( INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); @@ -357,6 +731,36 @@ public final class HapticScrollFeedbackProviderTest { } @Test + public void testScrollLimit_inputDeviceCustomized_enabledWithProgress() { + mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); + List<HapticFeedbackRequest> requests = new ArrayList<>(); + + mProvider.onSnapToItem( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); + requests.add(new HapticFeedbackRequest( + SCROLL_ITEM_FOCUS, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + // end played. + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + requests.add(new HapticFeedbackRequest( + SCROLL_LIMIT, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + // No tick since tick-interval is default 0, which means no tick. + // But still re-enable next limit feedback. + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ 80); + // scroll pixel not 0, so end played. + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + requests.add(new HapticFeedbackRequest( + SCROLL_LIMIT, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + + assertThat(mView.mHapticFeedbackRequests).containsExactlyElementsIn(requests).inOrder(); + } + + @Test public void testScrollLimit_enabledWithSnap() { mProvider.onSnapToItem( INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); @@ -374,6 +778,35 @@ public final class HapticScrollFeedbackProviderTest { } @Test + public void testScrollLimit_inputDeviceCustomized_enabledWithSnap() { + mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); + List<HapticFeedbackRequest> requests = new ArrayList<>(); + + // 1st enabled limit by snap + mProvider.onSnapToItem( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); + requests.add(new HapticFeedbackRequest( + SCROLL_ITEM_FOCUS, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + requests.add(new HapticFeedbackRequest( + SCROLL_LIMIT, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + // 2nd enabled limit by snap + mProvider.onSnapToItem( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); + requests.add(new HapticFeedbackRequest( + SCROLL_ITEM_FOCUS, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + requests.add(new HapticFeedbackRequest( + SCROLL_LIMIT, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + + assertThat(mView.mHapticFeedbackRequests).containsExactlyElementsIn(requests).inOrder(); + } + + @Test public void testScrollLimit_notEnabledWithDissimilarSnap() { mProvider.onSnapToItem( INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); @@ -391,6 +824,33 @@ public final class HapticScrollFeedbackProviderTest { } @Test + public void testScrollLimit_inputDeviceCustomized_notEnabledWithDissimilarSnap() { + mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); + List<HapticFeedbackRequest> requests = new ArrayList<>(); + + mProvider.onSnapToItem( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); + requests.add(new HapticFeedbackRequest( + SCROLL_ITEM_FOCUS, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + requests.add(new HapticFeedbackRequest( + SCROLL_LIMIT, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + + mProvider.onSnapToItem( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_X); + requests.add(new HapticFeedbackRequest( + SCROLL_ITEM_FOCUS, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + // Last snap is on AXIS_X, so end on AXIS_SCROLL is NOT played. + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + + assertThat(mView.mHapticFeedbackRequests).containsExactlyElementsIn(requests).inOrder(); + } + + @Test public void testScrollLimit_enabledWithDissimilarProgress() { mProvider.onSnapToItem( INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); @@ -408,6 +868,33 @@ public final class HapticScrollFeedbackProviderTest { assertFeedbackCount(mView, HapticFeedbackConstants.SCROLL_LIMIT, 2); } + @Test + public void testScrollLimit_inputDeviceCustomized_enabledWithDissimilarProgress() { + mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); + List<HapticFeedbackRequest> requests = new ArrayList<>(); + + mProvider.onSnapToItem( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); + requests.add(new HapticFeedbackRequest( + SCROLL_ITEM_FOCUS, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + requests.add(new HapticFeedbackRequest( + SCROLL_LIMIT, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + // No tick since tick-interval is default 0, which means no tick. + // But still re-enable next limit feedback. + mProvider.onScrollProgress( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* deltaInPixels= */ 80); + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + requests.add(new HapticFeedbackRequest( + SCROLL_LIMIT, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + + assertThat(mView.mHapticFeedbackRequests).containsExactlyElementsIn(requests).inOrder(); + } @Test public void testScrollLimit_doesNotEnabledWithMotionFromDifferentDeviceId() { @@ -457,9 +944,65 @@ public final class HapticScrollFeedbackProviderTest { assertFeedbackCount(mView, HapticFeedbackConstants.SCROLL_TICK, 2); } + @Test + public void testScrollLimit_inputDeviceCustomized_doesNotEnabledWithMotionFromDifferentDeviceId() { + mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); + List<HapticFeedbackRequest> requests = new ArrayList<>(); + + mProvider.onSnapToItem( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); + requests.add(new HapticFeedbackRequest( + SCROLL_ITEM_FOCUS, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + requests.add(new HapticFeedbackRequest( + SCROLL_LIMIT, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + + mProvider.onSnapToItem( + INPUT_DEVICE_2, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); + requests.add(new HapticFeedbackRequest( + SCROLL_ITEM_FOCUS, INPUT_DEVICE_2, InputDevice.SOURCE_ROTARY_ENCODER)); + // last snap was for input device #2, so limit for input device #1 not re-enabled. + mProvider.onScrollLimit( + INPUT_DEVICE_1, + InputDevice.SOURCE_ROTARY_ENCODER, + MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + + assertThat(mView.mHapticFeedbackRequests).containsExactlyElementsIn(requests).inOrder(); + } + + @Test + public void testScrollLimit_inputDeviceCustomized_doesNotEnabledWithMotionFromDifferentSource() { + mSetFlagsRule.enableFlags(FLAG_HAPTIC_FEEDBACK_INPUT_SOURCE_CUSTOMIZATION_ENABLED); + List<HapticFeedbackRequest> requests = new ArrayList<>(); + + mProvider.onSnapToItem( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL); + requests.add(new HapticFeedbackRequest( + SCROLL_ITEM_FOCUS, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + mProvider.onScrollLimit( + INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER, MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + requests.add(new HapticFeedbackRequest( + SCROLL_LIMIT, INPUT_DEVICE_1, InputDevice.SOURCE_ROTARY_ENCODER)); + mProvider.onSnapToItem( + INPUT_DEVICE_1, InputDevice.SOURCE_TOUCHSCREEN, MotionEvent.AXIS_SCROLL); + requests.add(new HapticFeedbackRequest( + SCROLL_ITEM_FOCUS, INPUT_DEVICE_1, InputDevice.SOURCE_TOUCHSCREEN)); + // last snap was for input source touch screen, so rotary's limit is NOT re-enabled. + mProvider.onScrollLimit( + INPUT_DEVICE_1, + InputDevice.SOURCE_ROTARY_ENCODER, + MotionEvent.AXIS_SCROLL, + /* isStart= */ false); + + assertThat(mView.mHapticFeedbackRequests).containsExactlyElementsIn(requests).inOrder(); + } private void assertNoFeedback(TestView view) { - for (int feedback : new int[] {SCROLL_ITEM_FOCUS, SCROLL_LIMIT, SCROLL_TICK}) { + for (int feedback : new int[]{SCROLL_ITEM_FOCUS, SCROLL_LIMIT, SCROLL_TICK}) { assertFeedbackCount(view, feedback, 0); } } @@ -469,7 +1012,7 @@ public final class HapticScrollFeedbackProviderTest { } private void assertOnlyFeedback(TestView view, int expectedFeedback, int expectedCount) { - for (int feedback : new int[] {SCROLL_ITEM_FOCUS, SCROLL_LIMIT, SCROLL_TICK}) { + for (int feedback : new int[]{SCROLL_ITEM_FOCUS, SCROLL_LIMIT, SCROLL_TICK}) { assertFeedbackCount(view, feedback, (feedback == expectedFeedback) ? expectedCount : 0); } } @@ -489,8 +1032,9 @@ public final class HapticScrollFeedbackProviderTest { .thenReturn(enabled); } - private static class TestView extends View { + private static class TestView extends View { final Map<Integer, Integer> mFeedbackCount = new HashMap<>(); + final List<HapticFeedbackRequest> mHapticFeedbackRequests = new ArrayList<>(); TestView(Context context) { super(context); @@ -504,5 +1048,47 @@ public final class HapticScrollFeedbackProviderTest { mFeedbackCount.put(feedback, mFeedbackCount.get(feedback) + 1); return true; } + + @Override + public void performHapticFeedbackForInputDevice(int feedback, int inputDeviceId, + int inputSource, int flags) { + mHapticFeedbackRequests.add( + new HapticFeedbackRequest(feedback, inputDeviceId, inputSource)); + } + } + + private static class HapticFeedbackRequest { + // <feedback, inputDeviceId, inputSource> + private final int[] mArgs = new int[3]; + + private HapticFeedbackRequest(int feedback, int inputDeviceId, int inputSource) { + mArgs[0] = feedback; + mArgs[1] = inputDeviceId; + mArgs[2] = inputSource; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + HapticFeedbackRequest other = (HapticFeedbackRequest) obj; + return Arrays.equals(this.mArgs, other.mArgs); + } + + @Override + public int hashCode() { + // Shouldn't depend on hash. Should explicitly match mArgs. + return Objects.hash(mArgs[0], mArgs[1], mArgs[2]); + } + + @Override + public String toString() { + return String.format("<feedback=%d; inputDeviceId=%d; inputSource=%d>", + mArgs[0], mArgs[1], mArgs[2]); + } } }
\ No newline at end of file diff --git a/core/tests/coretests/src/android/view/ViewRootImplTest.java b/core/tests/coretests/src/android/view/ViewRootImplTest.java index c5b75ff50da7..b990f2486f9e 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 */ 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/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/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/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/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/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/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/protolog/ShellProtoLogGroup.java b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java index 497c3f704c82..f739d65e63c3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java @@ -61,6 +61,8 @@ public enum ShellProtoLogGroup implements IProtoLogGroup { Consts.TAG_WM_SHELL), WM_SHELL_BUBBLES(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, "Bubbles"), + WM_SHELL_COMPAT_UI(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, + Consts.TAG_WM_COMPAT_UI), TEST_GROUP(true, true, false, "WindowManagerShellProtoLogTest"); private final boolean mEnabled; @@ -128,6 +130,7 @@ public enum ShellProtoLogGroup implements IProtoLogGroup { private static final String TAG_WM_STARTING_WINDOW = "ShellStartingWindow"; private static final String TAG_WM_SPLIT_SCREEN = "ShellSplitScreen"; private static final String TAG_WM_DESKTOP_MODE = "ShellDesktopMode"; + private static final String TAG_WM_COMPAT_UI = "CompatUi"; private static final boolean ENABLE_DEBUG = true; private static final boolean ENABLE_LOG_TO_PROTO_DEBUG = true; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index a7551bddc42d..87dc16a79766 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); }); }); 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/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/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/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/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/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout-v31/non_collapsing_toolbar_base_layout.xml b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout-v31/non_collapsing_toolbar_base_layout.xml new file mode 100644 index 000000000000..1e48443fcf13 --- /dev/null +++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout-v31/non_collapsing_toolbar_base_layout.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<androidx.coordinatorlayout.widget.CoordinatorLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/content_parent" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:fitsSystemWindows="true"> + + <include layout="@layout/non_collapsing_toolbar_content_layout"/> +</androidx.coordinatorlayout.widget.CoordinatorLayout> diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout-v31/non_collapsing_toolbar_content_layout.xml b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout-v31/non_collapsing_toolbar_content_layout.xml new file mode 100644 index 000000000000..33519cba2940 --- /dev/null +++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout-v31/non_collapsing_toolbar_content_layout.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<merge + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + <com.google.android.material.appbar.AppBarLayout + android:id="@+id/app_bar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:fitsSystemWindows="true" + android:outlineAmbientShadowColor="@android:color/transparent" + android:outlineSpotShadowColor="@android:color/transparent" + android:background="@android:color/transparent" + android:theme="@style/Theme.CollapsingToolbar.Settings"> + + <Toolbar + android:id="@+id/action_bar" + android:layout_width="match_parent" + android:layout_height="?attr/actionBarSize" + android:theme="?android:attr/actionBarTheme" + android:transitionName="shared_element_view" + app:layout_collapseMode="pin"/> + </com.google.android.material.appbar.AppBarLayout> + + <FrameLayout + android:id="@+id/content_frame" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:layout_behavior="@string/appbar_scrolling_view_behavior"/> +</merge> diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarAppCompatActivity.java b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarAppCompatActivity.java index 465905170347..f46f110e65b8 100644 --- a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarAppCompatActivity.java +++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarAppCompatActivity.java @@ -171,7 +171,7 @@ public class CollapsingToolbarAppCompatActivity extends AppCompatActivity { private CollapsingToolbarDelegate getToolbarDelegate() { if (mToolbardelegate == null) { - mToolbardelegate = new CollapsingToolbarDelegate(new DelegateCallback()); + mToolbardelegate = new CollapsingToolbarDelegate(new DelegateCallback(), true); } return mToolbardelegate; } diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarBaseActivity.java b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarBaseActivity.java index 3965303d3ba5..16ed5a8079fc 100644 --- a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarBaseActivity.java +++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarBaseActivity.java @@ -169,7 +169,7 @@ public class CollapsingToolbarBaseActivity extends FragmentActivity { private CollapsingToolbarDelegate getToolbarDelegate() { if (mToolbardelegate == null) { - mToolbardelegate = new CollapsingToolbarDelegate(new DelegateCallback()); + mToolbardelegate = new CollapsingToolbarDelegate(new DelegateCallback(), true); } return mToolbardelegate; } diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarBaseFragment.java b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarBaseFragment.java index b605074f72c8..da97c305ea51 100644 --- a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarBaseFragment.java +++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarBaseFragment.java @@ -57,7 +57,8 @@ public abstract class CollapsingToolbarBaseFragment extends Fragment { @Override public void onAttach(Context context) { super.onAttach(context); - mToolbardelegate = new CollapsingToolbarDelegate(new DelegateCallback()); + mToolbardelegate = + new CollapsingToolbarDelegate(new DelegateCallback(), useCollapsingToolbar()); } @Nullable @@ -98,4 +99,8 @@ public abstract class CollapsingToolbarBaseFragment extends Fragment { public FrameLayout getContentFrameLayout() { return mToolbardelegate.getContentFrameLayout(); } + + protected boolean useCollapsingToolbar() { + return true; + } } diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarDelegate.java b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarDelegate.java index b63333719334..2ab2abd03c87 100644 --- a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarDelegate.java +++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarDelegate.java @@ -21,6 +21,8 @@ import static android.text.Layout.HYPHENATION_FREQUENCY_NORMAL_FAST; import android.app.ActionBar; import android.app.Activity; import android.content.res.Configuration; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; import android.graphics.text.LineBreakConfig; import android.os.Build; import android.util.Log; @@ -80,8 +82,12 @@ public class CollapsingToolbarDelegate { @NonNull private final HostCallback mHostCallback; - public CollapsingToolbarDelegate(@NonNull HostCallback hostCallback) { + private boolean mUseCollapsingToolbar; + + public CollapsingToolbarDelegate(@NonNull HostCallback hostCallback, + boolean useCollapsingToolbar) { mHostCallback = hostCallback; + mUseCollapsingToolbar = useCollapsingToolbar; } /** Method to call that creates the root view of the collapsing toolbar. */ @@ -94,13 +100,32 @@ public class CollapsingToolbarDelegate { @SuppressWarnings("RestrictTo") View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, Activity activity) { - final View view = - inflater.inflate(R.layout.collapsing_toolbar_base_layout, container, false); + int layoutId; + boolean useCollapsingToolbar = + mUseCollapsingToolbar || Build.VERSION.SDK_INT < Build.VERSION_CODES.S; + if (useCollapsingToolbar) { + layoutId = R.layout.collapsing_toolbar_base_layout; + } else { + layoutId = R.layout.non_collapsing_toolbar_base_layout; + } + final View view = inflater.inflate(layoutId, container, false); if (view instanceof CoordinatorLayout) { mCoordinatorLayout = (CoordinatorLayout) view; } mCollapsingToolbarLayout = view.findViewById(R.id.collapsing_toolbar); mAppBarLayout = view.findViewById(R.id.app_bar); + + if (!useCollapsingToolbar) { + // In the non-collapsing toolbar layout, we need to set the background of the app bar to + // the same as the activity background so that it covers the items extending above the + // bounds of the list for edge-to-edge. + TypedArray ta = container.getContext().obtainStyledAttributes(new int[] { + android.R.attr.windowBackground}); + Drawable background = ta.getDrawable(0); + ta.recycle(); + mAppBarLayout.setBackground(background); + } + if (mCollapsingToolbarLayout != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { mCollapsingToolbarLayout.setLineSpacingMultiplier(TOOLBAR_LINE_SPACING_MULTIPLIER); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java index 0fec61c5affe..92da2be60d1e 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java @@ -1026,21 +1026,29 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> return mDevice.getBluetoothClass(); } + /** + * Returns a list of {@link LocalBluetoothProfile} supported by the device. + */ public List<LocalBluetoothProfile> getProfiles() { return new ArrayList<>(mProfiles); } - public List<LocalBluetoothProfile> getConnectableProfiles() { - List<LocalBluetoothProfile> connectableProfiles = - new ArrayList<LocalBluetoothProfile>(); + /** + * Returns a list of {@link LocalBluetoothProfile} that are user-accessible from UI to + * initiate a connection. + * + * Note: Use {@link #getProfiles()} to retrieve all supported profiles on the device. + */ + public List<LocalBluetoothProfile> getUiAccessibleProfiles() { + List<LocalBluetoothProfile> accessibleProfiles = new ArrayList<>(); synchronized (mProfileLock) { for (LocalBluetoothProfile profile : mProfiles) { if (profile.accessProfileEnabled()) { - connectableProfiles.add(profile); + accessibleProfiles.add(profile); } } } - return connectableProfiles; + return accessibleProfiles; } public List<LocalBluetoothProfile> getRemovedProfiles() { diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java index a49314aae1b3..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/devicesettings/data/repository/DeviceSettingRepository.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepository.kt index 9ff5c438e32a..326bb31bdb9f 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepository.kt +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepository.kt @@ -19,37 +19,39 @@ package com.android.settingslib.bluetooth.devicesettings.data.repository import android.bluetooth.BluetoothAdapter import android.content.Context import com.android.settingslib.bluetooth.CachedBluetoothDevice +import com.android.settingslib.bluetooth.devicesettings.ActionSwitchPreference import com.android.settingslib.bluetooth.devicesettings.DeviceSetting import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId -import com.android.settingslib.bluetooth.devicesettings.DeviceSettingPreferenceState +import com.android.settingslib.bluetooth.devicesettings.DeviceSettingItem import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsConfig -import java.util.concurrent.ConcurrentHashMap +import com.android.settingslib.bluetooth.devicesettings.MultiTogglePreference +import com.android.settingslib.bluetooth.devicesettings.ToggleInfo +import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel +import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigModel +import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingModel +import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel +import com.android.settingslib.bluetooth.devicesettings.shared.model.ToggleModel +import com.google.common.cache.CacheBuilder +import com.google.common.cache.CacheLoader +import com.google.common.cache.LoadingCache import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch /** Provides functionality to control bluetooth device settings. */ interface DeviceSettingRepository { /** Gets config for the bluetooth device, returns null if failed. */ - suspend fun getDeviceSettingsConfig(cachedDevice: CachedBluetoothDevice): DeviceSettingsConfig? - - /** Gets all device settings for the bluetooth device. */ - fun getDeviceSettingList( - cachedDevice: CachedBluetoothDevice, - ): Flow<List<DeviceSetting>?> + suspend fun getDeviceSettingsConfig( + cachedDevice: CachedBluetoothDevice + ): DeviceSettingConfigModel? /** Gets device setting for the bluetooth device. */ fun getDeviceSetting( cachedDevice: CachedBluetoothDevice, @DeviceSettingId settingId: Int - ): Flow<DeviceSetting?> - - /** Updates device setting for the bluetooth device. */ - suspend fun updateDeviceSettingState( - cachedDevice: CachedBluetoothDevice, - @DeviceSettingId deviceSettingId: Int, - deviceSettingPreferenceState: DeviceSettingPreferenceState, - ) + ): Flow<DeviceSettingModel?> } class DeviceSettingRepositoryImpl( @@ -58,40 +60,94 @@ class DeviceSettingRepositoryImpl( private val coroutineScope: CoroutineScope, private val backgroundCoroutineContext: CoroutineContext, ) : DeviceSettingRepository { - private val deviceSettings = - ConcurrentHashMap<CachedBluetoothDevice, DeviceSettingServiceConnection>() + private val connectionCache: + LoadingCache<CachedBluetoothDevice, DeviceSettingServiceConnection> = + CacheBuilder.newBuilder() + .weakValues() + .build( + object : CacheLoader<CachedBluetoothDevice, DeviceSettingServiceConnection>() { + override fun load( + cachedDevice: CachedBluetoothDevice + ): DeviceSettingServiceConnection = + DeviceSettingServiceConnection( + cachedDevice, + context, + bluetoothAdaptor, + coroutineScope, + backgroundCoroutineContext, + ) + } + ) override suspend fun getDeviceSettingsConfig( cachedDevice: CachedBluetoothDevice - ): DeviceSettingsConfig? = createConnectionIfAbsent(cachedDevice).getDeviceSettingsConfig() - - override fun getDeviceSettingList( - cachedDevice: CachedBluetoothDevice - ): Flow<List<DeviceSetting>?> = createConnectionIfAbsent(cachedDevice).getDeviceSettingList() + ): DeviceSettingConfigModel? = + connectionCache.get(cachedDevice).getDeviceSettingsConfig()?.toModel() override fun getDeviceSetting( cachedDevice: CachedBluetoothDevice, settingId: Int - ): Flow<DeviceSetting?> = createConnectionIfAbsent(cachedDevice).getDeviceSetting(settingId) + ): Flow<DeviceSettingModel?> = + connectionCache.get(cachedDevice).let { connection -> + connection.getDeviceSetting(settingId).map { it?.toModel(cachedDevice, connection) } + } - override suspend fun updateDeviceSettingState( - cachedDevice: CachedBluetoothDevice, - @DeviceSettingId deviceSettingId: Int, - deviceSettingPreferenceState: DeviceSettingPreferenceState, - ) = - createConnectionIfAbsent(cachedDevice) - .updateDeviceSettings(deviceSettingId, deviceSettingPreferenceState) + private fun DeviceSettingsConfig.toModel(): DeviceSettingConfigModel = + DeviceSettingConfigModel( + mainItems = mainContentItems.map { it.toModel() }, + moreSettingsItems = moreSettingsItems.map { it.toModel() }, + moreSettingsPageFooter = moreSettingsFooter + ) - private fun createConnectionIfAbsent( - cachedDevice: CachedBluetoothDevice - ): DeviceSettingServiceConnection = - deviceSettings.computeIfAbsent(cachedDevice) { - DeviceSettingServiceConnection( - cachedDevice, - context, - bluetoothAdaptor, - coroutineScope, - backgroundCoroutineContext, - ) + private fun DeviceSettingItem.toModel(): DeviceSettingConfigItemModel = + DeviceSettingConfigItemModel(settingId) + + private fun DeviceSetting.toModel( + cachedDevice: CachedBluetoothDevice, + connection: DeviceSettingServiceConnection + ): DeviceSettingModel = + when (val pref = preference) { + is ActionSwitchPreference -> + DeviceSettingModel.ActionSwitchPreference( + cachedDevice = cachedDevice, + id = settingId, + title = pref.title, + summary = pref.summary, + icon = pref.icon, + isAllowedChangingState = pref.isAllowedChangingState, + intent = pref.intent, + switchState = + if (pref.hasSwitch()) { + DeviceSettingStateModel.ActionSwitchPreferenceState(pref.checked) + } else { + null + }, + updateState = { newState -> + coroutineScope.launch(backgroundCoroutineContext) { + connection.updateDeviceSettings( + settingId, + newState.toParcelable(), + ) + } + }, + ) + is MultiTogglePreference -> + DeviceSettingModel.MultiTogglePreference( + cachedDevice = cachedDevice, + id = settingId, + title = pref.title, + toggles = pref.toggleInfos.map { it.toModel() }, + isAllowedChangingState = pref.isAllowedChangingState, + isActive = true, + state = DeviceSettingStateModel.MultiTogglePreferenceState(pref.state), + updateState = { newState -> + coroutineScope.launch(backgroundCoroutineContext) { + connection.updateDeviceSettings(settingId, newState.toParcelable()) + } + }, + ) + else -> DeviceSettingModel.Unknown(cachedDevice, settingId) } + + private fun ToggleInfo.toModel(): ToggleModel = ToggleModel(label, icon) } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/shared/model/DeviceSettingConfigModel.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/shared/model/DeviceSettingConfigModel.kt new file mode 100644 index 000000000000..cd597ee65bce --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/shared/model/DeviceSettingConfigModel.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.bluetooth.devicesettings.shared.model + +import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId + +/** Models a device setting config. */ +data class DeviceSettingConfigModel( + /** Items need to be shown in device details main page. */ + val mainItems: List<DeviceSettingConfigItemModel>, + /** Items need to be shown in device details more settings page. */ + val moreSettingsItems: List<DeviceSettingConfigItemModel>, + /** Footer text in more settings page. */ + val moreSettingsPageFooter: String) + +/** Models a device setting item in config. */ +data class DeviceSettingConfigItemModel( + @DeviceSettingId val settingId: Int, +) diff --git a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java index 72a60fbc9fea..fe6659d1dc4f 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java @@ -634,7 +634,7 @@ public class LocalMediaManager implements BluetoothCallback { } private boolean isMediaDevice(CachedBluetoothDevice device) { - for (LocalBluetoothProfile profile : device.getConnectableProfiles()) { + for (LocalBluetoothProfile profile : device.getUiAccessibleProfiles()) { if (profile instanceof A2dpProfile || profile instanceof HearingAidProfile || profile instanceof LeAudioProfile) { return true; diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java index 22e6133a3019..2c982d6a9fe2 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java +++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java @@ -40,13 +40,18 @@ 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")) + public static final ZenMode MANUAL_DND_ACTIVE = ZenMode.manualDndMode( + new AutomaticZenRule.Builder("Do Not Disturb", Uri.parse("rule://dnd")) .setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_PRIORITY) .setZenPolicy(new ZenPolicy.Builder().disallowAllSounds().build()) .build(), - true /* isActive */ - ); + /* isActive= */ true); + public static final ZenMode MANUAL_DND_INACTIVE = ZenMode.manualDndMode( + new AutomaticZenRule.Builder("Do Not Disturb", Uri.parse("rule://dnd")) + .setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().disallowAllSounds().build()) + .build(), + /* isActive= */ false); public TestModeBuilder() { // Reasonable defaults 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/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..e66bacf40576 100644 --- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java +++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java @@ -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/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 3767a27c2e6f..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." @@ -1272,4 +1271,14 @@ flag { namespace: "systemui" description: "Adding haptic component infrastructure to sliders in Compose." bug: "341968766" +} + +flag { + namespace: "systemui" + name: "settings_ext_register_content_observer_on_bg_thread" + description: "Register content observer in callback flow APIs on background thread in SettingsProxyExt." + bug: "355389014" + metadata { + purpose: PURPOSE_BUGFIX + } }
\ No newline at end of file diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterContentObserverSyncViaSettingsProxyDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterContentObserverSyncViaSettingsProxyDetector.kt new file mode 100644 index 000000000000..d8c7c06c8c5b --- /dev/null +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterContentObserverSyncViaSettingsProxyDetector.kt @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.systemui.lint + +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Implementation +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.Scope +import com.android.tools.lint.detector.api.Severity +import com.android.tools.lint.detector.api.SourceCodeScanner +import com.intellij.psi.PsiMethod +import org.jetbrains.uast.UCallExpression + +/** + * Checks if the synchronous APIs like registerContentObserverSync/unregisterContentObserverSync are + * invoked for SettingsProxy or it's sub-classes, and raise a warning notifying the caller to use + * the asynchronous/suspend APIs instead. + */ +@Suppress("UnstableApiUsage") +class RegisterContentObserverSyncViaSettingsProxyDetector : Detector(), SourceCodeScanner { + + override fun getApplicableMethodNames(): List<String> { + return SYNC_METHOD_LIST + } + + override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) { + + val evaluator = context.evaluator + if (evaluator.isMemberInSubClassOf(method, SETTINGS_PROXY_CLASS)) { + context.report( + issue = SYNC_WARNING, + location = context.getNameLocation(node), + message = + "`Avoid using ${method.name}()` if calling the API is not " + + "required on the main thread. Instead use an appropriate async interface " + + "API call for eg. `registerContentObserver()` or " + + "`registerContentObserverAsync()`." + ) + } + } + + companion object { + val SYNC_WARNING: Issue = + Issue.create( + id = "RegisterContentObserverSyncWarning", + briefDescription = + "Synchronous content observer registration API called " + + "instead of the async APIs.`", + // lint trims indents and converts \ to line continuations + explanation = + """ + ContentObserver registration/de-registration done via \ + `SettingsProxy.registerContentObserverSync` will block the main thread \ + and may cause missed frames. Instead, use \ + `SettingsProxy.registerContentObserver()` or \ + `SettingsProxy.registerContentObserverAsync()`. These APIs will ensure \ + that the registrations/de-registrations happen sequentially on a + background worker thread.""", + category = Category.PERFORMANCE, + priority = 8, + severity = Severity.WARNING, + implementation = + Implementation( + RegisterContentObserverSyncViaSettingsProxyDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) + ) + + private val SYNC_METHOD_LIST = + listOf( + "registerContentObserverSync", + "unregisterContentObserverSync", + "registerContentObserverForUserSync" + ) + + private val SETTINGS_PROXY_CLASS = "com.android.systemui.util.settings.SettingsProxy" + } +} diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt index 73ac6ccf8f76..5206b05a3f4e 100644 --- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt @@ -46,10 +46,12 @@ class SystemUIIssueRegistry : IssueRegistry() { DemotingTestWithoutBugDetector.ISSUE, TestFunctionNameViolationDetector.ISSUE, MissingApacheLicenseDetector.ISSUE, + RegisterContentObserverSyncViaSettingsProxyDetector.SYNC_WARNING ) override val api: Int get() = CURRENT_API + override val minApi: Int get() = 8 diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterContentObserverSyncViaSettingsProxyDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterContentObserverSyncViaSettingsProxyDetectorTest.kt new file mode 100644 index 000000000000..57347d351543 --- /dev/null +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterContentObserverSyncViaSettingsProxyDetectorTest.kt @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.internal.systemui.lint + +import com.android.tools.lint.checks.infrastructure.TestFiles +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Issue +import org.junit.Test + +/** Test class for [RegisterContentObserverSyncViaSettingsProxyDetector]. */ +class RegisterContentObserverSyncViaSettingsProxyDetectorTest : SystemUILintDetectorTest() { + override fun getDetector(): Detector = RegisterContentObserverSyncViaSettingsProxyDetector() + + override fun getIssues(): List<Issue> = + listOf(RegisterContentObserverSyncViaSettingsProxyDetector.SYNC_WARNING) + + @Test + fun testRegisterContentObserverSync_throwError() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import com.android.systemui.util.settings.SecureSettings; + public class TestClass { + public void register(SecureSettings secureSettings) { + secureSettings. + registerContentObserverSync(Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED), + false, mSettingObserver); + } + } + """ + ) + .indented(), + *stubs + ) + .issues(RegisterContentObserverSyncViaSettingsProxyDetector.SYNC_WARNING) + .run() + .expect( + """ + src/test/pkg/TestClass.java:6: Warning: Avoid using registerContentObserverSync() if calling the API is not required on the main thread. Instead use an appropriate async interface API call for eg. registerContentObserver() or registerContentObserverAsync(). [RegisterContentObserverSyncWarning] + registerContentObserverSync(Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~ +0 errors, 1 warnings + """ + .trimIndent() + ) + } + + @Test + fun testRegisterContentObserverForUserSync_throwError() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import com.android.systemui.util.settings.SecureSettings; + public class TestClass { + public void register(SecureSettings secureSettings) { + secureSettings. + registerContentObserverForUserSync(Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED), + false, mSettingObserver); + } + } + """ + ) + .indented(), + *stubs + ) + .issues(RegisterContentObserverSyncViaSettingsProxyDetector.SYNC_WARNING) + .run() + .expect( + """ + src/test/pkg/TestClass.java:6: Warning: Avoid using registerContentObserverForUserSync() if calling the API is not required on the main thread. Instead use an appropriate async interface API call for eg. registerContentObserver() or registerContentObserverAsync(). [RegisterContentObserverSyncWarning] + registerContentObserverForUserSync(Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +0 errors, 1 warnings + """ + .trimIndent() + ) + } + + @Test + fun testSuppressRegisterContentObserverSync() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import com.android.systemui.util.settings.SecureSettings; + public class TestClass { + @SuppressWarnings("RegisterContentObserverSyncWarning") + public void register(SecureSettings secureSettings) { + secureSettings. + registerContentObserverForUserSync(Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED), + false, mSettingObserver); + } + } + """ + ) + .indented(), + *stubs + ) + .issues(RegisterContentObserverSyncViaSettingsProxyDetector.SYNC_WARNING) + .run() + .expectClean() + } + + @Test + fun testNoopIfNoCall() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import com.android.systemui.util.settings.SecureSettings; + public class TestClass { + public void register(SecureSettings secureSettings) { + } + } + """ + ) + .indented(), + *stubs + ) + .issues(RegisterContentObserverSyncViaSettingsProxyDetector.SYNC_WARNING) + .run() + .expectClean() + } + + @Test + fun testUnRegisterContentObserverSync_throwError() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import com.android.systemui.util.settings.SecureSettings; + public class TestClass { + public void register(SecureSettings secureSettings) { + secureSettings. + unregisterContentObserverSync(mSettingObserver); + } + } + """ + ) + .indented(), + *stubs + ) + .issues(RegisterContentObserverSyncViaSettingsProxyDetector.SYNC_WARNING) + .run() + .expect( + """ + src/test/pkg/TestClass.java:6: Warning: Avoid using unregisterContentObserverSync() if calling the API is not required on the main thread. Instead use an appropriate async interface API call for eg. registerContentObserver() or registerContentObserverAsync(). [RegisterContentObserverSyncWarning] + unregisterContentObserverSync(mSettingObserver); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +0 errors, 1 warnings + """ + .trimIndent() + ) + } + + private companion object { + private val SETTINGS_PROXY_STUB = + kotlin( + """ + package com.android.systemui.util.settings + interface SettingsProxy { + fun registerContentObserverSync() {} + fun unregisterContentObserverSync() {} + } + """ + ) + .indented() + + private val USER_SETTINGS_PROXY_STUB = + kotlin( + """ + package com.android.systemui.util.settings + interface UserSettingsProxy : SettingsProxy { + fun registerContentObserverForUserSync() {} + } + """ + ) + .indented() + + private val SECURE_SETTINGS_STUB = + kotlin( + """ + package com.android.systemui.util.settings + interface SecureSettings : UserSettingsProxy {} + """ + ) + .indented() + } + + private val stubs = arrayOf(SETTINGS_PROXY_STUB, USER_SETTINGS_PROXY_STUB, SECURE_SETTINGS_STUB) +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt index 69f117431663..b65b47123eaa 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt @@ -160,6 +160,7 @@ import com.android.compose.modifiers.thenIf import com.android.compose.theme.LocalAndroidColorScheme import com.android.compose.ui.graphics.painter.rememberDrawablePainter import com.android.internal.R.dimen.system_app_widget_background_radius +import com.android.systemui.Flags import com.android.systemui.Flags.communalTimerFlickerFix import com.android.systemui.communal.domain.model.CommunalContentModel import com.android.systemui.communal.shared.model.CommunalContentSize @@ -269,7 +270,7 @@ fun CommunalHub( } } // Nested scroll for full screen swipe to get to shade and bouncer - .thenIf(!viewModel.isEditMode) { + .thenIf(!viewModel.isEditMode && Flags.hubmodeFullscreenVerticalSwipeFix()) { Modifier.nestedScroll(nestedScrollConnection).pointerInput(viewModel) { awaitPointerEventScope { while (true) { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt index f0f407a52243..4e117d6ff4db 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt @@ -23,19 +23,16 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.android.compose.animation.scene.SceneScope import com.android.compose.modifiers.thenIf -import com.android.systemui.Flags import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.ui.composable.modifier.burnInAware import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters import com.android.systemui.notifications.ui.composable.ConstrainedNotificationStack -import com.android.systemui.res.R import com.android.systemui.shade.LargeScreenHeaderHelper import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView @@ -97,11 +94,7 @@ constructor( } val splitShadeTopMargin: Dp = - if (Flags.centralizedStatusBarHeightFix()) { - LargeScreenHeaderHelper.getLargeScreenHeaderHeight(LocalContext.current).dp - } else { - dimensionResource(id = R.dimen.large_screen_shade_header_height) - } + LargeScreenHeaderHelper.getLargeScreenHeaderHeight(LocalContext.current).dp ConstrainedNotificationStack( stackScrollView = stackScrollView.get(), diff --git a/packages/SystemUI/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/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..dc225a399250 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,24 +14,10 @@ * 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 com.android.systemui.keyguard.shared.model.DozeStateModel.Companion.isDozeOff +import com.android.systemui.coroutines.collectLastValue import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -42,24 +28,28 @@ import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepos import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.data.repository.keyguardOcclusionRepository +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor 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 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 +import com.google.common.truth.Truth.assertThat @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @@ -79,21 +69,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 +121,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 +153,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..90e13a57cefe 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) @@ -757,12 +760,8 @@ class KeyguardTransitionScenariosTest(flags: FlagsParameterization?) : SysuiTest @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 +1129,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 +1146,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 +2184,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 +2238,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/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/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt index 9fea7a2bfbf6..2fb9e1e038c8 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 @@ -172,7 +172,7 @@ 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) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt index 32f66c1ccd66..11504aadf743 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt @@ -172,7 +172,7 @@ class ZenModeInteractorTest : SysuiTestCase() { @Test fun shouldAskForZenDuration_changesWithSetting() = testScope.runTest { - val manualDnd = TestModeBuilder.MANUAL_DND + val manualDnd = TestModeBuilder.MANUAL_DND_ACTIVE settingsRepository.setInt(ZEN_DURATION, ZEN_DURATION_FOREVER) runCurrent() @@ -201,7 +201,7 @@ class ZenModeInteractorTest : SysuiTestCase() { @Test fun activateMode_usesCorrectDuration() = testScope.runTest { - val manualDnd = TestModeBuilder.MANUAL_DND + val manualDnd = TestModeBuilder.MANUAL_DND_ACTIVE zenModeRepository.addModes(listOf(manualDnd)) settingsRepository.setInt(ZEN_DURATION, ZEN_DURATION_FOREVER) runCurrent() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt index 62161bfeffb3..bcad7e7bc31c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt @@ -69,7 +69,7 @@ class ModesDialogViewModelTest : SysuiTestCase() { .setName("Disabled by other") .setEnabled(false, /* byUser= */ false) .build(), - TestModeBuilder.MANUAL_DND, + TestModeBuilder.MANUAL_DND_ACTIVE, TestModeBuilder() .setName("Enabled") .setEnabled(true) @@ -91,7 +91,7 @@ class ModesDialogViewModelTest : SysuiTestCase() { assertThat(this.enabled).isEqualTo(false) } with(tiles?.elementAt(1)!!) { - assertThat(this.text).isEqualTo("Manual DND") + assertThat(this.text).isEqualTo("Do Not Disturb") assertThat(this.subtext).isEqualTo("On") assertThat(this.enabled).isEqualTo(true) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/settings/SettingsProxyExtTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/settings/SettingsProxyExtTest.kt new file mode 100644 index 000000000000..e281894a93ab --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/settings/SettingsProxyExtTest.kt @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.util.settings + +import android.database.ContentObserver +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.Flags +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.android.systemui.util.settings.SettingsProxyExt.observerFlow +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.any +import org.mockito.kotlin.times +import org.mockito.kotlin.verify + +/** Tests for [SettingsProxyExt]. */ +@RunWith(AndroidJUnit4::class) +@SmallTest +@OptIn(ExperimentalCoroutinesApi::class) +class SettingsProxyExtTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + @Mock lateinit var settingsProxy: SettingsProxy + @Mock lateinit var userSettingsProxy: UserSettingsProxy + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + } + + @Test + @EnableFlags(Flags.FLAG_SETTINGS_EXT_REGISTER_CONTENT_OBSERVER_ON_BG_THREAD) + fun observeFlow_bgFlagEnabled_settingsProxy_registerContentObserverInvoked() = + testScope.runTest { + val unused by collectLastValue(settingsProxy.observerFlow(SETTING_1, SETTING_2)) + runCurrent() + verify(settingsProxy, times(2)) + .registerContentObserver(any<String>(), any<ContentObserver>()) + } + + @Test + @DisableFlags(Flags.FLAG_SETTINGS_EXT_REGISTER_CONTENT_OBSERVER_ON_BG_THREAD) + fun observeFlow_bgFlagDisabled_multipleSettings_SettingsProxy_registerContentObserverInvoked() = + testScope.runTest { + val unused by collectLastValue(settingsProxy.observerFlow(SETTING_1, SETTING_2)) + runCurrent() + verify(settingsProxy, times(2)) + .registerContentObserverSync(any<String>(), any<ContentObserver>()) + } + + @Test + @EnableFlags(Flags.FLAG_SETTINGS_EXT_REGISTER_CONTENT_OBSERVER_ON_BG_THREAD) + fun observeFlow_bgFlagEnabled_channelClosed_settingsProxy_unregisterContentObserverInvoked() = + testScope.runTest { + val job = Job() + val unused by + collectLastValue(settingsProxy.observerFlow(SETTING_1, SETTING_2), context = job) + runCurrent() + job.cancel() + runCurrent() + verify(settingsProxy).unregisterContentObserverAsync(any<ContentObserver>()) + } + + @Test + @DisableFlags(Flags.FLAG_SETTINGS_EXT_REGISTER_CONTENT_OBSERVER_ON_BG_THREAD) + fun observeFlow_bgFlagDisabled_channelClosed_settingsProxy_unregisterContentObserverInvoked() = + testScope.runTest { + val job = Job() + val unused by + collectLastValue(settingsProxy.observerFlow(SETTING_1, SETTING_2), context = job) + runCurrent() + job.cancel() + runCurrent() + verify(settingsProxy).unregisterContentObserverSync(any<ContentObserver>()) + } + + @Test + @EnableFlags(Flags.FLAG_SETTINGS_EXT_REGISTER_CONTENT_OBSERVER_ON_BG_THREAD) + fun observeFlow_bgFlagEnabled_userSettingsProxy_registerContentObserverForUserInvoked() = + testScope.runTest { + val unused by + collectLastValue(userSettingsProxy.observerFlow(userId = 0, SETTING_1, SETTING_2)) + runCurrent() + verify(userSettingsProxy, times(2)) + .registerContentObserverForUser(any<String>(), any<ContentObserver>(), any<Int>()) + } + + @Test + @DisableFlags(Flags.FLAG_SETTINGS_EXT_REGISTER_CONTENT_OBSERVER_ON_BG_THREAD) + fun observeFlow_bgFlagDisabled_userSettingsProxy_registerContentObserverForUserInvoked() = + testScope.runTest { + val unused by + collectLastValue(userSettingsProxy.observerFlow(userId = 0, SETTING_1, SETTING_2)) + runCurrent() + verify(userSettingsProxy, times(2)) + .registerContentObserverForUserSync( + any<String>(), + any<ContentObserver>(), + any<Int>() + ) + } + + @Test + @EnableFlags(Flags.FLAG_SETTINGS_EXT_REGISTER_CONTENT_OBSERVER_ON_BG_THREAD) + fun observeFlow_bgFlagEnabled_channelClosed_userSettingsProxy_unregisterContentObserverInvoked() = + testScope.runTest { + val job = Job() + val unused by + collectLastValue( + userSettingsProxy.observerFlow(userId = 0, SETTING_1, SETTING_2), + context = job + ) + runCurrent() + job.cancel() + runCurrent() + verify(userSettingsProxy).unregisterContentObserverAsync(any<ContentObserver>()) + } + + @Test + @DisableFlags(Flags.FLAG_SETTINGS_EXT_REGISTER_CONTENT_OBSERVER_ON_BG_THREAD) + fun observeFlow_bgFlagDisabled_channelClosed_userSettingsProxy_unregisterContentObserverInvoked() = + testScope.runTest { + val job = Job() + val unused by + collectLastValue( + userSettingsProxy.observerFlow(userId = 0, SETTING_1, SETTING_2), + context = job + ) + runCurrent() + job.cancel() + runCurrent() + verify(userSettingsProxy).unregisterContentObserverSync(any<ContentObserver>()) + } + + private companion object { + val SETTING_1 = "settings_1" + val SETTING_2 = "settings_2" + } +} diff --git a/packages/SystemUI/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..aae6d9317432 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -3687,17 +3687,25 @@ <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 gesture was done successfully [CHAR LIMIT=NONE] --> + <string name="touchpad_tutorial_gesture_done">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> + <!-- HOME GESTURE --> + <!-- Touchpad home gesture action name in tutorial [CHAR LIMIT=NONE] --> + <string name="touchpad_home_gesture_action_title">Go home</string> + <!-- Touchpad home gesture guidance in gestures tutorial [CHAR LIMIT=NONE] --> + <string name="touchpad_home_gesture_guidance">To go to your home screen at any time, swipe up with three fingers from the bottom of your screen.</string> + <!-- Screen title after home gesture was done successfully [CHAR LIMIT=NONE] --> + <string name="touchpad_home_gesture_done">Nice!</string> + <!-- Text shown to the user after they complete home gesture tutorial [CHAR LIMIT=NONE] --> + <string name="touchpad_home_gesture_finished">You completed the go home gesture.</string> <!-- Content description for keyboard backlight brightness dialog [CHAR LIMIT=NONE] --> <string name="keyboard_backlight_dialog_title">Keyboard backlight</string> diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/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/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/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/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/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 1d1ac5ab21a3..562ba369f47a 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt @@ -34,8 +34,6 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.KeyguardBottomAreaRefactor import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.shared.ComposeLockscreen -import com.android.systemui.qs.flags.NewQsUI -import com.android.systemui.qs.flags.QSComposeFragment import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.shared.flag.DualShade import com.android.systemui.statusbar.notification.collection.SortBySectionTimeFlag @@ -76,9 +74,6 @@ class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, ha // DualShade dependencies DualShade.token dependsOn SceneContainerFlag.getMainAconfigFlag() - // QS Fragment using Compose dependencies - QSComposeFragment.token dependsOn NewQsUI.token - // Status bar chip dependencies statusBarCallChipNotificationIconToken dependsOn statusBarUseReposForCallChipToken statusBarCallChipNotificationIconToken dependsOn statusBarScreenSharingChipsToken diff --git a/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt b/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt index 4652b2a30eb4..e50c05c7f6ed 100644 --- a/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt +++ b/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt @@ -107,7 +107,11 @@ constructor( fun handleActionDown() { logEvent(qsTile?.tileSpec, state, "action down received") when (state) { - State.IDLE -> { + State.IDLE, + // ACTION_DOWN typically only happens in State.IDLE but including CLICKED and + // LONG_CLICKED just to be safe`b + State.CLICKED, + State.LONG_CLICKED -> { setState(State.TIMEOUT_WAIT) } State.RUNNING_BACKWARDS_FROM_UP, diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/oobe/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/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/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt index 42490c4176c6..69e10d9a6009 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 @@ -14,7 +14,6 @@ * limitations under the License. * */ - @file:OptIn(ExperimentalCoroutinesApi::class) package com.android.systemui.keyguard.domain.interactor @@ -156,14 +155,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 @@ -179,12 +187,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(), diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt index e132eb7ec32a..b89eb2723fab 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt @@ -98,6 +98,16 @@ constructor( } scope.launch { + keyguardInteractor.isDreaming.collect { logger.log(TAG, VERBOSE, "isDreaming", it) } + } + + scope.launch { + keyguardInteractor.isDreamingWithOverlay.collect { + logger.log(TAG, VERBOSE, "isDreamingWithOverlay", it) + } + } + + scope.launch { keyguardInteractor.isAbleToDream.collect { logger.log(TAG, VERBOSE, "isAbleToDream", it) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/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/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/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/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/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/screenrecord/RecordingService.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java index cbb61b37b7a4..117035422c51 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java @@ -181,7 +181,7 @@ public class RecordingService extends Service implements ScreenMediaRecorderList mUiEventLogger.log(Events.ScreenRecordEvent.SCREEN_RECORD_START); } else { updateState(false); - createErrorNotification(); + createErrorStartingNotification(); stopForeground(STOP_FOREGROUND_DETACH); stopSelf(); return Service.START_NOT_STICKY; @@ -272,17 +272,30 @@ 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() { + createErrorNotification(strings().getStartError()); + } + + /** + * Simple "error saving" notification, needed since startForeground must be called to avoid + * errors. + */ + @VisibleForTesting + protected void createErrorSavingNotification() { + createErrorNotification(strings().getSaveError()); + } + + private void createErrorNotification(String notificationContentTitle) { 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) .addExtras(extras); startForeground(mNotificationId, builder.build()); } @@ -427,11 +440,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(); } catch (Throwable throwable) { if (getRecorder() != null) { // Something unexpected happen, SystemUI will crash but let's delete 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/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt index 05c50fe18c8b..15bbef02196a 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt @@ -293,7 +293,7 @@ constructor( ) containerView.systemGestureExclusionRects = - if (Flags.hubmodeFullscreenVerticalSwipe()) { + if (Flags.hubmodeFullscreenVerticalSwipeFix()) { listOf( // Disable back gestures on the left side of the screen, to avoid // conflicting with scene transitions. diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt index 8b88da1754f0..348b6bab1617 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt @@ -27,7 +27,6 @@ import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID import androidx.constraintlayout.widget.ConstraintSet.START import androidx.constraintlayout.widget.ConstraintSet.TOP import androidx.lifecycle.lifecycleScope -import com.android.systemui.Flags.centralizedStatusBarHeightFix import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.fragments.FragmentService @@ -191,11 +190,7 @@ constructor( } private fun calculateLargeShadeHeaderHeight(): Int { - return if (centralizedStatusBarHeightFix()) { - largeScreenHeaderHelperLazy.get().getLargeScreenHeaderHeight() - } else { - resources.getDimensionPixelSize(R.dimen.large_screen_shade_header_height) - } + return largeScreenHeaderHelperLazy.get().getLargeScreenHeaderHeight() } private fun calculateShadeHeaderHeight(): Int { diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java index 9f61d4e5d949..0a092a088e69 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); 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/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/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/stack/domain/interactor/SharedNotificationContainerInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt index 9b21fa9bbe35..2537affbb28b 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,7 +18,6 @@ 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 @@ -75,11 +74,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/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index 2c7ce00adbeb..b6d58d6a23d9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -2269,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; 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/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..51b14c295275 --- /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_done, + bodySuccessResId = R.string.touchpad_home_gesture_finished + ), + 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/settings/SettingsProxyExt.kt b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxyExt.kt index d757e33fcc29..364681444c1b 100644 --- a/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxyExt.kt +++ b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxyExt.kt @@ -19,6 +19,7 @@ package com.android.systemui.util.settings import android.annotation.UserIdInt import android.database.ContentObserver +import com.android.systemui.Flags import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow @@ -39,9 +40,21 @@ object SettingsProxyExt { } } - names.forEach { name -> registerContentObserverForUserSync(name, observer, userId) } + names.forEach { name -> + if (Flags.settingsExtRegisterContentObserverOnBgThread()) { + registerContentObserverForUser(name, observer, userId) + } else { + registerContentObserverForUserSync(name, observer, userId) + } + } - awaitClose { unregisterContentObserverSync(observer) } + awaitClose { + if (Flags.settingsExtRegisterContentObserverOnBgThread()) { + unregisterContentObserverAsync(observer) + } else { + unregisterContentObserverSync(observer) + } + } } } @@ -57,9 +70,21 @@ object SettingsProxyExt { } } - names.forEach { name -> registerContentObserverSync(name, observer) } + names.forEach { name -> + if (Flags.settingsExtRegisterContentObserverOnBgThread()) { + registerContentObserver(name, observer) + } else { + registerContentObserverSync(name, observer) + } + } - awaitClose { unregisterContentObserverSync(observer) } + awaitClose { + if (Flags.settingsExtRegisterContentObserverOnBgThread()) { + unregisterContentObserverAsync(observer) + } else { + unregisterContentObserverSync(observer) + } + } } } } diff --git a/packages/SystemUI/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/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt index 82465065c1e1..74bc9282eebb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt @@ -208,8 +208,8 @@ class DeviceItemActionInteractorTest : SysuiTestCase() { with(kosmos) { testScope.runTest { whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS) - whenever(cachedBluetoothDevice.connectableProfiles) - .thenReturn(listOf(leAudioProfile)) + whenever(cachedBluetoothDevice.uiAccessibleProfiles) + .thenReturn(listOf(leAudioProfile)) whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true) whenever(localBluetoothManager.profileManager).thenReturn(profileManager) @@ -243,8 +243,8 @@ class DeviceItemActionInteractorTest : SysuiTestCase() { testScope.runTest { whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS) whenever(cachedBluetoothDevice.device).thenReturn(bluetoothDevice) - whenever(cachedBluetoothDevice.connectableProfiles) - .thenReturn(listOf(leAudioProfile)) + whenever(cachedBluetoothDevice.uiAccessibleProfiles) + .thenReturn(listOf(leAudioProfile)) whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true) whenever(localBluetoothManager.profileManager).thenReturn(profileManager) @@ -254,12 +254,12 @@ class DeviceItemActionInteractorTest : SysuiTestCase() { whenever(BluetoothUtils.isBroadcasting(ArgumentMatchers.any())).thenReturn(true) whenever( - BluetoothUtils.hasConnectedBroadcastSource( - ArgumentMatchers.any(), - ArgumentMatchers.any() + BluetoothUtils.hasConnectedBroadcastSource( + ArgumentMatchers.any(), + ArgumentMatchers.any() + ) ) - ) - .thenReturn(false) + .thenReturn(false) actionInteractorImpl.onClick(connectedMediaDeviceItem, dialog) verify(activityStarter) diff --git a/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.kt index 20cb1e129f49..0ac04b61f13d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.kt @@ -28,6 +28,7 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.FlowValue import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.coroutines.collectValues import com.android.systemui.util.mockito.kotlinArgumentCaptor import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever @@ -456,8 +457,20 @@ class DisplayRepositoryTest : SysuiTestCase() { assertThat(value?.ids()).containsExactly(DEFAULT_DISPLAY) } + @Test + fun displayFlow_emitsCorrectDisplaysAtFirst() = + testScope.runTest { + setDisplays(0, 1, 2) + + val values: List<Set<Display>> by collectValues(displayRepository.displays) + + assertThat(values.toIdSets()).containsExactly(setOf(0, 1, 2)) + } + private fun Iterable<Display>.ids(): List<Int> = map { it.displayId } + private fun Iterable<Set<Display>>.toIdSets(): List<Set<Int>> = map { it.ids().toSet() } + // Wrapper to capture the displayListener. private fun TestScope.latestDisplayFlowValue(): FlowValue<Set<Display>?> { val flowValue = collectLastValue(displayRepository.displays) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt index e44bc7b43fb1..313292a5fab8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt @@ -102,34 +102,6 @@ class AlternateBouncerViewModelTest : SysuiTestCase() { } @Test - fun forcePluginOpen() = - testScope.runTest { - val forcePluginOpen by collectLastValue(underTest.forcePluginOpen) - - transitionRepository.sendTransitionSteps( - listOf( - stepToAlternateBouncer(0f, TransitionState.STARTED), - stepToAlternateBouncer(.4f), - stepToAlternateBouncer(.6f), - stepToAlternateBouncer(1f), - ), - testScope, - ) - assertThat(forcePluginOpen).isTrue() - - transitionRepository.sendTransitionSteps( - listOf( - stepFromAlternateBouncer(0f, TransitionState.STARTED), - stepFromAlternateBouncer(.3f), - stepFromAlternateBouncer(.6f), - stepFromAlternateBouncer(1f), - ), - testScope, - ) - assertThat(forcePluginOpen).isFalse() - } - - @Test fun registerForDismissGestures() = testScope.runTest { val registerForDismissGestures by collectLastValue(underTest.registerForDismissGestures) diff --git a/packages/SystemUI/tests/src/com/android/systemui/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/screenrecord/RecordingServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java index f866f740345e..79c206c1a838 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java @@ -126,7 +126,8 @@ public class RecordingServiceTest extends SysuiTestCase { doNothing().when(mRecordingService).createRecordingNotification(); doReturn(mNotification).when(mRecordingService).createProcessingNotification(); doReturn(mNotification).when(mRecordingService).createSaveNotification(any()); - doNothing().when(mRecordingService).createErrorNotification(); + doNothing().when(mRecordingService).createErrorStartingNotification(); + doNothing().when(mRecordingService).createErrorSavingNotification(); doNothing().when(mRecordingService).showErrorToast(anyInt()); doNothing().when(mRecordingService).stopForeground(anyInt()); @@ -234,7 +235,7 @@ public class RecordingServiceTest extends SysuiTestCase { mRecordingService.onStopped(); - verify(mRecordingService).createErrorNotification(); + verify(mRecordingService).createErrorSavingNotification(); } @Test 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/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/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/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/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..a1c2f79536c4 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 @@ -216,6 +216,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/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/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/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/input/InputManagerInternal.java b/services/core/java/com/android/server/input/InputManagerInternal.java index d32a5ed60094..819b9a166daa 100644 --- a/services/core/java/com/android/server/input/InputManagerInternal.java +++ b/services/core/java/com/android/server/input/InputManagerInternal.java @@ -21,6 +21,7 @@ import android.annotation.Nullable; import android.annotation.UserIdInt; import android.graphics.PointF; import android.hardware.display.DisplayViewport; +import android.hardware.input.KeyboardSystemShortcut; import android.os.IBinder; import android.view.InputChannel; import android.view.inputmethod.InputMethodSubtype; @@ -227,4 +228,20 @@ public abstract class InputManagerInternal { * since boot. */ public abstract int getLastUsedInputDeviceId(); + + /** + * Notify Keyboard system shortcut was triggered by the user and handled by the framework. + * + * NOTE: This is just to notify that a system shortcut was triggered. No further action is + * required to execute the said shortcut. This callback is meant for purposes of providing user + * hints or logging, etc. + * + * @param deviceId the device ID of the keyboard using which the shortcut was triggered + * @param keycodes the keys pressed for triggering the shortcut + * @param modifierState the modifier state of the key event that triggered the shortcut + * @param shortcut the shortcut that was triggered + * + */ + public abstract void notifyKeyboardShortcutTriggered(int deviceId, int[] keycodes, + int modifierState, @KeyboardSystemShortcut.SystemShortcut int shortcut); } diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index a06ad145100d..e555761e34e1 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -47,6 +47,7 @@ import android.hardware.input.IInputDevicesChangedListener; import android.hardware.input.IInputManager; import android.hardware.input.IInputSensorEventListener; import android.hardware.input.IKeyboardBacklightListener; +import android.hardware.input.IKeyboardSystemShortcutListener; import android.hardware.input.IStickyModifierStateListener; import android.hardware.input.ITabletModeChangedListener; import android.hardware.input.InputDeviceIdentifier; @@ -56,6 +57,7 @@ import android.hardware.input.InputSettings; import android.hardware.input.KeyGlyphMap; import android.hardware.input.KeyboardLayout; import android.hardware.input.KeyboardLayoutSelectionResult; +import android.hardware.input.KeyboardSystemShortcut; import android.hardware.input.TouchCalibration; import android.hardware.lights.Light; import android.hardware.lights.LightState; @@ -157,6 +159,7 @@ public class InputManagerService extends IInputManager.Stub private static final int MSG_DELIVER_INPUT_DEVICES_CHANGED = 1; private static final int MSG_RELOAD_DEVICE_ALIASES = 2; private static final int MSG_DELIVER_TABLET_MODE_CHANGED = 3; + private static final int MSG_KEYBOARD_SYSTEM_SHORTCUT_TRIGGERED = 4; private static final int DEFAULT_VIBRATION_MAGNITUDE = 192; private static final AdditionalDisplayInputProperties @@ -306,6 +309,9 @@ public class InputManagerService extends IInputManager.Stub // Manages Sticky modifier state private final StickyModifierStateController mStickyModifierStateController; + // Manages keyboard system shortcut callbacks + private final KeyboardShortcutCallbackHandler mKeyboardShortcutCallbackHandler; + // Manages Keyboard microphone mute led private final KeyboardLedController mKeyboardLedController; @@ -461,6 +467,7 @@ public class InputManagerService extends IInputManager.Stub injector.getLooper(), injector.getUEventManager()) : new KeyboardBacklightControllerInterface() {}; mStickyModifierStateController = new StickyModifierStateController(); + mKeyboardShortcutCallbackHandler = new KeyboardShortcutCallbackHandler(); mKeyboardLedController = new KeyboardLedController(mContext, injector.getLooper(), mNative); mKeyRemapper = new KeyRemapper(mContext, mNative, mDataStore, injector.getLooper()); @@ -2703,6 +2710,36 @@ public class InputManagerService extends IInputManager.Stub lockedModifierState); } + @Override + @EnforcePermission(Manifest.permission.MONITOR_KEYBOARD_SYSTEM_SHORTCUTS) + public void registerKeyboardSystemShortcutListener( + @NonNull IKeyboardSystemShortcutListener listener) { + super.registerKeyboardSystemShortcutListener_enforcePermission(); + Objects.requireNonNull(listener); + mKeyboardShortcutCallbackHandler.registerKeyboardSystemShortcutListener(listener, + Binder.getCallingPid()); + } + + @Override + @EnforcePermission(Manifest.permission.MONITOR_KEYBOARD_SYSTEM_SHORTCUTS) + public void unregisterKeyboardSystemShortcutListener( + @NonNull IKeyboardSystemShortcutListener listener) { + super.unregisterKeyboardSystemShortcutListener_enforcePermission(); + Objects.requireNonNull(listener); + mKeyboardShortcutCallbackHandler.unregisterKeyboardSystemShortcutListener(listener, + Binder.getCallingPid()); + } + + private void handleKeyboardSystemShortcutTriggered(int deviceId, + KeyboardSystemShortcut shortcut) { + InputDevice device = getInputDevice(deviceId); + if (device == null || device.isVirtual() || !device.isFullKeyboard()) { + return; + } + KeyboardMetricsCollector.logKeyboardSystemsEventReportedAtom(device, shortcut); + mKeyboardShortcutCallbackHandler.onKeyboardSystemShortcutTriggered(deviceId, shortcut); + } + /** * Callback interface implemented by the Window Manager. */ @@ -2871,6 +2908,10 @@ public class InputManagerService extends IInputManager.Stub boolean inTabletMode = (boolean) args.arg1; deliverTabletModeChanged(whenNanos, inTabletMode); break; + case MSG_KEYBOARD_SYSTEM_SHORTCUT_TRIGGERED: + int deviceId = msg.arg1; + KeyboardSystemShortcut shortcut = (KeyboardSystemShortcut) msg.obj; + handleKeyboardSystemShortcutTriggered(deviceId, shortcut); } } } @@ -3196,6 +3237,13 @@ public class InputManagerService extends IInputManager.Stub public int getLastUsedInputDeviceId() { return mNative.getLastUsedInputDeviceId(); } + + @Override + public void notifyKeyboardShortcutTriggered(int deviceId, int[] keycodes, int modifierState, + @KeyboardSystemShortcut.SystemShortcut int shortcut) { + mHandler.obtainMessage(MSG_KEYBOARD_SYSTEM_SHORTCUT_TRIGGERED, deviceId, 0, + new KeyboardSystemShortcut(keycodes, modifierState, shortcut)).sendToTarget(); + } } @Override diff --git a/services/core/java/com/android/server/input/KeyboardMetricsCollector.java b/services/core/java/com/android/server/input/KeyboardMetricsCollector.java index f21fd4132f0f..3d2f95105e76 100644 --- a/services/core/java/com/android/server/input/KeyboardMetricsCollector.java +++ b/services/core/java/com/android/server/input/KeyboardMetricsCollector.java @@ -24,31 +24,25 @@ import static android.hardware.input.KeyboardLayoutSelectionResult.layoutSelecti import android.annotation.NonNull; import android.annotation.Nullable; -import android.app.role.RoleManager; -import android.content.Intent; import android.hardware.input.KeyboardLayout; import android.hardware.input.KeyboardLayoutSelectionResult.LayoutSelectionCriteria; +import android.hardware.input.KeyboardSystemShortcut; import android.icu.util.ULocale; import android.text.TextUtils; import android.util.Log; import android.util.Slog; -import android.util.SparseArray; import android.util.proto.ProtoOutputStream; import android.view.InputDevice; -import android.view.KeyEvent; import android.view.inputmethod.InputMethodSubtype; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.KeyboardConfiguredProto.KeyboardLayoutConfig; import com.android.internal.os.KeyboardConfiguredProto.RepeatedKeyboardLayoutConfig; import com.android.internal.util.FrameworkStatsLog; -import com.android.server.policy.ModifierShortcutManager; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Objects; -import java.util.Set; /** * Collect Keyboard metrics @@ -66,336 +60,20 @@ public final class KeyboardMetricsCollector { @VisibleForTesting public static final String DEFAULT_LANGUAGE_TAG = "None"; - public enum KeyboardLogEvent { - UNSPECIFIED( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__UNSPECIFIED, - "INVALID_KEYBOARD_EVENT"), - HOME(FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__HOME, - "HOME"), - RECENT_APPS( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__RECENT_APPS, - "RECENT_APPS"), - BACK(FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__BACK, - "BACK"), - APP_SWITCH( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__APP_SWITCH, - "APP_SWITCH"), - LAUNCH_ASSISTANT( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_ASSISTANT, - "LAUNCH_ASSISTANT"), - LAUNCH_VOICE_ASSISTANT( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_VOICE_ASSISTANT, - "LAUNCH_VOICE_ASSISTANT"), - LAUNCH_SYSTEM_SETTINGS( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_SYSTEM_SETTINGS, - "LAUNCH_SYSTEM_SETTINGS"), - TOGGLE_NOTIFICATION_PANEL( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__TOGGLE_NOTIFICATION_PANEL, - "TOGGLE_NOTIFICATION_PANEL"), - TOGGLE_TASKBAR( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__TOGGLE_TASKBAR, - "TOGGLE_TASKBAR"), - TAKE_SCREENSHOT( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__TAKE_SCREENSHOT, - "TAKE_SCREENSHOT"), - OPEN_SHORTCUT_HELPER( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__OPEN_SHORTCUT_HELPER, - "OPEN_SHORTCUT_HELPER"), - BRIGHTNESS_UP( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__BRIGHTNESS_UP, - "BRIGHTNESS_UP"), - BRIGHTNESS_DOWN( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__BRIGHTNESS_DOWN, - "BRIGHTNESS_DOWN"), - KEYBOARD_BACKLIGHT_UP( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__KEYBOARD_BACKLIGHT_UP, - "KEYBOARD_BACKLIGHT_UP"), - KEYBOARD_BACKLIGHT_DOWN( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__KEYBOARD_BACKLIGHT_DOWN, - "KEYBOARD_BACKLIGHT_DOWN"), - KEYBOARD_BACKLIGHT_TOGGLE( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__KEYBOARD_BACKLIGHT_TOGGLE, - "KEYBOARD_BACKLIGHT_TOGGLE"), - VOLUME_UP( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__VOLUME_UP, - "VOLUME_UP"), - VOLUME_DOWN( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__VOLUME_DOWN, - "VOLUME_DOWN"), - VOLUME_MUTE( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__VOLUME_MUTE, - "VOLUME_MUTE"), - ALL_APPS( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__ALL_APPS, - "ALL_APPS"), - LAUNCH_SEARCH( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_SEARCH, - "LAUNCH_SEARCH"), - LANGUAGE_SWITCH( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LANGUAGE_SWITCH, - "LANGUAGE_SWITCH"), - ACCESSIBILITY_ALL_APPS( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__ACCESSIBILITY_ALL_APPS, - "ACCESSIBILITY_ALL_APPS"), - TOGGLE_CAPS_LOCK( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__TOGGLE_CAPS_LOCK, - "TOGGLE_CAPS_LOCK"), - SYSTEM_MUTE( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__SYSTEM_MUTE, - "SYSTEM_MUTE"), - SPLIT_SCREEN_NAVIGATION( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__SPLIT_SCREEN_NAVIGATION, - "SPLIT_SCREEN_NAVIGATION"), - - CHANGE_SPLITSCREEN_FOCUS( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__CHANGE_SPLITSCREEN_FOCUS, - "CHANGE_SPLITSCREEN_FOCUS"), - TRIGGER_BUG_REPORT( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__TRIGGER_BUG_REPORT, - "TRIGGER_BUG_REPORT"), - LOCK_SCREEN( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LOCK_SCREEN, - "LOCK_SCREEN"), - OPEN_NOTES( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__OPEN_NOTES, - "OPEN_NOTES"), - TOGGLE_POWER( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__TOGGLE_POWER, - "TOGGLE_POWER"), - SYSTEM_NAVIGATION( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__SYSTEM_NAVIGATION, - "SYSTEM_NAVIGATION"), - SLEEP(FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__SLEEP, - "SLEEP"), - WAKEUP(FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__WAKEUP, - "WAKEUP"), - MEDIA_KEY( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__MEDIA_KEY, - "MEDIA_KEY"), - LAUNCH_DEFAULT_BROWSER( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_BROWSER, - "LAUNCH_DEFAULT_BROWSER"), - LAUNCH_DEFAULT_EMAIL( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_EMAIL, - "LAUNCH_DEFAULT_EMAIL"), - LAUNCH_DEFAULT_CONTACTS( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_CONTACTS, - "LAUNCH_DEFAULT_CONTACTS"), - LAUNCH_DEFAULT_CALENDAR( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_CALENDAR, - "LAUNCH_DEFAULT_CALENDAR"), - LAUNCH_DEFAULT_CALCULATOR( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_CALCULATOR, - "LAUNCH_DEFAULT_CALCULATOR"), - LAUNCH_DEFAULT_MUSIC( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_MUSIC, - "LAUNCH_DEFAULT_MUSIC"), - LAUNCH_DEFAULT_MAPS( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_MAPS, - "LAUNCH_DEFAULT_MAPS"), - LAUNCH_DEFAULT_MESSAGING( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_MESSAGING, - "LAUNCH_DEFAULT_MESSAGING"), - LAUNCH_DEFAULT_GALLERY( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_GALLERY, - "LAUNCH_DEFAULT_GALLERY"), - LAUNCH_DEFAULT_FILES( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_FILES, - "LAUNCH_DEFAULT_FILES"), - LAUNCH_DEFAULT_WEATHER( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_WEATHER, - "LAUNCH_DEFAULT_WEATHER"), - LAUNCH_DEFAULT_FITNESS( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_FITNESS, - "LAUNCH_DEFAULT_FITNESS"), - LAUNCH_APPLICATION_BY_PACKAGE_NAME( - FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_APPLICATION_BY_PACKAGE_NAME, - "LAUNCH_APPLICATION_BY_PACKAGE_NAME"), - DESKTOP_MODE( - FrameworkStatsLog - .KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__DESKTOP_MODE, - "DESKTOP_MODE"), - MULTI_WINDOW_NAVIGATION(FrameworkStatsLog - .KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__MULTI_WINDOW_NAVIGATION, - "MULTIWINDOW_NAVIGATION"); - - - private final int mValue; - private final String mName; - - private static final SparseArray<KeyboardLogEvent> VALUE_TO_ENUM_MAP = new SparseArray<>(); - - static { - for (KeyboardLogEvent type : KeyboardLogEvent.values()) { - VALUE_TO_ENUM_MAP.put(type.mValue, type); - } - } - - KeyboardLogEvent(int enumValue, String enumName) { - mValue = enumValue; - mName = enumName; - } - - public int getIntValue() { - return mValue; - } - - /** - * Convert int value to corresponding KeyboardLogEvent enum. If can't find any matching - * value will return {@code null} - */ - @Nullable - public static KeyboardLogEvent from(int value) { - return VALUE_TO_ENUM_MAP.get(value); - } - - /** - * Find KeyboardLogEvent corresponding to volume up/down/mute key events. - */ - @Nullable - public static KeyboardLogEvent getVolumeEvent(int keycode) { - switch (keycode) { - case KeyEvent.KEYCODE_VOLUME_DOWN: - return VOLUME_DOWN; - case KeyEvent.KEYCODE_VOLUME_UP: - return VOLUME_UP; - case KeyEvent.KEYCODE_VOLUME_MUTE: - return VOLUME_MUTE; - default: - return null; - } - } - - /** - * Find KeyboardLogEvent corresponding to brightness up/down key events. - */ - @Nullable - public static KeyboardLogEvent getBrightnessEvent(int keycode) { - switch (keycode) { - case KeyEvent.KEYCODE_BRIGHTNESS_DOWN: - return BRIGHTNESS_DOWN; - case KeyEvent.KEYCODE_BRIGHTNESS_UP: - return BRIGHTNESS_UP; - default: - return null; - } - } - - /** - * Find KeyboardLogEvent corresponding to intent filter category. Returns - * {@code null if no matching event found} - */ - @Nullable - public static KeyboardLogEvent getLogEventFromIntent(Intent intent) { - Intent selectorIntent = intent.getSelector(); - if (selectorIntent != null) { - Set<String> selectorCategories = selectorIntent.getCategories(); - if (selectorCategories != null && !selectorCategories.isEmpty()) { - for (String intentCategory : selectorCategories) { - KeyboardLogEvent logEvent = getEventFromSelectorCategory(intentCategory); - if (logEvent == null) { - continue; - } - return logEvent; - } - } - } - - // The shortcut may be targeting a system role rather than using an intent selector, - // so check for that. - String role = intent.getStringExtra(ModifierShortcutManager.EXTRA_ROLE); - if (!TextUtils.isEmpty(role)) { - return getLogEventFromRole(role); - } - - Set<String> intentCategories = intent.getCategories(); - if (intentCategories == null || intentCategories.isEmpty() - || !intentCategories.contains(Intent.CATEGORY_LAUNCHER)) { - return null; - } - if (intent.getComponent() == null) { - return null; - } - - // TODO(b/280423320): Add new field package name associated in the - // KeyboardShortcutEvent atom and log it accordingly. - return LAUNCH_APPLICATION_BY_PACKAGE_NAME; - } - - @Nullable - private static KeyboardLogEvent getEventFromSelectorCategory(String category) { - switch (category) { - case Intent.CATEGORY_APP_BROWSER: - return LAUNCH_DEFAULT_BROWSER; - case Intent.CATEGORY_APP_EMAIL: - return LAUNCH_DEFAULT_EMAIL; - case Intent.CATEGORY_APP_CONTACTS: - return LAUNCH_DEFAULT_CONTACTS; - case Intent.CATEGORY_APP_CALENDAR: - return LAUNCH_DEFAULT_CALENDAR; - case Intent.CATEGORY_APP_CALCULATOR: - return LAUNCH_DEFAULT_CALCULATOR; - case Intent.CATEGORY_APP_MUSIC: - return LAUNCH_DEFAULT_MUSIC; - case Intent.CATEGORY_APP_MAPS: - return LAUNCH_DEFAULT_MAPS; - case Intent.CATEGORY_APP_MESSAGING: - return LAUNCH_DEFAULT_MESSAGING; - case Intent.CATEGORY_APP_GALLERY: - return LAUNCH_DEFAULT_GALLERY; - case Intent.CATEGORY_APP_FILES: - return LAUNCH_DEFAULT_FILES; - case Intent.CATEGORY_APP_WEATHER: - return LAUNCH_DEFAULT_WEATHER; - case Intent.CATEGORY_APP_FITNESS: - return LAUNCH_DEFAULT_FITNESS; - default: - return null; - } - } - - /** - * Find KeyboardLogEvent corresponding to the provide system role name. - * Returns {@code null} if no matching event found. - */ - @Nullable - private static KeyboardLogEvent getLogEventFromRole(String role) { - if (RoleManager.ROLE_BROWSER.equals(role)) { - return LAUNCH_DEFAULT_BROWSER; - } else if (RoleManager.ROLE_SMS.equals(role)) { - return LAUNCH_DEFAULT_MESSAGING; - } else { - Log.w(TAG, "Keyboard shortcut to launch " - + role + " not supported for logging"); - return null; - } - } - } - /** * Log keyboard system shortcuts for the proto * {@link com.android.os.input.KeyboardSystemsEventReported} * defined in "stats/atoms/input/input_extension_atoms.proto" */ - public static void logKeyboardSystemsEventReportedAtom(@Nullable InputDevice inputDevice, - @Nullable KeyboardLogEvent keyboardSystemEvent, int modifierState, int... keyCodes) { - // Logging Keyboard system event only for an external HW keyboard. We should not log events - // for virtual keyboards or internal Key events. - if (inputDevice == null || inputDevice.isVirtual() || !inputDevice.isFullKeyboard()) { - return; - } - if (keyboardSystemEvent == null) { - Slog.w(TAG, "Invalid keyboard event logging, keycode = " + Arrays.toString(keyCodes) - + ", modifier state = " + modifierState); - return; - } + public static void logKeyboardSystemsEventReportedAtom(@NonNull InputDevice inputDevice, + @NonNull KeyboardSystemShortcut keyboardSystemShortcut) { FrameworkStatsLog.write(FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED, inputDevice.getVendorId(), inputDevice.getProductId(), - keyboardSystemEvent.getIntValue(), keyCodes, modifierState, - inputDevice.getDeviceBus()); + keyboardSystemShortcut.getSystemShortcut(), keyboardSystemShortcut.getKeycodes(), + keyboardSystemShortcut.getModifierState(), inputDevice.getDeviceBus()); if (DEBUG) { - Slog.d(TAG, "Logging Keyboard system event: " + keyboardSystemEvent.mName); + Slog.d(TAG, "Logging Keyboard system event: " + keyboardSystemShortcut); } } diff --git a/services/core/java/com/android/server/input/KeyboardShortcutCallbackHandler.java b/services/core/java/com/android/server/input/KeyboardShortcutCallbackHandler.java new file mode 100644 index 000000000000..092058e6f7d0 --- /dev/null +++ b/services/core/java/com/android/server/input/KeyboardShortcutCallbackHandler.java @@ -0,0 +1,137 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.input; + +import android.annotation.BinderThread; +import android.hardware.input.IKeyboardSystemShortcutListener; +import android.hardware.input.KeyboardSystemShortcut; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; +import android.util.Slog; +import android.util.SparseArray; + +import com.android.internal.annotations.GuardedBy; + +/** + * A thread-safe component of {@link InputManagerService} responsible for managing callbacks when a + * keyboard shortcut is triggered. + */ +final class KeyboardShortcutCallbackHandler { + + private static final String TAG = "KeyboardShortcut"; + + // To enable these logs, run: + // 'adb shell setprop log.tag.KeyboardShortcutCallbackHandler DEBUG' (requires restart) + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + // List of currently registered keyboard system shortcut listeners keyed by process pid + @GuardedBy("mKeyboardSystemShortcutListenerRecords") + private final SparseArray<KeyboardSystemShortcutListenerRecord> + mKeyboardSystemShortcutListenerRecords = new SparseArray<>(); + + public void onKeyboardSystemShortcutTriggered(int deviceId, + KeyboardSystemShortcut systemShortcut) { + if (DEBUG) { + Slog.d(TAG, "Keyboard system shortcut triggered, deviceId = " + deviceId + + ", systemShortcut = " + systemShortcut); + } + + synchronized (mKeyboardSystemShortcutListenerRecords) { + for (int i = 0; i < mKeyboardSystemShortcutListenerRecords.size(); i++) { + mKeyboardSystemShortcutListenerRecords.valueAt(i).onKeyboardSystemShortcutTriggered( + deviceId, systemShortcut); + } + } + } + + /** Register the keyboard system shortcut listener for a process. */ + @BinderThread + public void registerKeyboardSystemShortcutListener(IKeyboardSystemShortcutListener listener, + int pid) { + synchronized (mKeyboardSystemShortcutListenerRecords) { + if (mKeyboardSystemShortcutListenerRecords.get(pid) != null) { + throw new IllegalStateException("The calling process has already registered " + + "a KeyboardSystemShortcutListener."); + } + KeyboardSystemShortcutListenerRecord record = new KeyboardSystemShortcutListenerRecord( + pid, listener); + try { + listener.asBinder().linkToDeath(record, 0); + } catch (RemoteException ex) { + throw new RuntimeException(ex); + } + mKeyboardSystemShortcutListenerRecords.put(pid, record); + } + } + + /** Unregister the keyboard system shortcut listener for a process. */ + @BinderThread + public void unregisterKeyboardSystemShortcutListener(IKeyboardSystemShortcutListener listener, + int pid) { + synchronized (mKeyboardSystemShortcutListenerRecords) { + KeyboardSystemShortcutListenerRecord record = + mKeyboardSystemShortcutListenerRecords.get(pid); + if (record == null) { + throw new IllegalStateException("The calling process has no registered " + + "KeyboardSystemShortcutListener."); + } + if (record.mListener.asBinder() != listener.asBinder()) { + throw new IllegalStateException("The calling process has a different registered " + + "KeyboardSystemShortcutListener."); + } + record.mListener.asBinder().unlinkToDeath(record, 0); + mKeyboardSystemShortcutListenerRecords.remove(pid); + } + } + + private void onKeyboardSystemShortcutListenerDied(int pid) { + synchronized (mKeyboardSystemShortcutListenerRecords) { + mKeyboardSystemShortcutListenerRecords.remove(pid); + } + } + + // A record of a registered keyboard system shortcut listener from one process. + private class KeyboardSystemShortcutListenerRecord implements IBinder.DeathRecipient { + public final int mPid; + public final IKeyboardSystemShortcutListener mListener; + + KeyboardSystemShortcutListenerRecord(int pid, IKeyboardSystemShortcutListener listener) { + mPid = pid; + mListener = listener; + } + + @Override + public void binderDied() { + if (DEBUG) { + Slog.d(TAG, "Keyboard system shortcut listener for pid " + mPid + " died."); + } + onKeyboardSystemShortcutListenerDied(mPid); + } + + public void onKeyboardSystemShortcutTriggered(int deviceId, KeyboardSystemShortcut data) { + try { + mListener.onKeyboardSystemShortcutTriggered(deviceId, data.getKeycodes(), + data.getModifierState(), data.getSystemShortcut()); + } catch (RemoteException ex) { + Slog.w(TAG, "Failed to notify process " + mPid + + " that keyboard system shortcut was triggered, assuming it died.", ex); + binderDied(); + } + } + } +} diff --git a/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java b/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java index 94b14730bb07..079b7242b1f3 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java @@ -109,10 +109,6 @@ final class InputMethodBindingController { * <dd> * If this bit is ON, some of IME view, e.g. software input, candidate view, is visible. * </dd> - * <dt>{@link InputMethodService#IME_INVISIBLE}</dt> - * <dd> If this bit is ON, IME is ready with views from last EditorInfo but is - * currently invisible. - * </dd> * </dl> * <em>Do not update this value outside of {@link #setImeWindowStatus(IBinder, int, int)} and * {@link InputMethodBindingController#unbindCurrentMethod()}.</em> diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java index 4d06f50d4d45..e36d5bbbd8d2 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java @@ -16,7 +16,7 @@ package com.android.server.inputmethod; -import static com.android.server.inputmethod.InputMethodUtils.NOT_A_SUBTYPE_ID; +import static com.android.server.inputmethod.InputMethodUtils.NOT_A_SUBTYPE_INDEX; import static java.lang.annotation.RetentionPolicy.SOURCE; @@ -140,23 +140,26 @@ public abstract class InputMethodManagerInternal { * to be switched. */ public boolean switchToInputMethod(@NonNull String imeId, @UserIdInt int userId) { - return switchToInputMethod(imeId, NOT_A_SUBTYPE_ID, userId); + return switchToInputMethod(imeId, NOT_A_SUBTYPE_INDEX, userId); } /** * Force switch to the enabled input method by {@code imeId} for the current user. If the input - * method with {@code imeId} is not enabled or not installed, do nothing. If {@code subtypeId} - * is also supplied (not {@link InputMethodUtils#NOT_A_SUBTYPE_ID}) and valid, also switches to - * it, otherwise the system decides the most sensible default subtype to use. + * method with {@code imeId} is not enabled or not installed, do nothing. If + * {@code subtypeIndex} is also supplied (not {@link InputMethodUtils#NOT_A_SUBTYPE_INDEX}) and + * valid, also switches to it, otherwise the system decides the most sensible default subtype to + * use. * - * @param imeId the input method ID to be switched to - * @param subtypeId the input method subtype ID to be switched to - * @param userId the user ID to be queried + * @param imeId the input method ID to be switched to + * @param subtypeIndex the subtype to be switched to, as an index in the input method's array of + * subtypes, or {@link InputMethodUtils#NOT_A_SUBTYPE_INDEX} if the system + * should decide the most sensible subtype + * @param userId the user ID to be queried * @return {@code true} if the current input method was successfully switched to the input * method by {@code imeId}; {@code false} the input method with {@code imeId} is not available * to be switched. */ - public abstract boolean switchToInputMethod(@NonNull String imeId, int subtypeId, + public abstract boolean switchToInputMethod(@NonNull String imeId, int subtypeIndex, @UserIdInt int userId); /** @@ -376,7 +379,7 @@ public abstract class InputMethodManagerInternal { } @Override - public boolean switchToInputMethod(@NonNull String imeId, int subtypeId, + public boolean switchToInputMethod(@NonNull String imeId, int subtypeIndex, @UserIdInt int userId) { return false; } diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index ea3d7d10f3f8..3863cf0a04cf 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -49,10 +49,12 @@ import static android.view.inputmethod.ConnectionlessHandwritingCallback.CONNECT import static com.android.server.inputmethod.ImeVisibilityStateComputer.ImeTargetWindowState; import static com.android.server.inputmethod.ImeVisibilityStateComputer.ImeVisibilityResult; -import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_SHOW_IME; import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_HIDE_IME; +import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_SHOW_IME; import static com.android.server.inputmethod.InputMethodBindingController.TIME_TO_RECONNECT; +import static com.android.server.inputmethod.InputMethodSettings.INVALID_SUBTYPE_HASHCODE; import static com.android.server.inputmethod.InputMethodSubtypeSwitchingController.MODE_AUTO; +import static com.android.server.inputmethod.InputMethodUtils.NOT_A_SUBTYPE_INDEX; import static com.android.server.inputmethod.InputMethodUtils.isSoftInputModeStateVisibleAllowed; import static java.lang.annotation.RetentionPolicy.SOURCE; @@ -81,7 +83,6 @@ import android.content.pm.PackageManagerInternal; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.content.pm.UserInfo; -import android.content.res.Configuration; import android.content.res.Resources; import android.hardware.input.InputManager; import android.inputmethodservice.InputMethodService; @@ -273,11 +274,6 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. private static final int MSG_NOTIFY_IME_UID_TO_AUDIO_SERVICE = 7000; - private static final int NOT_A_SUBTYPE_ID = InputMethodUtils.NOT_A_SUBTYPE_ID; - - private static final int INVALID_SUBTYPE_HASHCODE = - InputMethodSettings.INVALID_SUBTYPE_HASHCODE; - private static final String TAG_TRY_SUPPRESSING_IME_SWITCHER = "TrySuppressingImeSwitcher"; private static final String HANDLER_THREAD_NAME = "android.imms"; private static final String PACKAGE_MONITOR_THREAD_NAME = "android.imms2"; @@ -303,28 +299,6 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. private final String[] mNonPreemptibleInputMethods; /** - * Whether the new Input Method Switcher menu is enabled. - * - * @see #shouldEnableNewInputMethodSwitcherMenu - */ - @SharedByAllUsersField - private final boolean mNewInputMethodSwitcherMenuEnabled; - - /** - * Returns {@code true} if the new Input Method Switcher menu is enabled. This will be - * {@code false} for watches and small screen devices. - * - * @param context the context to check the device configuration for. - */ - private static boolean shouldEnableNewInputMethodSwitcherMenu(@NonNull Context context) { - final boolean isWatch = context.getPackageManager() - .hasSystemFeature(PackageManager.FEATURE_WATCH); - final boolean isSmallScreen = (context.getResources().getConfiguration().screenLayout - & Configuration.SCREENLAYOUT_SIZE_MASK) == Configuration.SCREENLAYOUT_SIZE_SMALL; - return Flags.imeSwitcherRevamp() && !isWatch && !isSmallScreen; - } - - /** * See {@link #shouldEnableConcurrentMultiUserMode(Context)} about when set to be {@code true}. */ @SharedByAllUsersField @@ -612,7 +586,7 @@ 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 (!Flags.imeSwitcherRevamp()) { if (userId == mCurrentUserId) { mMenuController.updateKeyboardFromSettingsLocked(userId); } @@ -680,7 +654,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. return; } final int userId = mCurrentUserId; - if (mNewInputMethodSwitcherMenuEnabled) { + if (Flags.imeSwitcherRevamp()) { final var bindingController = getInputMethodBindingController(userId); mMenuControllerNew.hide(bindingController.getCurTokenDisplayId(), userId); } else { @@ -718,7 +692,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. userData.mRawInputMethodMap.set(rawMethodMap); final var methodMap = rawMethodMap.toInputMethodMap(additionalSubtypeMap, DirectBootAwareness.AUTO, - mUserManagerInternal.isUserUnlockingOrUnlocked(userId)); + userData.mIsUnlockingOrUnlocked.get()); final var settings = InputMethodSettings.create(methodMap, userId); InputMethodSettingsRepository.put(userId, settings); } @@ -842,7 +816,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. final var newMethodMap = userData.mRawInputMethodMap.get().toInputMethodMap( newAdditionalSubtypeMap, DirectBootAwareness.AUTO, - mUserManagerInternal.isUserUnlockingOrUnlocked(userId)); + userData.mIsUnlockingOrUnlocked.get()); final boolean noUpdate = InputMethodMap.areSame(settings.getMethodMap(), newMethodMap); if (noUpdate && imesToBeDisabled.isEmpty()) { @@ -1072,6 +1046,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. final int userId = user.getUserIdentifier(); final var userData = mService.getUserData(userId); final boolean userUnlocked = true; + userData.mIsUnlockingOrUnlocked.set(userUnlocked); SecureSettingsWrapper.onUserUnlocking(userId); final var methodMap = userData.mRawInputMethodMap.get().toInputMethodMap( AdditionalSubtypeMapRepository.get(userId), DirectBootAwareness.AUTO, @@ -1122,9 +1097,12 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. final var additionalSubtypeMap = AdditionalSubtypeMapRepository.get(userId); final var rawMethodMap = queryRawInputMethodServiceMap(context, userId); userData.mRawInputMethodMap.set(rawMethodMap); + + final boolean unlocked = userManagerInternal.isUserUnlockingOrUnlocked(userId); + userData.mIsUnlockingOrUnlocked.set(unlocked); final var methodMap = rawMethodMap.toInputMethodMap(additionalSubtypeMap, - DirectBootAwareness.AUTO, - userManagerInternal.isUserUnlockingOrUnlocked(userId)); + DirectBootAwareness.AUTO, unlocked); + final var settings = InputMethodSettings.create(methodMap, userId); InputMethodSettingsRepository.put(userId, settings); @@ -1152,6 +1130,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. final var additionalSubtypeMap = AdditionalSubtypeMapRepository.get(userId); final var rawMethodMap = userData.mRawInputMethodMap.get(); final boolean userUnlocked = false; // Stopping a user also locks their storage. + userData.mIsUnlockingOrUnlocked.set(userUnlocked); final var methodMap = rawMethodMap.toInputMethodMap(additionalSubtypeMap, DirectBootAwareness.AUTO, userUnlocked); InputMethodSettingsRepository.put(userId, @@ -1205,7 +1184,6 @@ 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; @@ -1221,7 +1199,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. : bindingControllerFactory, visibilityStateComputerFactory); mMenuController = new InputMethodMenuController(this); - mMenuControllerNew = mNewInputMethodSwitcherMenuEnabled + mMenuControllerNew = Flags.imeSwitcherRevamp() ? new InputMethodMenuControllerNew() : null; mVisibilityApplier = new DefaultImeVisibilityApplier(this); @@ -1289,7 +1267,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 @@ -1355,6 +1333,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), @@ -1635,7 +1624,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. final var userData = getUserData(userId); final var methodMap = userData.mRawInputMethodMap.get().toInputMethodMap( AdditionalSubtypeMapRepository.get(userId), directBootAwareness, - mUserManagerInternal.isUserUnlockingOrUnlocked(userId)); + userData.mIsUnlockingOrUnlocked.get()); final var settings = InputMethodSettings.create(methodMap, userId); // Create a copy. final ArrayList<InputMethodInfo> methodList = new ArrayList<>(settings.getMethodList()); @@ -1810,7 +1799,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. ImeTracker.PHASE_SERVER_WAIT_IME); userData.mCurStatsToken = null; // TODO: Make mMenuController multi-user aware - if (mNewInputMethodSwitcherMenuEnabled) { + if (Flags.imeSwitcherRevamp()) { mMenuControllerNew.hide(bindingController.getCurTokenDisplayId(), userId); } else { mMenuController.hideInputMethodMenuLocked(userId); @@ -2018,7 +2007,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; } @@ -2620,7 +2609,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. if (!mShowOngoingImeSwitcherForPhones) return false; // When the IME switcher dialog is shown, the IME switcher button should be hidden. // TODO(b/305849394): Make mMenuController multi-user aware. - final boolean switcherMenuShowing = mNewInputMethodSwitcherMenuEnabled + final boolean switcherMenuShowing = Flags.imeSwitcherRevamp() ? mMenuControllerNew.isShowing() : mMenuController.getSwitchingDialogLocked() != null; if (switcherMenuShowing) { @@ -2636,12 +2625,10 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. && mWindowManagerInternal.isKeyguardSecure(userId)) { return false; } - if ((visibility & InputMethodService.IME_ACTIVE) == 0 - || (visibility & InputMethodService.IME_INVISIBLE) != 0) { + if ((visibility & InputMethodService.IME_ACTIVE) == 0) { return false; } - if (mWindowManagerInternal.isHardKeyboardAvailable() - && !mNewInputMethodSwitcherMenuEnabled) { + if (mWindowManagerInternal.isHardKeyboardAvailable() && !Flags.imeSwitcherRevamp()) { // When physical keyboard is attached, we show the ime switcher (or notification if // NavBar is not available) because SHOW_IME_WITH_HARD_KEYBOARD settings currently // exists in the IME switcher dialog. Might be OK to remove this condition once @@ -2652,7 +2639,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()); @@ -2803,7 +2790,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. if (DEBUG) { Slog.d(TAG, "IME window vis: " + vis + " active: " + (vis & InputMethodService.IME_ACTIVE) - + " inv: " + (vis & InputMethodService.IME_INVISIBLE) + + " visible: " + (vis & InputMethodService.IME_VISIBLE) + " displayId: " + curTokenDisplayId); } final IBinder focusedWindowToken = userData.mImeBindingState != null @@ -2825,7 +2812,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } final var curId = bindingController.getCurId(); // TODO(b/305849394): Make mMenuController multi-user aware. - final boolean switcherMenuShowing = mNewInputMethodSwitcherMenuEnabled + final boolean switcherMenuShowing = Flags.imeSwitcherRevamp() ? mMenuControllerNew.isShowing() : mMenuController.getSwitchingDialogLocked() != null; if (switcherMenuShowing @@ -2847,7 +2834,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @GuardedBy("ImfLock.class") void updateFromSettingsLocked(boolean enabledMayChange, @UserIdInt int userId) { updateInputMethodsFromSettingsLocked(enabledMayChange, userId); - if (!mNewInputMethodSwitcherMenuEnabled) { + if (!Flags.imeSwitcherRevamp()) { mMenuController.updateKeyboardFromSettingsLocked(userId); } } @@ -2915,7 +2902,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } if (!TextUtils.isEmpty(id)) { try { - setInputMethodLocked(id, settings.getSelectedInputMethodSubtypeId(id), userId); + setInputMethodLocked(id, settings.getSelectedInputMethodSubtypeIndex(id), userId); } catch (IllegalArgumentException e) { Slog.w(TAG, "Unknown input method from prefs: " + id, e); resetCurrentMethodAndClientLocked(UnbindReason.SWITCH_IME_FAILED, userId); @@ -2938,17 +2925,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 != mCurrentUserId) { + return; + } mInputManagerInternal.onInputMethodSubtypeChangedForKeyboardLayoutMapping( userId, newSubtypeHandle, normalizedSubtype); } @GuardedBy("ImfLock.class") - void setInputMethodLocked(String id, int subtypeId, @UserIdInt int userId) { - setInputMethodLocked(id, subtypeId, DEVICE_ID_DEFAULT, userId); + void setInputMethodLocked(String id, int subtypeIndex, @UserIdInt int userId) { + setInputMethodLocked(id, subtypeIndex, DEVICE_ID_DEFAULT, userId); } @GuardedBy("ImfLock.class") - void setInputMethodLocked(String id, int subtypeId, int deviceId, @UserIdInt int userId) { + void setInputMethodLocked(String id, int subtypeIndex, int deviceId, @UserIdInt int userId) { final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); InputMethodInfo info = settings.getMethodMap().get(id); if (info == null) { @@ -2965,25 +2963,25 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } final InputMethodSubtype oldSubtype = bindingController.getCurrentSubtype(); final InputMethodSubtype newSubtype; - if (subtypeId >= 0 && subtypeId < subtypeCount) { - newSubtype = info.getSubtypeAt(subtypeId); + if (subtypeIndex >= 0 && subtypeIndex < subtypeCount) { + newSubtype = info.getSubtypeAt(subtypeIndex); } else { // If subtype is null, try to find the most applicable one from // getCurrentInputMethodSubtype. - subtypeId = NOT_A_SUBTYPE_ID; + subtypeIndex = NOT_A_SUBTYPE_INDEX; // TODO(b/347083680): The method below has questionable behaviors. newSubtype = bindingController.getCurrentInputMethodSubtype(); if (newSubtype != null) { for (int i = 0; i < subtypeCount; ++i) { if (Objects.equals(newSubtype, info.getSubtypeAt(i))) { - subtypeId = i; + subtypeIndex = i; break; } } } } if (!Objects.equals(newSubtype, oldSubtype)) { - setSelectedInputMethodAndSubtypeLocked(info, subtypeId, true, userId); + setSelectedInputMethodAndSubtypeLocked(info, subtypeIndex, true, userId); IInputMethodInvoker curMethod = bindingController.getCurMethod(); if (curMethod != null) { updateSystemUiLocked(bindingController.getImeWindowVis(), @@ -3010,9 +3008,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(). @@ -4016,7 +4012,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @IInputMethodManagerImpl.PermissionVerified(Manifest.permission.TEST_INPUT_METHOD) public boolean isInputMethodPickerShownForTest() { synchronized (ImfLock.class) { - return mNewInputMethodSwitcherMenuEnabled + return Flags.imeSwitcherRevamp() ? mMenuControllerNew.isShowing() : mMenuController.isisInputMethodPickerShownForTestLocked(); } @@ -4025,11 +4021,12 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. /** * Gets the list of Input Method Switcher Menu items and the index of the selected item. * - * @param items the list of input method and subtype items. - * @param selectedImeId the ID of the selected input method. - * @param selectedSubtypeId the ID of the selected input method subtype, - * or {@link #NOT_A_SUBTYPE_ID} if no subtype is selected. - * @param userId the ID of the user for which to get the menu items. + * @param items the list of input method and subtype items. + * @param selectedImeId the ID of the selected input method. + * @param selectedSubtypeIndex the index of the selected subtype in the input method's array of + * subtypes, or {@link InputMethodUtils#NOT_A_SUBTYPE_INDEX} if no + * subtype is selected. + * @param userId the ID of the user for which to get the menu items. * @return the list of menu items, and the index of the selected item, * or {@code -1} if no item is selected. */ @@ -4037,17 +4034,17 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @NonNull private Pair<List<MenuItem>, Integer> getInputMethodPickerItems( @NonNull List<ImeSubtypeListItem> items, @Nullable String selectedImeId, - int selectedSubtypeId, @UserIdInt int userId) { + int selectedSubtypeIndex, @UserIdInt int userId) { final var bindingController = getInputMethodBindingController(userId); final var settings = InputMethodSettingsRepository.get(userId); - if (selectedSubtypeId == NOT_A_SUBTYPE_ID) { + if (selectedSubtypeIndex == NOT_A_SUBTYPE_INDEX) { // TODO(b/351124299): Check if this fallback logic is still necessary. final var curSubtype = bindingController.getCurrentInputMethodSubtype(); if (curSubtype != null) { final var curMethodId = bindingController.getSelectedMethodId(); final var curImi = settings.getMethodMap().get(curMethodId); - selectedSubtypeId = SubtypeUtils.getSubtypeIdFromHashCode( + selectedSubtypeIndex = SubtypeUtils.getSubtypeIndexFromHashCode( curImi, curSubtype.hashCode()); } } @@ -4062,19 +4059,19 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. final var item = items.get(i); final var imeId = item.mImi.getId(); if (imeId.equals(selectedImeId)) { - final int subtypeId = item.mSubtypeId; + final int subtypeIndex = item.mSubtypeIndex; // Check if this is the selected IME-subtype pair. - if ((subtypeId == 0 && selectedSubtypeId == NOT_A_SUBTYPE_ID) - || subtypeId == NOT_A_SUBTYPE_ID - || subtypeId == selectedSubtypeId) { + if ((subtypeIndex == 0 && selectedSubtypeIndex == NOT_A_SUBTYPE_INDEX) + || subtypeIndex == NOT_A_SUBTYPE_INDEX + || subtypeIndex == selectedSubtypeIndex) { selectedIndex = i; } } final boolean hasHeader = !imeId.equals(prevImeId); final boolean hasDivider = hasHeader && prevImeId != null; prevImeId = imeId; - menuItems.add(new MenuItem(item.mImeName, item.mSubtypeName, item.mImi, item.mSubtypeId, - hasHeader, hasDivider)); + menuItems.add(new MenuItem(item.mImeName, item.mSubtypeName, item.mImi, + item.mSubtypeIndex, hasHeader, hasDivider)); } return new Pair<>(menuItems, selectedIndex); @@ -4131,10 +4128,10 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. imi.getPackageName(), callingUid, userId, settings)) { throw getExceptionForUnknownImeId(id); } - final int subtypeId = subtype != null - ? SubtypeUtils.getSubtypeIdFromHashCode(imi, subtype.hashCode()) - : NOT_A_SUBTYPE_ID; - setInputMethodWithSubtypeIdLocked(id, subtypeId, userId); + final int subtypeIndex = subtype != null + ? SubtypeUtils.getSubtypeIndexFromHashCode(imi, subtype.hashCode()) + : NOT_A_SUBTYPE_INDEX; + setInputMethodWithSubtypeIndexLocked(id, subtypeIndex, userId); } @BinderThread @@ -4152,18 +4149,18 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } final var currentSubtype = bindingController.getCurrentSubtype(); String targetLastImiId = null; - int subtypeId = NOT_A_SUBTYPE_ID; + int subtypeIndex = NOT_A_SUBTYPE_INDEX; if (lastIme != null && lastImi != null) { final boolean imiIdIsSame = lastImi.getId().equals( bindingController.getSelectedMethodId()); final int lastSubtypeHash = Integer.parseInt(lastIme.second); - final int currentSubtypeHash = currentSubtype == null ? NOT_A_SUBTYPE_ID + final int currentSubtypeHash = currentSubtype == null ? NOT_A_SUBTYPE_INDEX : currentSubtype.hashCode(); // If the last IME is the same as the current IME and the last subtype is not // defined, there is no need to switch to the last IME. if (!imiIdIsSame || lastSubtypeHash != currentSubtypeHash) { targetLastImiId = lastIme.first; - subtypeId = SubtypeUtils.getSubtypeIdFromHashCode(lastImi, lastSubtypeHash); + subtypeIndex = SubtypeUtils.getSubtypeIndexFromHashCode(lastImi, lastSubtypeHash); } } @@ -4191,7 +4188,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. SubtypeUtils.SUBTYPE_MODE_KEYBOARD, locale, true); if (keyboardSubtype != null) { targetLastImiId = imi.getId(); - subtypeId = SubtypeUtils.getSubtypeIdFromHashCode(imi, + subtypeIndex = SubtypeUtils.getSubtypeIndexFromHashCode(imi, keyboardSubtype.hashCode()); if (keyboardSubtype.getLocale().equals(locale)) { break; @@ -4206,9 +4203,9 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. if (DEBUG) { Slog.d(TAG, "Switch to: " + lastImi.getId() + ", " + lastIme.second + ", from: " + bindingController.getSelectedMethodId() + ", " - + subtypeId); + + subtypeIndex); } - setInputMethodWithSubtypeIdLocked(targetLastImiId, subtypeId, userId); + setInputMethodWithSubtypeIndexLocked(targetLastImiId, subtypeIndex, userId); return true; } else { return false; @@ -4228,7 +4225,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. if (nextSubtype == null) { return false; } - setInputMethodWithSubtypeIdLocked(nextSubtype.mImi.getId(), nextSubtype.mSubtypeId, + setInputMethodWithSubtypeIndexLocked(nextSubtype.mImi.getId(), nextSubtype.mSubtypeIndex, userData.mUserId); return true; } @@ -4294,7 +4291,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); @@ -4661,7 +4658,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. proto.write(IS_INTERACTIVE, mIsInteractive); proto.write(BACK_DISPOSITION, bindingController.getBackDisposition()); proto.write(IME_WINDOW_VISIBILITY, bindingController.getImeWindowVis()); - if (!mNewInputMethodSwitcherMenuEnabled) { + if (!Flags.imeSwitcherRevamp()) { proto.write(SHOW_IME_WITH_HARD_KEYBOARD, mMenuController.getShowImeWithHardKeyboard()); } @@ -4715,7 +4712,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } @GuardedBy("ImfLock.class") - private void setInputMethodWithSubtypeIdLocked(String id, int subtypeId, + private void setInputMethodWithSubtypeIndexLocked(String id, int subtypeIndex, @UserIdInt int userId) { final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); if (settings.getMethodMap().get(id) != null @@ -4725,7 +4722,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } final long ident = Binder.clearCallingIdentity(); try { - setInputMethodLocked(id, subtypeId, userId); + setInputMethodLocked(id, subtypeIndex, userId); } finally { Binder.restoreCallingIdentity(ident); } @@ -4886,7 +4883,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. final boolean isScreenLocked = mWindowManagerInternal.isKeyguardLocked() && mWindowManagerInternal.isKeyguardSecure(userId); final String lastInputMethodId = settings.getSelectedInputMethod(); - int lastInputMethodSubtypeId = settings.getSelectedInputMethodSubtypeId(lastInputMethodId); + final int lastInputMethodSubtypeIndex = + settings.getSelectedInputMethodSubtypeIndex(lastInputMethodId); final List<ImeSubtypeListItem> imList = InputMethodSubtypeSwitchingController .getSortedInputMethodAndSubtypeList( @@ -4900,30 +4898,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); } } @@ -4990,7 +4988,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) { @@ -5442,7 +5440,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } @GuardedBy("ImfLock.class") - private void setSelectedInputMethodAndSubtypeLocked(InputMethodInfo imi, int subtypeId, + private void setSelectedInputMethodAndSubtypeLocked(InputMethodInfo imi, int subtypeIndex, boolean setSubtypeOnly, @UserIdInt int userId) { final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); final var bindingController = getInputMethodBindingController(userId); @@ -5452,12 +5450,12 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. // Set Subtype here final int newSubtypeHashcode; final InputMethodSubtype newSubtype; - if (imi == null || subtypeId < 0) { + if (imi == null || subtypeIndex < 0) { newSubtypeHashcode = INVALID_SUBTYPE_HASHCODE; newSubtype = null; } else { - if (subtypeId < imi.getSubtypeCount()) { - InputMethodSubtype subtype = imi.getSubtypeAt(subtypeId); + if (subtypeIndex < imi.getSubtypeCount()) { + InputMethodSubtype subtype = imi.getSubtypeAt(subtypeIndex); newSubtypeHashcode = subtype.hashCode(); newSubtype = subtype; } else { @@ -5493,20 +5491,20 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. settings.putSelectedDefaultDeviceInputMethod(null); InputMethodInfo imi = settings.getMethodMap().get(newDefaultIme); - int lastSubtypeId = NOT_A_SUBTYPE_ID; + int lastSubtypeIndex = NOT_A_SUBTYPE_INDEX; // newDefaultIme is empty when there is no candidate for the selected IME. if (imi != null && !TextUtils.isEmpty(newDefaultIme)) { String subtypeHashCode = settings.getLastSubtypeForInputMethod(newDefaultIme); if (subtypeHashCode != null) { try { - lastSubtypeId = SubtypeUtils.getSubtypeIdFromHashCode(imi, + lastSubtypeIndex = SubtypeUtils.getSubtypeIndexFromHashCode(imi, Integer.parseInt(subtypeHashCode)); } catch (NumberFormatException e) { Slog.w(TAG, "HashCode for subtype looks broken: " + subtypeHashCode, e); } } } - setSelectedInputMethodAndSubtypeLocked(imi, lastSubtypeId, false, userId); + setSelectedInputMethodAndSubtypeLocked(imi, lastSubtypeIndex, false, userId); } /** @@ -5530,14 +5528,14 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } @GuardedBy("ImfLock.class") - private boolean switchToInputMethodLocked(@NonNull String imeId, int subtypeId, + private boolean switchToInputMethodLocked(@NonNull String imeId, int subtypeIndex, @UserIdInt int userId) { final var settings = InputMethodSettingsRepository.get(userId); final var enabledImes = settings.getEnabledInputMethodList(); if (!CollectionUtils.any(enabledImes, imi -> imi.getId().equals(imeId))) { return false; // IME is not found or not enabled. } - setInputMethodLocked(imeId, subtypeId, userId); + setInputMethodLocked(imeId, subtypeIndex, userId); return true; } @@ -5593,8 +5591,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. return; } - final var nextSubtype = nextItem.mSubtypeId > NOT_A_SUBTYPE_ID - ? nextItem.mImi.getSubtypeAt(nextItem.mSubtypeId) : null; + final var nextSubtype = nextItem.mSubtypeIndex > NOT_A_SUBTYPE_INDEX + ? nextItem.mImi.getSubtypeAt(nextItem.mSubtypeIndex) : null; nextSubtypeHandle = InputMethodSubtypeHandle.of(nextItem.mImi, nextSubtype); } else { final InputMethodSubtypeHandle currentSubtypeHandle = @@ -5613,7 +5611,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. final int subtypeCount = nextImi.getSubtypeCount(); if (subtypeCount == 0) { if (nextSubtypeHandle.equals(InputMethodSubtypeHandle.of(nextImi, null))) { - setInputMethodLocked(nextImi.getId(), NOT_A_SUBTYPE_ID, userId); + setInputMethodLocked(nextImi.getId(), NOT_A_SUBTYPE_INDEX, userId); } return; } @@ -5691,10 +5689,10 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } @Override - public boolean switchToInputMethod(@NonNull String imeId, int subtypeId, + public boolean switchToInputMethod(@NonNull String imeId, int subtypeIndex, @UserIdInt int userId) { synchronized (ImfLock.class) { - return switchToInputMethodLocked(imeId, subtypeId, userId); + return switchToInputMethodLocked(imeId, subtypeIndex, userId); } } @@ -5776,7 +5774,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 { @@ -6111,6 +6109,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @SuppressWarnings("GuardedBy") Consumer<UserData> userDataDump = u -> { p.println(" mUserId=" + u.mUserId); + p.println(" unlocked=" + u.mIsUnlockingOrUnlocked.get()); p.println(" hasMainConnection=" + u.mBindingController.hasMainConnection()); p.println(" isVisibleBound=" + u.mBindingController.isVisibleBound()); @@ -6134,7 +6133,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. }; mUserDataRepository.forAllUserData(userDataDump); - if (mNewInputMethodSwitcherMenuEnabled) { + if (Flags.imeSwitcherRevamp()) { p.println(" menuControllerNew:"); mMenuControllerNew.dump(p, " "); } else { @@ -6563,7 +6562,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. 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); @@ -6577,17 +6576,6 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. out.print(imeId); out.print(" selected for user #"); out.println(userId); - - // Workaround for b/354782333. - final var settingsValue = SecureSettingsWrapper.getString( - Settings.Secure.DEFAULT_INPUT_METHOD, "", userId); - if (!TextUtils.equals(settingsValue, imeId)) { - Slog.w(TAG, "DEFAULT_INPUT_METHOD=" + settingsValue - + " is not updated. Fixing it up to " + imeId - + " See b/354782333."); - SecureSettingsWrapper.putString( - Settings.Secure.DEFAULT_INPUT_METHOD, imeId, userId); - } } hasFailed |= failedToSelectUnknownIme; } 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/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/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/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java index b164a52ff5d7..8c280edf03c0 100644 --- a/services/core/java/com/android/server/notification/ZenModeHelper.java +++ b/services/core/java/com/android/server/notification/ZenModeHelper.java @@ -1676,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 } } 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/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/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java index 7610d7d6b659..76872cf1274c 100644 --- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java +++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java @@ -467,6 +467,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 +509,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. 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..a22db97f449c 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -5463,6 +5463,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } } + mAtmService.mBackNavigationController.onAppVisibilityChanged(this, visible); onChildVisibilityRequested(visible); final DisplayContent displayContent = getDisplayContent(); diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java index 509a060c096d..8ef2693ec327 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java +++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java @@ -2846,6 +2846,11 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { } finally { SaferIntentUtils.DISABLE_ENFORCE_INTENTS_TO_MATCH_INTENT_FILTERS.set(false); synchronized (mService.mGlobalLock) { + // Remove the empty task in case the activity was failed to be launched on the + // task that was restored from Recents. + if (!task.hasChild() && task.shouldRemoveSelfOnLastChildRemoval()) { + task.removeIfPossible("start-from-recents"); + } mService.continueWindowLayout(); } } diff --git a/services/core/java/com/android/server/wm/AppCompatReachabilityOverrides.java b/services/core/java/com/android/server/wm/AppCompatReachabilityOverrides.java index b9bdc325cf98..caff96ba4a9f 100644 --- a/services/core/java/com/android/server/wm/AppCompatReachabilityOverrides.java +++ b/services/core/java/com/android/server/wm/AppCompatReachabilityOverrides.java @@ -35,7 +35,6 @@ import android.annotation.NonNull; import android.content.res.Configuration; import android.graphics.Rect; -import com.android.internal.annotations.VisibleForTesting; import com.android.window.flags.Flags; /** @@ -112,12 +111,10 @@ class AppCompatReachabilityOverrides { : mAppCompatConfiguration.getLetterboxVerticalPositionMultiplier(tabletopMode); } - @VisibleForTesting boolean isHorizontalReachabilityEnabled() { return isHorizontalReachabilityEnabled(mActivityRecord.getParent().getConfiguration()); } - @VisibleForTesting boolean isVerticalReachabilityEnabled() { return isVerticalReachabilityEnabled(mActivityRecord.getParent().getConfiguration()); } diff --git a/services/core/java/com/android/server/wm/AppCompatReachabilityPolicy.java b/services/core/java/com/android/server/wm/AppCompatReachabilityPolicy.java index 90bfddb2095f..c3bf116e227d 100644 --- a/services/core/java/com/android/server/wm/AppCompatReachabilityPolicy.java +++ b/services/core/java/com/android/server/wm/AppCompatReachabilityPolicy.java @@ -31,6 +31,8 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.graphics.Rect; +import com.android.internal.annotations.VisibleForTesting; + import java.util.function.Supplier; /** @@ -43,7 +45,8 @@ class AppCompatReachabilityPolicy { @NonNull private final AppCompatConfiguration mAppCompatConfiguration; @Nullable - private Supplier<Rect> mLetterboxInnerBoundsSupplier; + @VisibleForTesting + Supplier<Rect> mLetterboxInnerBoundsSupplier; AppCompatReachabilityPolicy(@NonNull ActivityRecord activityRecord, @NonNull AppCompatConfiguration appCompatConfiguration) { diff --git a/services/core/java/com/android/server/wm/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..b4c75572d68f 100644 --- a/services/core/java/com/android/server/wm/BackNavigationController.java +++ b/services/core/java/com/android/server/wm/BackNavigationController.java @@ -768,6 +768,48 @@ class BackNavigationController { } } + void onAppVisibilityChanged(@NonNull ActivityRecord ar, boolean visible) { + if (!mAnimationHandler.mComposed) { + return; + } + + final boolean openingTransition = mAnimationHandler.mOpenAnimAdaptor + .mPreparedOpenTransition != null; + // Detect if another transition is collecting during predictive back animation. + if (openingTransition && !visible && mAnimationHandler.isTarget(ar, false /* open */) + && ar.mTransitionController.isCollecting(ar)) { + final TransitionController controller = ar.mTransitionController; + boolean collectTask = false; + ActivityRecord changedActivity = null; + for (int i = mAnimationHandler.mOpenActivities.length - 1; i >= 0; --i) { + final ActivityRecord next = mAnimationHandler.mOpenActivities[i]; + if (next.mLaunchTaskBehind) { + // collect previous activity, so shell side can handle the transition. + controller.collect(next); + collectTask = true; + restoreLaunchBehind(next, true /* cancel */, false /* finishTransition */); + changedActivity = next; + } + } + if (collectTask && mAnimationHandler.mOpenAnimAdaptor.mAdaptors[0].mSwitchType + == AnimationHandler.TASK_SWITCH) { + final Task topTask = mAnimationHandler.mOpenAnimAdaptor.mAdaptors[0].getTopTask(); + if (topTask != null) { + WindowContainer parent = mAnimationHandler.mOpenActivities[0].getParent(); + while (parent != topTask && parent.isDescendantOf(topTask)) { + controller.collect(parent); + parent = parent.getParent(); + } + controller.collect(topTask); + } + } + if (changedActivity != null) { + changedActivity.getDisplayContent().ensureActivitiesVisible(null /* starting */, + true /* notifyClients */); + } + } + } + // For shell transition /** * Check whether the transition targets was animated by back gesture animation. @@ -784,7 +826,13 @@ class BackNavigationController { mAnimationHandler.markStartingSurfaceMatch(startTransaction); return; } - if (!isMonitoringFinishTransition() || targets.isEmpty()) { + if (targets.isEmpty()) { + return; + } + final boolean migratePredictToTransition = Flags.migratePredictiveBackTransition(); + if (migratePredictToTransition && !mAnimationHandler.mComposed) { + return; + } else if (!isMonitoringFinishTransition()) { return; } if (mAnimationHandler.hasTargetDetached()) { @@ -808,20 +856,27 @@ class BackNavigationController { mTmpCloseApps.add(wc); } } - final boolean matchAnimationTargets = isWaitBackTransition() + final boolean matchAnimationTargets; + if (migratePredictToTransition) { + matchAnimationTargets = + mAnimationHandler.containsBackAnimationTargets(mTmpOpenApps, mTmpCloseApps); + } else { + matchAnimationTargets = isWaitBackTransition() && (transition.mType == TRANSIT_CLOSE || transition.mType == TRANSIT_TO_BACK) && mAnimationHandler.containsBackAnimationTargets(mTmpOpenApps, mTmpCloseApps); + } ProtoLog.d(WM_DEBUG_BACK_PREVIEW, "onTransactionReady, opening: %s, closing: %s, animating: %s, match: %b", mTmpOpenApps, mTmpCloseApps, mAnimationHandler, matchAnimationTargets); - if (!matchAnimationTargets) { + // Don't cancel transition, let transition handler to handle it + if (!matchAnimationTargets && !migratePredictToTransition) { mNavigationMonitor.onTransitionReadyWhileNavigate(mTmpOpenApps, mTmpCloseApps); } else { if (mAnimationHandler.mPrepareCloseTransition != null) { Slog.e(TAG, "Gesture animation is applied on another transition?"); } mAnimationHandler.mPrepareCloseTransition = transition; - if (!Flags.migratePredictiveBackTransition()) { + if (!migratePredictToTransition) { // Because the target will reparent to transition root, so it cannot be controlled // by animation leash. Hide the close target when transition starts. startTransaction.hide(mAnimationHandler.mCloseAdaptor.mTarget.getSurfaceControl()); @@ -839,7 +894,19 @@ class BackNavigationController { } boolean isMonitorTransitionTarget(WindowContainer wc) { - if ((isWaitBackTransition() && mAnimationHandler.mPrepareCloseTransition != null) + if (Flags.migratePredictiveBackTransition()) { + if (!mAnimationHandler.mComposed) { + return false; + } + if (mAnimationHandler.mSwitchType == AnimationHandler.TASK_SWITCH + && wc.asActivityRecord() != null + || (mAnimationHandler.mSwitchType == AnimationHandler.ACTIVITY_SWITCH + && wc.asTask() != null)) { + return false; + } + return (mAnimationHandler.isTarget(wc, true /* open */) + || mAnimationHandler.isTarget(wc, false /* open */)); + } else if ((isWaitBackTransition() && mAnimationHandler.mPrepareCloseTransition != null) || (mAnimationHandler.mOpenAnimAdaptor != null && mAnimationHandler.mOpenAnimAdaptor.mPreparedOpenTransition != null)) { return mAnimationHandler.isTarget(wc, wc.isVisibleRequested() /* open */); @@ -1840,6 +1907,43 @@ class BackNavigationController { return openActivities; } + boolean restoreBackNavigation() { + if (!mAnimationHandler.mComposed) { + return false; + } + ActivityRecord[] penActivities = mAnimationHandler.mOpenActivities; + boolean changed = false; + if (penActivities != null) { + for (int i = penActivities.length - 1; i >= 0; --i) { + ActivityRecord resetActivity = penActivities[i]; + if (resetActivity.mLaunchTaskBehind) { + resetActivity.mTransitionController.collect(resetActivity); + restoreLaunchBehind(resetActivity, true, false); + changed = true; + } + } + } + return changed; + } + + boolean restoreBackNavigationSetTransitionReady(Transition transition) { + if (!mAnimationHandler.mComposed) { + return false; + } + ActivityRecord[] penActivities = mAnimationHandler.mOpenActivities; + if (penActivities != null) { + for (int i = penActivities.length - 1; i >= 0; --i) { + ActivityRecord resetActivity = penActivities[i]; + if (transition.isInTransition(resetActivity)) { + resetActivity.mTransitionController.setReady( + resetActivity.getDisplayContent(), true); + return true; + } + } + } + return false; + } + private static Transition setLaunchBehind(@NonNull ActivityRecord[] activities) { final boolean migrateBackTransition = Flags.migratePredictiveBackTransition(); final ArrayList<ActivityRecord> affects = new ArrayList<>(); @@ -1919,9 +2023,12 @@ class BackNavigationController { activity); if (cancel) { final boolean migrateBackTransition = Flags.migratePredictiveBackTransition(); - if (migrateBackTransition && finishTransition) { - activity.commitVisibility(false /* visible */, false /* performLayout */, - true /* fromTransition */); + // could be visible if transition is canceled due to top activity is finishing. + if (migrateBackTransition) { + if (finishTransition && !activity.shouldBeVisible()) { + activity.commitVisibility(false /* visible */, false /* performLayout */, + true /* fromTransition */); + } } else { // Restore the launch-behind state // TODO b/347168362 Change status directly during collecting for a transition. @@ -2015,7 +2122,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 +2133,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/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/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/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/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java index 8e3248eaa6bf..f86d307d97bd 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java @@ -708,17 +708,15 @@ final class PolicyDefinition<V> { } @Nullable - static <V> PolicyKey readPolicyKeyFromXml(TypedXmlPullParser parser) + static PolicyKey readPolicyKeyFromXml(TypedXmlPullParser parser) throws XmlPullParserException, IOException { - // TODO: can we avoid casting? PolicyKey policyKey = PolicyKey.readGenericPolicyKeyFromXml(parser); if (policyKey == null) { Slogf.wtf(TAG, "Error parsing PolicyKey, GenericPolicyKey is null"); return null; } - PolicyDefinition<PolicyValue<V>> genericPolicyDefinition = - (PolicyDefinition<PolicyValue<V>>) POLICY_DEFINITIONS.get( - policyKey.getIdentifier()); + PolicyDefinition<?> genericPolicyDefinition = + POLICY_DEFINITIONS.get(policyKey.getIdentifier()); if (genericPolicyDefinition == null) { Slogf.wtf(TAG, "Error parsing PolicyKey, Unknown generic policy key: " + policyKey); return null; diff --git a/services/tests/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/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/ZenModeConfigTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java index b955a795e94a..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()); } @@ -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 63cf1068f51e..776a840466c8 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java @@ -1495,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(); 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/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/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/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/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/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/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/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/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/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. |