diff options
520 files changed, 17515 insertions, 5078 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index a16aa2dea25b..d62cba910673 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -403,17 +403,6 @@ java_aconfig_library { cc_aconfig_library { name: "android.companion.virtualdevice.flags-aconfig-cc", aconfig_declarations: "android.companion.virtualdevice.flags-aconfig", -} - -cc_aconfig_library { - name: "android.companion.virtualdevice.flags-aconfig-cc-host", - aconfig_declarations: "android.companion.virtualdevice.flags-aconfig", - host_supported: true, -} - -cc_aconfig_library { - name: "android.companion.virtualdevice.flags-aconfig-cc-test", - aconfig_declarations: "android.companion.virtualdevice.flags-aconfig", host_supported: true, mode: "test", } @@ -1490,6 +1479,13 @@ java_aconfig_library { defaults: ["framework-minus-apex-aconfig-java-defaults"], } +java_aconfig_library { + name: "backstage_power_flags_lib-host", + aconfig_declarations: "backstage_power_flags", + host_supported: true, + defaults: ["framework-minus-apex-aconfig-java-defaults"], +} + // Dropbox data aconfig_declarations { name: "dropbox_flags", diff --git a/apct-tests/perftests/packagemanager/src/android/os/PackageManagerPerfTest.java b/apct-tests/perftests/packagemanager/src/android/os/PackageManagerPerfTest.java index 4bcc8c499f0d..f302033dee0f 100644 --- a/apct-tests/perftests/packagemanager/src/android/os/PackageManagerPerfTest.java +++ b/apct-tests/perftests/packagemanager/src/android/os/PackageManagerPerfTest.java @@ -31,6 +31,7 @@ import android.content.pm.PackageInstaller; import android.content.pm.PackageManager; import android.perftests.utils.BenchmarkState; import android.perftests.utils.PerfStatusReporter; +import android.permission.PermissionManager; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.LargeTest; @@ -107,6 +108,8 @@ public class PackageManagerPerfTest { public void setup() { PackageManager.disableApplicationInfoCache(); PackageManager.disablePackageInfoCache(); + PermissionManager.disablePermissionCache(); + PermissionManager.disablePackageNamePermissionCache(); } @Test diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/UserWakeupStore.java b/apex/jobscheduler/service/java/com/android/server/alarm/UserWakeupStore.java index dc5e3414a819..93904a773ed5 100644 --- a/apex/jobscheduler/service/java/com/android/server/alarm/UserWakeupStore.java +++ b/apex/jobscheduler/service/java/com/android/server/alarm/UserWakeupStore.java @@ -20,6 +20,7 @@ package com.android.server.alarm; import android.annotation.Nullable; import android.os.Environment; import android.os.SystemClock; +import android.os.UserHandle; import android.util.AtomicFile; import android.util.IndentingPrintWriter; import android.util.Pair; @@ -113,15 +114,18 @@ public class UserWakeupStore { } /** - * Add user wakeup for the alarm. + * Add user wakeup for the alarm if needed. * @param userId Id of the user that scheduled alarm. * @param alarmTime time when alarm is expected to trigger. */ public void addUserWakeup(int userId, long alarmTime) { - synchronized (mUserWakeupLock) { - mUserStarts.put(userId, alarmTime - BUFFER_TIME_MS + getUserWakeupOffset()); + // SYSTEM user is always running, so no need to schedule wakeup for it. + if (userId != UserHandle.USER_SYSTEM) { + synchronized (mUserWakeupLock) { + mUserStarts.put(userId, alarmTime - BUFFER_TIME_MS + getUserWakeupOffset()); + } + updateUserListFile(); } - updateUserListFile(); } /** diff --git a/api/Android.bp b/api/Android.bp index 6a04f0d76bf9..89a0c186651a 100644 --- a/api/Android.bp +++ b/api/Android.bp @@ -29,12 +29,14 @@ bootstrap_go_package { pkgPath: "android/soong/api", deps: [ "blueprint", + "blueprint-proptools", "soong", "soong-android", "soong-genrule", "soong-java", ], srcs: ["api.go"], + testSrcs: ["api_test.go"], pluginFor: ["soong_build"], } diff --git a/api/StubLibraries.bp b/api/StubLibraries.bp index 12820f9ff277..8dfddf0e13c8 100644 --- a/api/StubLibraries.bp +++ b/api/StubLibraries.bp @@ -1345,4 +1345,5 @@ java_library { ":hwbinder-stubs-docs", ], visibility: ["//visibility:public"], + is_stubs_module: true, } diff --git a/api/api.go b/api/api.go index d4db49e90a01..f0d1f42f61d4 100644 --- a/api/api.go +++ b/api/api.go @@ -54,11 +54,11 @@ var non_updatable_modules = []string{virtualization, location} // The properties of the combined_apis module type. type CombinedApisProperties struct { // Module libraries in the bootclasspath - Bootclasspath []string + Bootclasspath proptools.Configurable[[]string] // Module libraries on the bootclasspath if include_nonpublic_framework_api is true. Conditional_bootclasspath []string // Module libraries in system server - System_server_classpath []string + System_server_classpath proptools.Configurable[[]string] } type CombinedApis struct { @@ -79,29 +79,37 @@ func registerBuildComponents(ctx android.RegistrationContext) { var PrepareForCombinedApisTest = android.FixtureRegisterWithContext(registerBuildComponents) -func (a *CombinedApis) apiFingerprintStubDeps() []string { +func (a *CombinedApis) bootclasspath(ctx android.ConfigAndErrorContext) []string { + return a.properties.Bootclasspath.GetOrDefault(a.ConfigurableEvaluator(ctx), nil) +} + +func (a *CombinedApis) systemServerClasspath(ctx android.ConfigAndErrorContext) []string { + return a.properties.System_server_classpath.GetOrDefault(a.ConfigurableEvaluator(ctx), nil) +} + +func (a *CombinedApis) apiFingerprintStubDeps(ctx android.BottomUpMutatorContext) []string { ret := []string{} ret = append( ret, - transformArray(a.properties.Bootclasspath, "", ".stubs")..., + transformArray(a.bootclasspath(ctx), "", ".stubs")..., ) ret = append( ret, - transformArray(a.properties.Bootclasspath, "", ".stubs.system")..., + transformArray(a.bootclasspath(ctx), "", ".stubs.system")..., ) ret = append( ret, - transformArray(a.properties.Bootclasspath, "", ".stubs.module_lib")..., + transformArray(a.bootclasspath(ctx), "", ".stubs.module_lib")..., ) ret = append( ret, - transformArray(a.properties.System_server_classpath, "", ".stubs.system_server")..., + transformArray(a.systemServerClasspath(ctx), "", ".stubs.system_server")..., ) return ret } func (a *CombinedApis) DepsMutator(ctx android.BottomUpMutatorContext) { - ctx.AddDependency(ctx.Module(), nil, a.apiFingerprintStubDeps()...) + ctx.AddDependency(ctx.Module(), nil, a.apiFingerprintStubDeps(ctx)...) } func (a *CombinedApis) GenerateAndroidBuildActions(ctx android.ModuleContext) { @@ -532,8 +540,8 @@ func createFullExportableApiLibraries(ctx android.LoadHookContext) { } func (a *CombinedApis) createInternalModules(ctx android.LoadHookContext) { - bootclasspath := a.properties.Bootclasspath - system_server_classpath := a.properties.System_server_classpath + bootclasspath := a.bootclasspath(ctx) + system_server_classpath := a.systemServerClasspath(ctx) if ctx.Config().VendorConfig("ANDROID").Bool("include_nonpublic_framework_api") { bootclasspath = append(bootclasspath, a.properties.Conditional_bootclasspath...) sort.Strings(bootclasspath) diff --git a/api/api_test.go b/api/api_test.go new file mode 100644 index 000000000000..47d167093b39 --- /dev/null +++ b/api/api_test.go @@ -0,0 +1,254 @@ +// 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 api + +import ( + "android/soong/android" + "android/soong/java" + "fmt" + "testing" + + "github.com/google/blueprint/proptools" +) + +var prepareForTestWithCombinedApis = android.GroupFixturePreparers( + android.FixtureRegisterWithContext(registerBuildComponents), + java.PrepareForTestWithJavaBuildComponents, + android.FixtureAddTextFile("a/Android.bp", gatherRequiredDepsForTest()), + java.PrepareForTestWithJavaSdkLibraryFiles, + android.FixtureMergeMockFs(android.MockFS{ + "a/api/current.txt": nil, + "a/api/removed.txt": nil, + "a/api/system-current.txt": nil, + "a/api/system-removed.txt": nil, + "a/api/test-current.txt": nil, + "a/api/test-removed.txt": nil, + "a/api/module-lib-current.txt": nil, + "a/api/module-lib-removed.txt": nil, + }), + android.FixtureModifyProductVariables(func(variables android.FixtureProductVariables) { + variables.Allow_missing_dependencies = proptools.BoolPtr(true) + }), +) + +func gatherRequiredDepsForTest() string { + var bp string + + extraLibraryModules := []string{ + "stable.core.platform.api.stubs", + "core-lambda-stubs", + "core.current.stubs", + "ext", + "framework", + "android_stubs_current.from-text", + "android_system_stubs_current.from-text", + "android_test_stubs_current.from-text", + "android_test_frameworks_core_stubs_current.from-text", + "android_module_lib_stubs_current.from-text", + "android_system_server_stubs_current.from-text", + "android_stubs_current.from-source", + "android_system_stubs_current.from-source", + "android_test_stubs_current.from-source", + "android_test_frameworks_core_stubs_current.from-source", + "android_module_lib_stubs_current.from-source", + "android_system_server_stubs_current.from-source", + "android_stubs_current_exportable.from-source", + "android_system_stubs_current_exportable.from-source", + "android_test_stubs_current_exportable.from-source", + "android_module_lib_stubs_current_exportable.from-source", + "android_system_server_stubs_current_exportable.from-source", + "stub-annotations", + } + + extraSdkLibraryModules := []string{ + "framework-virtualization", + "framework-location", + } + + extraSystemModules := []string{ + "core-public-stubs-system-modules", + "core-module-lib-stubs-system-modules", + "stable-core-platform-api-stubs-system-modules", + } + + extraFilegroupModules := []string{ + "non-updatable-current.txt", + "non-updatable-removed.txt", + "non-updatable-system-current.txt", + "non-updatable-system-removed.txt", + "non-updatable-test-current.txt", + "non-updatable-test-removed.txt", + "non-updatable-module-lib-current.txt", + "non-updatable-module-lib-removed.txt", + "non-updatable-system-server-current.txt", + "non-updatable-system-server-removed.txt", + "non-updatable-exportable-current.txt", + "non-updatable-exportable-removed.txt", + "non-updatable-exportable-system-current.txt", + "non-updatable-exportable-system-removed.txt", + "non-updatable-exportable-test-current.txt", + "non-updatable-exportable-test-removed.txt", + "non-updatable-exportable-module-lib-current.txt", + "non-updatable-exportable-module-lib-removed.txt", + "non-updatable-exportable-system-server-current.txt", + "non-updatable-exportable-system-server-removed.txt", + } + + for _, extra := range extraLibraryModules { + bp += fmt.Sprintf(` + java_library { + name: "%s", + srcs: ["a.java"], + sdk_version: "none", + system_modules: "stable-core-platform-api-stubs-system-modules", + compile_dex: true, + } + `, extra) + } + + for _, extra := range extraSdkLibraryModules { + bp += fmt.Sprintf(` + java_sdk_library { + name: "%s", + srcs: ["a.java"], + public: { + enabled: true, + }, + system: { + enabled: true, + }, + test: { + enabled: true, + }, + module_lib: { + enabled: true, + }, + api_packages: [ + "foo", + ], + sdk_version: "core_current", + compile_dex: true, + annotations_enabled: true, + } + `, extra) + } + + for _, extra := range extraFilegroupModules { + bp += fmt.Sprintf(` + filegroup { + name: "%[1]s", + } + `, extra) + } + + for _, extra := range extraSystemModules { + bp += fmt.Sprintf(` + java_system_modules { + name: "%[1]s", + libs: ["%[1]s-lib"], + } + java_library { + name: "%[1]s-lib", + sdk_version: "none", + system_modules: "none", + } + `, extra) + } + + bp += fmt.Sprintf(` + java_defaults { + name: "android.jar_defaults", + } + `) + + return bp +} + +func TestCombinedApisDefaults(t *testing.T) { + + result := android.GroupFixturePreparers( + prepareForTestWithCombinedApis, + java.FixtureWithLastReleaseApis( + "framework-location", "framework-virtualization", "framework-foo", "framework-bar"), + android.FixtureModifyProductVariables(func(variables android.FixtureProductVariables) { + variables.VendorVars = map[string]map[string]string{ + "boolean_var": { + "for_testing": "true", + }, + } + }), + ).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-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, + } + + combined_apis { + name: "foo", + bootclasspath: [ + "framework-bar", + ] + select(boolean_var_for_testing(), { + true: [ + "framework-foo", + ], + default: [], + }), + } + `) + + subModuleDependsOnSelectAppendedModule := java.CheckModuleHasDependency(t, + result.TestContext, "foo-current.txt", "", "framework-foo") + android.AssertBoolEquals(t, "Submodule expected to depend on the select-appended module", + true, subModuleDependsOnSelectAppendedModule) +} diff --git a/api/go.work b/api/go.work index edd002e7efba..c09bee578b61 100644 --- a/api/go.work +++ b/api/go.work @@ -1,17 +1,17 @@ -go 1.18 +go 1.22 use ( . - ../../../build/soong ../../../build/blueprint + ../../../build/soong ../../../external/go-cmp ../../../external/golang-protobuf ) replace ( android/soong v0.0.0 => ../../../build/soong - google.golang.org/protobuf v0.0.0 => ../../../external/golang-protobuf github.com/google/blueprint v0.0.0 => ../../../build/blueprint github.com/google/go-cmp v0.0.0 => ../../../external/go-cmp go.starlark.net v0.0.0 => ../../../external/starlark-go + google.golang.org/protobuf v0.0.0 => ../../../external/golang-protobuf ) diff --git a/core/api/current.txt b/core/api/current.txt index 0f237212c768..69c409bb5261 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -26569,7 +26569,7 @@ package android.media.midi { package android.media.projection { public final class MediaProjection { - method public android.hardware.display.VirtualDisplay createVirtualDisplay(@NonNull String, int, int, int, int, @Nullable android.view.Surface, @Nullable android.hardware.display.VirtualDisplay.Callback, @Nullable android.os.Handler); + method @Nullable public android.hardware.display.VirtualDisplay createVirtualDisplay(@NonNull String, int, int, int, int, @Nullable android.view.Surface, @Nullable android.hardware.display.VirtualDisplay.Callback, @Nullable android.os.Handler); method public void registerCallback(@NonNull android.media.projection.MediaProjection.Callback, @Nullable android.os.Handler); method public void stop(); method public void unregisterCallback(@NonNull android.media.projection.MediaProjection.Callback); @@ -54862,6 +54862,8 @@ package android.view.accessibility { method @Deprecated public void addAction(int); method public void addChild(android.view.View); method public void addChild(android.view.View, int); + method @FlaggedApi("android.view.accessibility.support_multiple_labeledby") public void addLabeledBy(@NonNull android.view.View); + method @FlaggedApi("android.view.accessibility.support_multiple_labeledby") public void addLabeledBy(@NonNull android.view.View, int); method public boolean canOpenPopup(); method public int describeContents(); method public java.util.List<android.view.accessibility.AccessibilityNodeInfo> findAccessibilityNodeInfosByText(String); @@ -54890,6 +54892,7 @@ package android.view.accessibility { method public int getInputType(); method public android.view.accessibility.AccessibilityNodeInfo getLabelFor(); method public android.view.accessibility.AccessibilityNodeInfo getLabeledBy(); + method @FlaggedApi("android.view.accessibility.support_multiple_labeledby") @NonNull public java.util.List<android.view.accessibility.AccessibilityNodeInfo> getLabeledByList(); method public int getLiveRegion(); method public int getMaxTextLength(); method @NonNull public java.time.Duration getMinDurationBetweenContentChanges(); @@ -54950,6 +54953,8 @@ package android.view.accessibility { method public boolean removeAction(android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction); method public boolean removeChild(android.view.View); method public boolean removeChild(android.view.View, int); + method @FlaggedApi("android.view.accessibility.support_multiple_labeledby") public boolean removeLabeledBy(@NonNull android.view.View); + method @FlaggedApi("android.view.accessibility.support_multiple_labeledby") public boolean removeLabeledBy(@NonNull android.view.View, int); method public void setAccessibilityDataSensitive(boolean); method public void setAccessibilityFocused(boolean); method public void setAvailableExtraData(java.util.List<java.lang.String>); diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 44c4ab4e1b57..88b5275d37f8 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -423,6 +423,7 @@ package android.app { public final class PictureInPictureParams implements android.os.Parcelable { method public float getAspectRatioFloat(); method public float getExpandedAspectRatioFloat(); + method public static boolean isSameAspectRatio(@NonNull android.graphics.Rect, @NonNull android.util.Rational); } public final class PictureInPictureUiState implements android.os.Parcelable { @@ -618,6 +619,7 @@ package android.app.admin { method @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) public void resetDefaultCrossProfileIntentFilters(int); method @RequiresPermission(android.Manifest.permission.MANAGE_ROLE_HOLDERS) public void resetShouldAllowBypassingDevicePolicyManagementRoleQualificationState(); method @RequiresPermission(allOf={android.Manifest.permission.MANAGE_DEVICE_ADMINS, android.Manifest.permission.INTERACT_ACROSS_USERS_FULL}) public void setActiveAdmin(@NonNull android.content.ComponentName, boolean, int); + method @FlaggedApi("android.app.admin.flags.provisioning_context_parameter") @RequiresPermission(allOf={android.Manifest.permission.MANAGE_DEVICE_ADMINS, android.Manifest.permission.INTERACT_ACROSS_USERS_FULL}) public void setActiveAdmin(@NonNull android.content.ComponentName, boolean, int, @Nullable String); method @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) public boolean setDeviceOwner(@NonNull android.content.ComponentName, int); method @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) public boolean setDeviceOwnerOnly(@NonNull android.content.ComponentName, int); method public void setDeviceOwnerType(@NonNull android.content.ComponentName, int); diff --git a/core/java/android/animation/OWNERS b/core/java/android/animation/OWNERS index f3b330a02116..5223c870824c 100644 --- a/core/java/android/animation/OWNERS +++ b/core/java/android/animation/OWNERS @@ -3,3 +3,4 @@ romainguy@google.com tianliu@google.com adamp@google.com +mount@google.com diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java index c6a1546fb931..65acd49d44fa 100644 --- a/core/java/android/app/ActivityOptions.java +++ b/core/java/android/app/ActivityOptions.java @@ -104,7 +104,9 @@ public class ActivityOptions extends ComponentOptions { MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED, MODE_BACKGROUND_ACTIVITY_START_ALLOWED, MODE_BACKGROUND_ACTIVITY_START_DENIED, - MODE_BACKGROUND_ACTIVITY_START_COMPAT}) + MODE_BACKGROUND_ACTIVITY_START_COMPAT, + MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS, + MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE}) public @interface BackgroundActivityStartMode {} /** * No explicit value chosen. The system will decide whether to grant privileges. @@ -119,6 +121,20 @@ public class ActivityOptions extends ComponentOptions { */ public static final int MODE_BACKGROUND_ACTIVITY_START_DENIED = 2; /** + * Allow the {@link PendingIntent} to use ALL background activity start privileges, including + * special permissions that will allow starts at any time. + * + * @hide + */ + public static final int MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS = 3; + /** + * Allow the {@link PendingIntent} to use background activity start privileges based on + * visibility of the app. + * + * @hide + */ + public static final int MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE = 4; + /** * Special behavior for compatibility. * Similar to {@link #MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED} * diff --git a/core/java/android/app/ComponentOptions.java b/core/java/android/app/ComponentOptions.java index 0e8e2e30c26f..b3fc0588022b 100644 --- a/core/java/android/app/ComponentOptions.java +++ b/core/java/android/app/ComponentOptions.java @@ -18,9 +18,11 @@ package android.app; import static android.app.ActivityOptions.BackgroundActivityStartMode; import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS; import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_COMPAT; import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED; import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE; import android.annotation.NonNull; import android.annotation.Nullable; @@ -48,15 +50,7 @@ public class ComponentOptions { public static final String KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED = "android.pendingIntent.backgroundActivityAllowed"; - /** - * PendingIntent caller allows activity to be started if caller has BAL permission. - * @hide - */ - public static final String KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION = - "android.pendingIntent.backgroundActivityAllowedByPermission"; - private Integer mPendingIntentBalAllowed = MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED; - private boolean mPendingIntentBalAllowedByPermission = false; ComponentOptions() { } @@ -69,9 +63,6 @@ public class ComponentOptions { mPendingIntentBalAllowed = opts.getInt(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED, MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED); - setPendingIntentBackgroundActivityLaunchAllowedByPermission( - opts.getBoolean( - KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION, false)); } /** @@ -114,10 +105,19 @@ public class ComponentOptions { public @NonNull ComponentOptions setPendingIntentBackgroundActivityStartMode( @BackgroundActivityStartMode int state) { switch (state) { + case MODE_BACKGROUND_ACTIVITY_START_ALLOWED: + if (mPendingIntentBalAllowed != MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS) { + // do not overwrite ALWAYS with ALLOWED for backwards compatibility, + // if setPendingIntentBackgroundActivityLaunchAllowedByPermission is used + // before this method. + mPendingIntentBalAllowed = state; + } + break; case MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED: case MODE_BACKGROUND_ACTIVITY_START_DENIED: case MODE_BACKGROUND_ACTIVITY_START_COMPAT: - case MODE_BACKGROUND_ACTIVITY_START_ALLOWED: + case MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS: + case MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE: mPendingIntentBalAllowed = state; break; default: @@ -140,20 +140,32 @@ public class ComponentOptions { } /** - * Set PendingIntent activity can be launched from background if caller has BAL permission. + * Get PendingIntent activity is allowed to be started in the background if the caller + * has BAL permission. * @hide + * @deprecated check for #MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS */ - public void setPendingIntentBackgroundActivityLaunchAllowedByPermission(boolean allowed) { - mPendingIntentBalAllowedByPermission = allowed; + @Deprecated + public boolean isPendingIntentBackgroundActivityLaunchAllowedByPermission() { + return mPendingIntentBalAllowed == MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS; } /** - * Get PendingIntent activity is allowed to be started in the background if the caller - * has BAL permission. + * Set PendingIntent activity can be launched from background if caller has BAL permission. * @hide + * @deprecated use #MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS */ - public boolean isPendingIntentBackgroundActivityLaunchAllowedByPermission() { - return mPendingIntentBalAllowedByPermission; + @Deprecated + public void setPendingIntentBackgroundActivityLaunchAllowedByPermission(boolean allowed) { + if (allowed) { + setPendingIntentBackgroundActivityStartMode( + MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS); + } else { + if (getPendingIntentBackgroundActivityStartMode() + == MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS) { + setPendingIntentBackgroundActivityStartMode(MODE_BACKGROUND_ACTIVITY_START_ALLOWED); + } + } } /** @hide */ @@ -162,10 +174,6 @@ public class ComponentOptions { if (mPendingIntentBalAllowed != MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED) { b.putInt(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED, mPendingIntentBalAllowed); } - if (mPendingIntentBalAllowedByPermission) { - b.putBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION, - mPendingIntentBalAllowedByPermission); - } return b; } diff --git a/core/java/android/app/OWNERS b/core/java/android/app/OWNERS index 0fad979e27cf..1200b4b45712 100644 --- a/core/java/android/app/OWNERS +++ b/core/java/android/app/OWNERS @@ -118,6 +118,8 @@ per-file *Task* = file:/services/core/java/com/android/server/wm/OWNERS per-file Window* = file:/services/core/java/com/android/server/wm/OWNERS per-file ConfigurationController.java = file:/services/core/java/com/android/server/wm/OWNERS per-file *ScreenCapture* = file:/services/core/java/com/android/server/wm/OWNERS +per-file ComponentOptions.java = file:/services/core/java/com/android/server/wm/OWNERS + # Multitasking per-file multitasking.aconfig = file:/services/core/java/com/android/server/wm/OWNERS diff --git a/core/java/android/app/PictureInPictureParams.java b/core/java/android/app/PictureInPictureParams.java index 96d874ee110b..afe915eece26 100644 --- a/core/java/android/app/PictureInPictureParams.java +++ b/core/java/android/app/PictureInPictureParams.java @@ -654,6 +654,33 @@ public final class PictureInPictureParams implements Parcelable { && !hasSetSubtitle() && mIsLaunchIntoPip == null; } + /** + * Compare a given {@link Rect} against the aspect ratio, with rounding error tolerance. + * @param bounds The {@link Rect} represents the source rect hint, this check is not needed + * if app provides a null source rect hint. + * @param aspectRatio {@link Rational} representation of aspect ratio, this check is not needed + * if app provides a null aspect ratio. + * @return {@code true} if the given {@link Rect} matches the aspect ratio. + * @hide + */ + @SuppressWarnings("UnflaggedApi") + @TestApi + public static boolean isSameAspectRatio(@NonNull Rect bounds, @NonNull Rational aspectRatio) { + // Validations + if (bounds.isEmpty() || aspectRatio.floatValue() <= 0) { + return false; + } + // Check against both the width and height. + final int exactWidth = (aspectRatio.getNumerator() * bounds.height()) + / aspectRatio.getDenominator(); + if (Math.abs(exactWidth - bounds.width()) <= 1) { + return true; + } + final int exactHeight = (aspectRatio.getDenominator() * bounds.width()) + / aspectRatio.getNumerator(); + return Math.abs(exactHeight - bounds.height()) <= 1; + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index fb0ce0d2d077..a7070b910f17 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -9202,6 +9202,14 @@ public class DevicePolicyManager { /** * @hide */ + @UnsupportedAppUsage + public void setActiveAdmin(@NonNull ComponentName policyReceiver, boolean refreshing) { + setActiveAdmin(policyReceiver, refreshing, myUserId()); + } + + /** + * @hide + */ @TestApi @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) @RequiresPermission(allOf = { @@ -9210,21 +9218,45 @@ public class DevicePolicyManager { }) public void setActiveAdmin(@NonNull ComponentName policyReceiver, boolean refreshing, int userHandle) { - if (mService != null) { - try { - mService.setActiveAdmin(policyReceiver, refreshing, userHandle); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } + setActiveAdminInternal(policyReceiver, refreshing, userHandle, null); } /** * @hide */ - @UnsupportedAppUsage - public void setActiveAdmin(@NonNull ComponentName policyReceiver, boolean refreshing) { - setActiveAdmin(policyReceiver, refreshing, myUserId()); + @TestApi + @RequiresPermission(allOf = { + MANAGE_DEVICE_ADMINS, + INTERACT_ACROSS_USERS_FULL + }) + @FlaggedApi(Flags.FLAG_PROVISIONING_CONTEXT_PARAMETER) + public void setActiveAdmin( + @NonNull ComponentName policyReceiver, + boolean refreshing, + int userHandle, + @Nullable String provisioningContext + ) { + setActiveAdminInternal(policyReceiver, refreshing, userHandle, provisioningContext); + } + + private void setActiveAdminInternal( + @NonNull ComponentName policyReceiver, + boolean refreshing, + int userHandle, + @Nullable String provisioningContext + ) { + if (mService != null) { + try { + mService.setActiveAdmin( + policyReceiver, + refreshing, + userHandle, + provisioningContext + ); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } } /** @@ -9678,7 +9710,7 @@ public class DevicePolicyManager { if (mService != null) { try { final int myUserId = myUserId(); - mService.setActiveAdmin(admin, false, myUserId); + mService.setActiveAdmin(admin, false, myUserId, null); return mService.setProfileOwner(admin, myUserId); } catch (RemoteException re) { throw re.rethrowFromSystemServer(); diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl index d1837132e1a4..381f9963789b 100644 --- a/core/java/android/app/admin/IDevicePolicyManager.aidl +++ b/core/java/android/app/admin/IDevicePolicyManager.aidl @@ -160,7 +160,8 @@ interface IDevicePolicyManager { void setKeyguardDisabledFeatures(in ComponentName who, String callerPackageName, int which, boolean parent); int getKeyguardDisabledFeatures(in ComponentName who, int userHandle, boolean parent); - void setActiveAdmin(in ComponentName policyReceiver, boolean refreshing, int userHandle); + void setActiveAdmin(in ComponentName policyReceiver, boolean refreshing, + int userHandle, String provisioningContext); boolean isAdminActive(in ComponentName policyReceiver, int userHandle); List<ComponentName> getActiveAdmins(int userHandle); @UnsupportedAppUsage diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig index 82271129c69e..c789af32e2b1 100644 --- a/core/java/android/app/admin/flags/flags.aconfig +++ b/core/java/android/app/admin/flags/flags.aconfig @@ -393,3 +393,13 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "provisioning_context_parameter" + namespace: "enterprise" + description: "Add provisioningContext to store metadata about when the admin was set" + bug: "326525847" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index 97404dcdea0c..111e6a8e93ef 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -5277,12 +5277,28 @@ public class Intent implements Parcelable, Cloneable { * through {@link #getData()}. User interaction is required to return the edited screenshot to * the calling activity. * + * <p>The response {@link Intent} may include additional data to "backlink" directly back to the + * application for which the screenshot was captured. If present, the application "backlink" can + * be retrieved via {@link #getClipData()}. The data is present only if the user accepted to + * include the link information with the screenshot. The data can contain one of the following: + * <ul> + * <li>A deeplinking {@link Uri} or an {@link Intent} if the captured app integrates with + * {@link android.app.assist.AssistContent}.</li> + * <li>Otherwise, a main launcher intent that launches the screenshotted application to + * its home screen.</li> + * </ul> + * The "backlink" to the screenshotted application will be set within {@link ClipData}, either + * as a {@link Uri} or an {@link Intent} if present. + * * <p>This intent action requires the permission * {@link android.Manifest.permission#LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE}. * * <p>Callers should query * {@link StatusBarManager#canLaunchCaptureContentActivityForNote(Activity)} before showing a UI * element that allows users to trigger this flow. + * + * <p>Callers should query for {@link #EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE} in the + * response {@link Intent} to check if the request was a success. */ @RequiresPermission(Manifest.permission.LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE) @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig index c2c7b81871df..5a3970295ac2 100644 --- a/core/java/android/content/pm/multiuser.aconfig +++ b/core/java/android/content/pm/multiuser.aconfig @@ -327,3 +327,13 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "fix_large_display_private_space_settings" + namespace: "profile_experiences" + description: "Fix tablet and foldable specific bugs for private space" + bug: "342563741" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/android/hardware/usb/flags/system_sw_usb_flags.aconfig b/core/java/android/hardware/usb/flags/system_sw_usb_flags.aconfig index 0f944cfb7f67..51024ba64bd9 100644 --- a/core/java/android/hardware/usb/flags/system_sw_usb_flags.aconfig +++ b/core/java/android/hardware/usb/flags/system_sw_usb_flags.aconfig @@ -36,3 +36,10 @@ flag { description: "Enable identifying midi device using USB sysfs" bug: "333778731" } + +flag { + name: "enable_udc_sysfs_usb_state_update" + namespace: "system_sw_usb" + description: "Enable usb state update based on udc sysfs" + bug: "339241080" +} diff --git a/core/java/android/inputmethodservice/navigationbar/NavigationBarView.java b/core/java/android/inputmethodservice/navigationbar/NavigationBarView.java index a3beaf427226..209f323d7b34 100644 --- a/core/java/android/inputmethodservice/navigationbar/NavigationBarView.java +++ b/core/java/android/inputmethodservice/navigationbar/NavigationBarView.java @@ -216,7 +216,11 @@ public final class NavigationBarView extends FrameLayout { oldConfig.getLayoutDirection() != mConfiguration.getLayoutDirection(); if (densityChange || dirChange) { - mImeSwitcherIcon = getDrawable(com.android.internal.R.drawable.ic_ime_switcher); + final int switcherResId = Flags.imeSwitcherRevamp() + ? com.android.internal.R.drawable.ic_ime_switcher_new + : com.android.internal.R.drawable.ic_ime_switcher; + + mImeSwitcherIcon = getDrawable(switcherResId); } if (orientationChange || densityChange || dirChange) { mBackIcon = getBackDrawable(); diff --git a/core/java/android/os/AggregateBatteryConsumer.java b/core/java/android/os/AggregateBatteryConsumer.java index 67e21957839a..c7f8878f104e 100644 --- a/core/java/android/os/AggregateBatteryConsumer.java +++ b/core/java/android/os/AggregateBatteryConsumer.java @@ -55,7 +55,7 @@ public final class AggregateBatteryConsumer extends BatteryConsumer { @Override public void dump(PrintWriter pw, boolean skipEmptyComponents) { - mPowerComponents.dump(pw, skipEmptyComponents); + mPowerComponents.dump(pw, SCREEN_STATE_ANY, POWER_STATE_ANY, skipEmptyComponents); } @Override diff --git a/core/java/android/os/BatteryConsumer.java b/core/java/android/os/BatteryConsumer.java index 744f6a8d61ac..2447ff93fdbc 100644 --- a/core/java/android/os/BatteryConsumer.java +++ b/core/java/android/os/BatteryConsumer.java @@ -19,14 +19,19 @@ package android.os; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.database.Cursor; import android.database.CursorWindow; +import android.util.IntArray; import android.util.Slog; +import android.util.SparseArray; import android.util.proto.ProtoOutputStream; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; /** * Interface for objects containing battery attribution data. @@ -192,31 +197,106 @@ public abstract class BatteryConsumer { sProcessStateNames[PROCESS_STATE_CACHED] = "cached"; } - private static final int[] SUPPORTED_POWER_COMPONENTS_PER_PROCESS_STATE = { - POWER_COMPONENT_CPU, - POWER_COMPONENT_MOBILE_RADIO, - POWER_COMPONENT_WIFI, - POWER_COMPONENT_BLUETOOTH, - POWER_COMPONENT_AUDIO, - POWER_COMPONENT_VIDEO, - POWER_COMPONENT_FLASHLIGHT, - POWER_COMPONENT_CAMERA, - POWER_COMPONENT_GNSS, + private static final IntArray SUPPORTED_POWER_COMPONENTS_PER_PROCESS_STATE; + static { + int[] supportedPowerComponents = { + POWER_COMPONENT_CPU, + POWER_COMPONENT_MOBILE_RADIO, + POWER_COMPONENT_WIFI, + POWER_COMPONENT_BLUETOOTH, + POWER_COMPONENT_AUDIO, + POWER_COMPONENT_VIDEO, + POWER_COMPONENT_FLASHLIGHT, + POWER_COMPONENT_CAMERA, + POWER_COMPONENT_GNSS}; + Arrays.sort(supportedPowerComponents); + SUPPORTED_POWER_COMPONENTS_PER_PROCESS_STATE = IntArray.wrap(supportedPowerComponents); }; static final int COLUMN_INDEX_BATTERY_CONSUMER_TYPE = 0; static final int COLUMN_COUNT = 1; /** + * Identifiers of consumed power aggregations per SCREEN state. + * + * @hide + */ + @IntDef(prefix = {"SCREEN_STATE_"}, value = { + SCREEN_STATE_UNSPECIFIED, + SCREEN_STATE_ANY, + SCREEN_STATE_ON, + SCREEN_STATE_OTHER, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ScreenState { + } + + public static final int SCREEN_STATE_UNSPECIFIED = 0; + public static final int SCREEN_STATE_ANY = SCREEN_STATE_UNSPECIFIED; + public static final int SCREEN_STATE_ON = 1; + public static final int SCREEN_STATE_OTHER = 2; // Off, doze etc + + public static final int SCREEN_STATE_COUNT = 3; + + private static final String[] sScreenStateNames = new String[SCREEN_STATE_COUNT]; + + static { + // Assign individually to avoid future mismatch + sScreenStateNames[SCREEN_STATE_UNSPECIFIED] = "unspecified"; + sScreenStateNames[SCREEN_STATE_ON] = "on"; + sScreenStateNames[SCREEN_STATE_OTHER] = "off/doze"; + } + + /** + * Identifiers of consumed power aggregations per POWER state. + * + * @hide + */ + @IntDef(prefix = {"POWER_STATE_"}, value = { + POWER_STATE_UNSPECIFIED, + POWER_STATE_ANY, + POWER_STATE_BATTERY, + POWER_STATE_OTHER, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface PowerState { + } + + public static final int POWER_STATE_UNSPECIFIED = 0; + public static final int POWER_STATE_ANY = POWER_STATE_UNSPECIFIED; + public static final int POWER_STATE_BATTERY = 1; + public static final int POWER_STATE_OTHER = 2; // Plugged in, or on wireless charger, etc. + + public static final int POWER_STATE_COUNT = 3; + + private static final String[] sPowerStateNames = new String[POWER_STATE_COUNT]; + + static { + // Assign individually to avoid future mismatch + sPowerStateNames[POWER_STATE_UNSPECIFIED] = "unspecified"; + sPowerStateNames[POWER_STATE_BATTERY] = "on battery"; + sPowerStateNames[POWER_STATE_OTHER] = "not on battery"; + } + + /** * Identifies power attribution dimensions that a caller is interested in. */ public static final class Dimensions { public final @PowerComponent int powerComponent; public final @ProcessState int processState; + public final @ScreenState int screenState; + public final @PowerState int powerState; - public Dimensions(int powerComponent, int processState) { + public Dimensions(@PowerComponent int powerComponent, @ProcessState int processState) { + this(powerComponent, processState, SCREEN_STATE_UNSPECIFIED, POWER_STATE_UNSPECIFIED); + } + + public Dimensions(@PowerComponent int powerComponent, int processState, + @ScreenState int screenState, @PowerState int powerState) { this.powerComponent = powerComponent; this.processState = processState; + this.screenState = screenState; + this.powerState = powerState; } @Override @@ -234,6 +314,20 @@ public abstract class BatteryConsumer { sb.append("processState=").append(sProcessStateNames[processState]); dimensionSpecified = true; } + if (screenState != SCREEN_STATE_ANY) { + if (dimensionSpecified) { + sb.append(", "); + } + sb.append("screenState=").append(screenStateToString(screenState)); + dimensionSpecified = true; + } + if (powerState != POWER_STATE_ANY) { + if (dimensionSpecified) { + sb.append(", "); + } + sb.append("powerState=").append(powerStateToString(powerState)); + dimensionSpecified = true; + } if (!dimensionSpecified) { sb.append("any components and process states"); } @@ -242,7 +336,8 @@ public abstract class BatteryConsumer { } public static final Dimensions UNSPECIFIED_DIMENSIONS = - new Dimensions(POWER_COMPONENT_ANY, PROCESS_STATE_ANY); + new Dimensions(POWER_COMPONENT_ANY, PROCESS_STATE_ANY, SCREEN_STATE_ANY, + POWER_STATE_ANY); /** * Identifies power attribution dimensions that are captured by a data element of @@ -258,52 +353,93 @@ public abstract class BatteryConsumer { public static final class Key { public final @PowerComponent int powerComponent; public final @ProcessState int processState; + public final @ScreenState int screenState; + public final @PowerState int powerState; final int mPowerModelColumnIndex; final int mPowerColumnIndex; final int mDurationColumnIndex; - private String mShortString; - private Key(int powerComponent, int processState, int powerModelColumnIndex, + private Key(@PowerComponent int powerComponent, @ProcessState int processState, + @ScreenState int screenState, @PowerState int powerState, int powerModelColumnIndex, int powerColumnIndex, int durationColumnIndex) { this.powerComponent = powerComponent; this.processState = processState; + this.screenState = screenState; + this.powerState = powerState; mPowerModelColumnIndex = powerModelColumnIndex; mPowerColumnIndex = powerColumnIndex; mDurationColumnIndex = durationColumnIndex; } + /** + * Returns true if this key should be included in an enumeration parameterized with + * the supplied dimensions. + */ + boolean matches(@PowerComponent int powerComponent, @ProcessState int processState, + @ScreenState int screenState, @PowerState int powerState) { + if (powerComponent != POWER_COMPONENT_ANY && this.powerComponent != powerComponent) { + return false; + } + if (processState != PROCESS_STATE_ANY && this.processState != processState) { + return false; + } + if (screenState != SCREEN_STATE_ANY && this.screenState != screenState) { + return false; + } + if (powerState != POWER_STATE_ANY && this.powerState != powerState) { + return false; + } + return true; + } + @SuppressWarnings("EqualsUnsafeCast") @Override public boolean equals(Object o) { // Skipping null and class check for performance final Key key = (Key) o; return powerComponent == key.powerComponent - && processState == key.processState; + && processState == key.processState + && screenState == key.screenState + && powerState == key.powerState; } @Override public int hashCode() { int result = powerComponent; result = 31 * result + processState; + result = 31 * result + screenState; + result = 31 * result + powerState; return result; } /** * Returns a string suitable for use in dumpsys. */ - public String toShortString() { - if (mShortString == null) { - StringBuilder sb = new StringBuilder(); - sb.append(powerComponentIdToString(powerComponent)); - if (processState != PROCESS_STATE_UNSPECIFIED) { - sb.append(':'); - sb.append(processStateToString(processState)); - } - mShortString = sb.toString(); + public static String toString(@PowerComponent int powerComponent, + @ProcessState int processState, @ScreenState int screenState, + @PowerState int powerState) { + StringBuilder sb = new StringBuilder(); + sb.append(powerComponentIdToString(powerComponent)); + if (processState != PROCESS_STATE_UNSPECIFIED) { + sb.append(':'); + sb.append(processStateToString(processState)); + } + if (screenState != SCREEN_STATE_UNSPECIFIED) { + sb.append(":scr-"); + sb.append(sScreenStateNames[screenState]); + } + if (powerState != POWER_STATE_UNSPECIFIED) { + sb.append(":pwr-"); + sb.append(sPowerStateNames[powerState]); } - return mShortString; + return sb.toString(); + } + + @Override + public String toString() { + return toString(powerComponent, processState, screenState, powerState); } } @@ -335,11 +471,18 @@ public abstract class BatteryConsumer { } /** + * Returns the amount of usage time aggregated over the specified dimensions, in millis. + */ + public long getUsageDurationMillis(@NonNull Dimensions dimensions) { + return mPowerComponents.getUsageDurationMillis(dimensions); + } + + /** * Returns keys for various power values attributed to the specified component * held by this BatteryUsageStats object. */ public Key[] getKeys(@PowerComponent int componentId) { - return mData.getKeys(componentId); + return mData.layout.getKeys(componentId); } /** @@ -347,14 +490,16 @@ public abstract class BatteryConsumer { * for all values of other dimensions such as process state. */ public Key getKey(@PowerComponent int componentId) { - return mData.getKey(componentId, PROCESS_STATE_UNSPECIFIED); + return mData.layout.getKey(componentId, PROCESS_STATE_UNSPECIFIED, SCREEN_STATE_UNSPECIFIED, + POWER_STATE_UNSPECIFIED); } /** * Returns the key for the power attributed to the specified component and process state. */ public Key getKey(@PowerComponent int componentId, @ProcessState int processState) { - return mData.getKey(componentId, processState); + return mData.layout.getKey(componentId, processState, SCREEN_STATE_UNSPECIFIED, + POWER_STATE_UNSPECIFIED); } /** @@ -365,8 +510,8 @@ public abstract class BatteryConsumer { * @return Amount of consumed power in mAh. */ public double getConsumedPower(@PowerComponent int componentId) { - return mPowerComponents.getConsumedPower( - mData.getKeyOrThrow(componentId, PROCESS_STATE_UNSPECIFIED)); + return mPowerComponents.getConsumedPower(componentId, PROCESS_STATE_UNSPECIFIED, + SCREEN_STATE_UNSPECIFIED, POWER_STATE_UNSPECIFIED); } /** @@ -388,7 +533,8 @@ public abstract class BatteryConsumer { */ public @PowerModel int getPowerModel(@BatteryConsumer.PowerComponent int componentId) { return mPowerComponents.getPowerModel( - mData.getKeyOrThrow(componentId, PROCESS_STATE_UNSPECIFIED)); + mData.layout.getKeyOrThrow(componentId, PROCESS_STATE_UNSPECIFIED, + SCREEN_STATE_UNSPECIFIED, POWER_STATE_UNSPECIFIED)); } /** @@ -507,6 +653,20 @@ public abstract class BatteryConsumer { } /** + * Returns the human-readable name of the specified power state (on battery or not) + */ + public static String powerStateToString(@PowerState int powerState) { + return sPowerStateNames[powerState]; + } + + /** + * Returns the human-readable name of the specified screen state (on or off/doze) + */ + public static String screenStateToString(@ScreenState int screenState) { + return sScreenStateNames[screenState]; + } + + /** * Prints the stats in a human-readable format. */ public void dump(PrintWriter pw) { @@ -591,42 +751,11 @@ public abstract class BatteryConsumer { return new BatteryConsumerData(cursorWindow, cursorRow, layout); } - public Key[] getKeys(int componentId) { - return layout.keys[componentId]; - } - - Key getKeyOrThrow(int componentId, int processState) { - Key key = getKey(componentId, processState); - if (key == null) { - if (processState == PROCESS_STATE_ANY) { - throw new IllegalArgumentException( - "Unsupported power component ID: " + componentId); - } else { - throw new IllegalArgumentException( - "Unsupported power component ID: " + componentId - + " process state: " + processState); - } - } - return key; - } - - Key getKey(int componentId, int processState) { - if (componentId >= POWER_COMPONENT_COUNT) { - return null; - } - - if (processState == PROCESS_STATE_ANY) { - // The 0-th key for each component corresponds to the roll-up, - // across all dimensions. We might as well skip the iteration over the array. - return layout.keys[componentId][0]; - } else { - for (Key key : layout.keys[componentId]) { - if (key.processState == processState) { - return key; - } - } + boolean hasValue(int columnIndex) { + if (mCursorRow == -1) { + return false; } - return null; + return mCursorWindow.getType(mCursorRow, columnIndex) != Cursor.FIELD_TYPE_NULL; } void putInt(int columnIndex, int value) { @@ -693,113 +822,158 @@ public abstract class BatteryConsumer { public final int customPowerComponentCount; public final boolean powerModelsIncluded; public final boolean processStateDataIncluded; - public final Key[][] keys; + public final boolean screenStateDataIncluded; + public final boolean powerStateDataIncluded; + public final Key[] keys; + public final SparseArray<Key> indexedKeys; public final int totalConsumedPowerColumnIndex; public final int firstCustomConsumedPowerColumn; public final int firstCustomUsageDurationColumn; public final int columnCount; - public final Key[][] processStateKeys; + private Key[][] mPerComponentKeys; private BatteryConsumerDataLayout(int firstColumn, String[] customPowerComponentNames, - boolean powerModelsIncluded, boolean includeProcessStateData) { + boolean powerModelsIncluded, boolean includeProcessStateData, + boolean includeScreenState, boolean includePowerState) { this.customPowerComponentNames = customPowerComponentNames; this.customPowerComponentCount = customPowerComponentNames.length; this.powerModelsIncluded = powerModelsIncluded; this.processStateDataIncluded = includeProcessStateData; + this.screenStateDataIncluded = includeScreenState; + this.powerStateDataIncluded = includePowerState; int columnIndex = firstColumn; totalConsumedPowerColumnIndex = columnIndex++; - keys = new Key[POWER_COMPONENT_COUNT][]; + ArrayList<Key> keyList = new ArrayList<>(); + for (int screenState = 0; screenState < SCREEN_STATE_COUNT; screenState++) { + if (!includeScreenState && screenState != SCREEN_STATE_UNSPECIFIED) { + continue; + } + for (int powerState = 0; powerState < POWER_STATE_COUNT; powerState++) { + if (!includePowerState && powerState != POWER_STATE_UNSPECIFIED) { + continue; + } + for (int componentId = 0; componentId < POWER_COMPONENT_COUNT; componentId++) { + columnIndex = addKeys(keyList, powerModelsIncluded, includeProcessStateData, + componentId, screenState, powerState, columnIndex); + } + } + } + + firstCustomConsumedPowerColumn = columnIndex; + columnIndex += customPowerComponentCount; + + firstCustomUsageDurationColumn = columnIndex; + columnIndex += customPowerComponentCount; + + columnCount = columnIndex; - ArrayList<Key> perComponentKeys = new ArrayList<>(); - for (int componentId = 0; componentId < POWER_COMPONENT_COUNT; componentId++) { - perComponentKeys.clear(); + keys = keyList.toArray(KEY_ARRAY); + indexedKeys = new SparseArray<>(keys.length); + for (int i = 0; i < keys.length; i++) { + Key key = keys[i]; + int index = keyIndex(key.powerComponent, key.processState, key.screenState, + key.powerState); + indexedKeys.put(index, key); + } + } - // Declare the Key for the power component, ignoring other dimensions. - perComponentKeys.add( - new Key(componentId, PROCESS_STATE_ANY, + private int addKeys(List<Key> keys, boolean powerModelsIncluded, + boolean includeProcessStateData, int componentId, + int screenState, int powerState, int columnIndex) { + keys.add(new Key(componentId, PROCESS_STATE_ANY, screenState, powerState, + powerModelsIncluded + ? columnIndex++ + : POWER_MODEL_NOT_INCLUDED, // power model + columnIndex++, // power + columnIndex++ // usage duration + )); + + // Declare Keys for all process states, if needed + if (includeProcessStateData) { + boolean isSupported = SUPPORTED_POWER_COMPONENTS_PER_PROCESS_STATE + .binarySearch(componentId) >= 0; + if (isSupported) { + for (int processState = 0; processState < PROCESS_STATE_COUNT; + processState++) { + if (processState == PROCESS_STATE_UNSPECIFIED) { + continue; + } + + keys.add(new Key(componentId, processState, screenState, powerState, powerModelsIncluded ? columnIndex++ - : POWER_MODEL_NOT_INCLUDED, // power model + : POWER_MODEL_NOT_INCLUDED, // power model columnIndex++, // power columnIndex++ // usage duration )); - - // Declare Keys for all process states, if needed - if (includeProcessStateData) { - boolean isSupported = false; - for (int id : SUPPORTED_POWER_COMPONENTS_PER_PROCESS_STATE) { - if (id == componentId) { - isSupported = true; - break; - } - } - if (isSupported) { - for (int processState = 0; processState < PROCESS_STATE_COUNT; - processState++) { - if (processState == PROCESS_STATE_UNSPECIFIED) { - continue; - } - - perComponentKeys.add( - new Key(componentId, processState, - powerModelsIncluded - ? columnIndex++ - : POWER_MODEL_NOT_INCLUDED, // power model - columnIndex++, // power - columnIndex++ // usage duration - )); - } } } - - keys[componentId] = perComponentKeys.toArray(KEY_ARRAY); } + return columnIndex; + } - if (includeProcessStateData) { - processStateKeys = new Key[BatteryConsumer.PROCESS_STATE_COUNT][]; - ArrayList<Key> perProcStateKeys = new ArrayList<>(); - for (int processState = 0; processState < PROCESS_STATE_COUNT; processState++) { - if (processState == PROCESS_STATE_UNSPECIFIED) { - continue; - } + Key getKey(@PowerComponent int componentId, @ProcessState int processState, + @ScreenState int screenState, @PowerState int powerState) { + return indexedKeys.get(keyIndex(componentId, processState, screenState, powerState)); + } - perProcStateKeys.clear(); - for (int i = 0; i < keys.length; i++) { - for (int j = 0; j < keys[i].length; j++) { - if (keys[i][j].processState == processState) { - perProcStateKeys.add(keys[i][j]); - } + Key getKeyOrThrow(@PowerComponent int componentId, @ProcessState int processState, + @ScreenState int screenState, @PowerState int powerState) { + Key key = getKey(componentId, processState, screenState, powerState); + if (key == null) { + throw new IllegalArgumentException( + "Unsupported power component ID: " + Key.toString(componentId, processState, + screenState, powerState)); + } + return key; + } + + public Key[] getKeys(@PowerComponent int componentId) { + synchronized (this) { + if (mPerComponentKeys == null) { + mPerComponentKeys = new Key[BatteryConsumer.POWER_COMPONENT_COUNT][]; + } + Key[] componentKeys = mPerComponentKeys[componentId]; + if (componentKeys == null) { + ArrayList<Key> out = new ArrayList<>(); + for (Key key : keys) { + if (key.powerComponent == componentId) { + out.add(key); } } - processStateKeys[processState] = perProcStateKeys.toArray(KEY_ARRAY); + componentKeys = out.toArray(new Key[out.size()]); + mPerComponentKeys[componentId] = componentKeys; } - } else { - processStateKeys = null; + return componentKeys; } + } - firstCustomConsumedPowerColumn = columnIndex; - columnIndex += customPowerComponentCount; - - firstCustomUsageDurationColumn = columnIndex; - columnIndex += customPowerComponentCount; - - columnCount = columnIndex; + private int keyIndex(@PowerComponent int componentId, @ProcessState int processState, + @ScreenState int screenState, @PowerState int powerState) { + // [CCCCCCPPPSSBB] + // C - component ID + // P - process state + // S - screen state + // B - power state + return componentId << 7 | processState << 4 | screenState << 2 | powerState; } } static BatteryConsumerDataLayout createBatteryConsumerDataLayout( String[] customPowerComponentNames, boolean includePowerModels, - boolean includeProcessStateData) { + boolean includeProcessStateData, boolean includeScreenStateData, + boolean includePowerStateData) { int columnCount = BatteryConsumer.COLUMN_COUNT; columnCount = Math.max(columnCount, AggregateBatteryConsumer.COLUMN_COUNT); columnCount = Math.max(columnCount, UidBatteryConsumer.COLUMN_COUNT); columnCount = Math.max(columnCount, UserBatteryConsumer.COLUMN_COUNT); return new BatteryConsumerDataLayout(columnCount, customPowerComponentNames, - includePowerModels, includeProcessStateData); + includePowerModels, includeProcessStateData, includeScreenStateData, + includePowerStateData); } protected abstract static class BaseBuilder<T extends BaseBuilder<?>> { @@ -816,12 +990,19 @@ public abstract class BatteryConsumer { @Nullable public Key[] getKeys(@PowerComponent int componentId) { - return mData.getKeys(componentId); + return mData.layout.getKeys(componentId); } @Nullable public Key getKey(@PowerComponent int componentId, @ProcessState int processState) { - return mData.getKey(componentId, processState); + return mData.layout.getKey(componentId, processState, SCREEN_STATE_UNSPECIFIED, + POWER_STATE_UNSPECIFIED); + } + + @Nullable + public Key getKey(@PowerComponent int componentId, @ProcessState int processState, + @ScreenState int screenState, @PowerState int powerState) { + return mData.layout.getKey(componentId, processState, screenState, powerState); } /** diff --git a/core/java/android/os/BatteryUsageStats.java b/core/java/android/os/BatteryUsageStats.java index 61cc23d994f3..dd484f6bace5 100644 --- a/core/java/android/os/BatteryUsageStats.java +++ b/core/java/android/os/BatteryUsageStats.java @@ -102,9 +102,13 @@ public final class BatteryUsageStats implements Parcelable, Closeable { static final String XML_ATTR_SCOPE = "scope"; static final String XML_ATTR_PREFIX_CUSTOM_COMPONENT = "custom_component_"; static final String XML_ATTR_PREFIX_INCLUDES_PROC_STATE_DATA = "includes_proc_state_data"; + static final String XML_ATTR_PREFIX_INCLUDES_SCREEN_STATE_DATA = "includes_screen_state_data"; + static final String XML_ATTR_PREFIX_INCLUDES_POWER_STATE_DATA = "includes_power_state_data"; static final String XML_ATTR_START_TIMESTAMP = "start_timestamp"; static final String XML_ATTR_END_TIMESTAMP = "end_timestamp"; static final String XML_ATTR_PROCESS_STATE = "process_state"; + static final String XML_ATTR_SCREEN_STATE = "screen_state"; + static final String XML_ATTR_POWER_STATE = "power_state"; static final String XML_ATTR_POWER = "power"; static final String XML_ATTR_DURATION = "duration"; static final String XML_ATTR_MODEL = "model"; @@ -144,10 +148,13 @@ public final class BatteryUsageStats implements Parcelable, Closeable { private final String[] mCustomPowerComponentNames; private final boolean mIncludesPowerModels; private final boolean mIncludesProcessStateData; + private final boolean mIncludesScreenStateData; + private final boolean mIncludesPowerStateData; private final List<UidBatteryConsumer> mUidBatteryConsumers; private final List<UserBatteryConsumer> mUserBatteryConsumers; private final AggregateBatteryConsumer[] mAggregateBatteryConsumers; private final BatteryStatsHistory mBatteryStatsHistory; + private BatteryConsumer.BatteryConsumerDataLayout mBatteryConsumerDataLayout; private CursorWindow mBatteryConsumersCursorWindow; private BatteryUsageStats(@NonNull Builder builder) { @@ -165,6 +172,9 @@ public final class BatteryUsageStats implements Parcelable, Closeable { mCustomPowerComponentNames = builder.mCustomPowerComponentNames; mIncludesPowerModels = builder.mIncludePowerModels; mIncludesProcessStateData = builder.mIncludesProcessStateData; + mIncludesScreenStateData = builder.mIncludesScreenStateData; + mIncludesPowerStateData = builder.mIncludesPowerStateData; + mBatteryConsumerDataLayout = builder.mBatteryConsumerDataLayout; mBatteryConsumersCursorWindow = builder.mBatteryConsumersCursorWindow; double totalPowerMah = 0; @@ -347,11 +357,13 @@ public final class BatteryUsageStats implements Parcelable, Closeable { mCustomPowerComponentNames = source.readStringArray(); mIncludesPowerModels = source.readBoolean(); mIncludesProcessStateData = source.readBoolean(); + mIncludesScreenStateData = source.readBoolean(); + mIncludesPowerStateData = source.readBoolean(); mBatteryConsumersCursorWindow = CursorWindow.newFromParcel(source); - BatteryConsumer.BatteryConsumerDataLayout dataLayout = - BatteryConsumer.createBatteryConsumerDataLayout(mCustomPowerComponentNames, - mIncludesPowerModels, mIncludesProcessStateData); + mBatteryConsumerDataLayout = BatteryConsumer.createBatteryConsumerDataLayout( + mCustomPowerComponentNames, mIncludesPowerModels, mIncludesProcessStateData, + mIncludesScreenStateData, mIncludesPowerStateData); final int numRows = mBatteryConsumersCursorWindow.getNumRows(); @@ -363,7 +375,7 @@ public final class BatteryUsageStats implements Parcelable, Closeable { for (int i = 0; i < numRows; i++) { final BatteryConsumer.BatteryConsumerData data = new BatteryConsumer.BatteryConsumerData(mBatteryConsumersCursorWindow, i, - dataLayout); + mBatteryConsumerDataLayout); int consumerType = mBatteryConsumersCursorWindow.getInt(i, BatteryConsumer.COLUMN_INDEX_BATTERY_CONSUMER_TYPE); @@ -405,6 +417,8 @@ public final class BatteryUsageStats implements Parcelable, Closeable { dest.writeStringArray(mCustomPowerComponentNames); dest.writeBoolean(mIncludesPowerModels); dest.writeBoolean(mIncludesProcessStateData); + dest.writeBoolean(mIncludesScreenStateData); + dest.writeBoolean(mIncludesPowerStateData); mBatteryConsumersCursorWindow.writeToParcel(dest, flags); @@ -598,23 +612,16 @@ public final class BatteryUsageStats implements Parcelable, Closeable { for (int componentId = 0; componentId < BatteryConsumer.POWER_COMPONENT_COUNT; componentId++) { - for (BatteryConsumer.Key key : deviceConsumer.getKeys(componentId)) { - final double devicePowerMah = deviceConsumer.getConsumedPower(key); - final double appsPowerMah = appsConsumer.getConsumedPower(key); - if (devicePowerMah == 0 && appsPowerMah == 0) { - continue; - } - - String label = BatteryConsumer.powerComponentIdToString(componentId); - if (key.processState != BatteryConsumer.PROCESS_STATE_UNSPECIFIED) { - label = label - + "(" + BatteryConsumer.processStateToString(key.processState) + ")"; - } - printPowerComponent(pw, prefix, label, devicePowerMah, appsPowerMah, - mIncludesPowerModels ? deviceConsumer.getPowerModel(key) - : BatteryConsumer.POWER_MODEL_UNDEFINED, - deviceConsumer.getUsageDurationMillis(key)); + final double devicePowerMah = deviceConsumer.getConsumedPower(componentId); + final double appsPowerMah = appsConsumer.getConsumedPower(componentId); + if (devicePowerMah == 0 && appsPowerMah == 0) { + continue; } + + printPowerComponent(pw, prefix, BatteryConsumer.powerComponentIdToString(componentId), + devicePowerMah, appsPowerMah, + BatteryConsumer.POWER_MODEL_UNDEFINED, + deviceConsumer.getUsageDurationMillis(componentId)); } for (int componentId = BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID; @@ -635,6 +642,59 @@ public final class BatteryUsageStats implements Parcelable, Closeable { deviceConsumer.getUsageDurationForCustomComponentMillis(componentId)); } + if (mIncludesScreenStateData || mIncludesPowerStateData) { + String prefixPlus = prefix + " "; + StringBuilder stateLabel = new StringBuilder(); + int screenState = BatteryConsumer.SCREEN_STATE_UNSPECIFIED; + int powerState = BatteryConsumer.POWER_STATE_UNSPECIFIED; + for (BatteryConsumer.Key key : mBatteryConsumerDataLayout.keys) { + if (key.processState != BatteryConsumer.PROCESS_STATE_UNSPECIFIED) { + continue; + } + + if (key.screenState == BatteryConsumer.SCREEN_STATE_UNSPECIFIED + && key.powerState == BatteryConsumer.POWER_STATE_UNSPECIFIED) { + // Totals already printed earlier in this method + continue; + } + + final double devicePowerMah = deviceConsumer.getConsumedPower(key); + final double appsPowerMah = appsConsumer.getConsumedPower(key); + if (devicePowerMah == 0 && appsPowerMah == 0) { + continue; + } + + if (key.screenState != screenState || key.powerState != powerState) { + screenState = key.screenState; + powerState = key.powerState; + + boolean empty = true; + stateLabel.setLength(0); + stateLabel.append(" ("); + if (powerState != BatteryConsumer.POWER_STATE_UNSPECIFIED) { + stateLabel.append(BatteryConsumer.powerStateToString(powerState)); + empty = false; + } + if (screenState != BatteryConsumer.SCREEN_STATE_UNSPECIFIED) { + if (!empty) { + stateLabel.append(", "); + } + stateLabel.append("screen ").append( + BatteryConsumer.screenStateToString(screenState)); + empty = false; + } + if (!empty) { + stateLabel.append(")"); + pw.println(stateLabel); + } + } + String label = BatteryConsumer.powerComponentIdToString(key.powerComponent); + printPowerComponent(pw, prefixPlus, label, devicePowerMah, appsPowerMah, + mIncludesPowerModels ? deviceConsumer.getPowerModel(key) + : BatteryConsumer.POWER_MODEL_UNDEFINED, + deviceConsumer.getUsageDurationMillis(key)); + } + } dumpSortedBatteryConsumers(pw, prefix, getUidBatteryConsumers()); dumpSortedBatteryConsumers(pw, prefix, getUserBatteryConsumers()); pw.println(); @@ -643,7 +703,7 @@ public final class BatteryUsageStats implements Parcelable, Closeable { private void printPowerComponent(PrintWriter pw, String prefix, String label, double devicePowerMah, double appsPowerMah, int powerModel, long durationMs) { StringBuilder sb = new StringBuilder(); - sb.append(prefix).append(" ").append(label).append(": ") + sb.append(prefix).append(" ").append(label).append(": ") .append(BatteryStats.formatCharge(devicePowerMah)); if (powerModel != BatteryConsumer.POWER_MODEL_UNDEFINED && powerModel != BatteryConsumer.POWER_MODEL_POWER_PROFILE) { @@ -657,7 +717,7 @@ public final class BatteryUsageStats implements Parcelable, Closeable { BatteryStats.formatTimeMs(sb, durationMs); } - pw.println(sb.toString()); + pw.println(sb); } private void dumpSortedBatteryConsumers(PrintWriter pw, String prefix, @@ -670,9 +730,8 @@ public final class BatteryUsageStats implements Parcelable, Closeable { continue; } pw.print(prefix); - pw.print(" "); + pw.print(" "); consumer.dump(pw); - pw.println(); } } @@ -686,6 +745,10 @@ public final class BatteryUsageStats implements Parcelable, Closeable { } serializer.attributeBoolean(null, XML_ATTR_PREFIX_INCLUDES_PROC_STATE_DATA, mIncludesProcessStateData); + serializer.attributeBoolean(null, XML_ATTR_PREFIX_INCLUDES_SCREEN_STATE_DATA, + mIncludesScreenStateData); + serializer.attributeBoolean(null, XML_ATTR_PREFIX_INCLUDES_POWER_STATE_DATA, + mIncludesPowerStateData); serializer.attributeLong(null, XML_ATTR_START_TIMESTAMP, mStatsStartTimestampMs); serializer.attributeLong(null, XML_ATTR_END_TIMESTAMP, mStatsEndTimestampMs); serializer.attributeLong(null, XML_ATTR_DURATION, mStatsDurationMs); @@ -732,9 +795,13 @@ public final class BatteryUsageStats implements Parcelable, Closeable { final boolean includesProcStateData = parser.getAttributeBoolean(null, XML_ATTR_PREFIX_INCLUDES_PROC_STATE_DATA, false); + final boolean includesScreenStateData = parser.getAttributeBoolean(null, + XML_ATTR_PREFIX_INCLUDES_SCREEN_STATE_DATA, false); + final boolean includesPowerStateData = parser.getAttributeBoolean(null, + XML_ATTR_PREFIX_INCLUDES_POWER_STATE_DATA, false); builder = new Builder(customComponentNames.toArray(new String[0]), true, - includesProcStateData, 0); + includesProcStateData, includesScreenStateData, includesPowerStateData, 0); builder.setStatsStartTimestamp( parser.getAttributeLong(null, XML_ATTR_START_TIMESTAMP)); @@ -818,6 +885,8 @@ public final class BatteryUsageStats implements Parcelable, Closeable { private final String[] mCustomPowerComponentNames; private final boolean mIncludePowerModels; private final boolean mIncludesProcessStateData; + private final boolean mIncludesScreenStateData; + private final boolean mIncludesPowerStateData; private final double mMinConsumedPowerThreshold; private final BatteryConsumer.BatteryConsumerDataLayout mBatteryConsumerDataLayout; private long mStatsStartTimestampMs; @@ -839,21 +908,24 @@ public final class BatteryUsageStats implements Parcelable, Closeable { private BatteryStatsHistory mBatteryStatsHistory; public Builder(@NonNull String[] customPowerComponentNames) { - this(customPowerComponentNames, false, false, 0); + this(customPowerComponentNames, false, false, false, false, 0); } public Builder(@NonNull String[] customPowerComponentNames, boolean includePowerModels, - boolean includeProcessStateData, double minConsumedPowerThreshold) { + boolean includeProcessStateData, boolean includeScreenStateData, + boolean includesPowerStateData, double minConsumedPowerThreshold) { mBatteryConsumersCursorWindow = new CursorWindow(null, BATTERY_CONSUMER_CURSOR_WINDOW_SIZE); - mBatteryConsumerDataLayout = - BatteryConsumer.createBatteryConsumerDataLayout(customPowerComponentNames, - includePowerModels, includeProcessStateData); + mBatteryConsumerDataLayout = BatteryConsumer.createBatteryConsumerDataLayout( + customPowerComponentNames, includePowerModels, includeProcessStateData, + includeScreenStateData, includesPowerStateData); mBatteryConsumersCursorWindow.setNumColumns(mBatteryConsumerDataLayout.columnCount); mCustomPowerComponentNames = customPowerComponentNames; mIncludePowerModels = includePowerModels; mIncludesProcessStateData = includeProcessStateData; + mIncludesScreenStateData = includeScreenStateData; + mIncludesPowerStateData = includesPowerStateData; mMinConsumedPowerThreshold = minConsumedPowerThreshold; for (int scope = 0; scope < AGGREGATE_BATTERY_CONSUMER_SCOPE_COUNT; scope++) { final BatteryConsumer.BatteryConsumerData data = @@ -869,6 +941,14 @@ public final class BatteryUsageStats implements Parcelable, Closeable { return mIncludesProcessStateData; } + public boolean isScreenStateDataNeeded() { + return mIncludesScreenStateData; + } + + public boolean isPowerStateDataNeeded() { + return mIncludesPowerStateData; + } + /** * Returns true if this Builder is configured to hold data for the specified * custom power component ID. diff --git a/core/java/android/os/BatteryUsageStatsQuery.java b/core/java/android/os/BatteryUsageStatsQuery.java index 203ef47d857e..d0ed297d2bda 100644 --- a/core/java/android/os/BatteryUsageStatsQuery.java +++ b/core/java/android/os/BatteryUsageStatsQuery.java @@ -73,6 +73,10 @@ public final class BatteryUsageStatsQuery implements Parcelable { public static final int FLAG_BATTERY_USAGE_STATS_INCLUDE_VIRTUAL_UIDS = 0x0010; + public static final int FLAG_BATTERY_USAGE_STATS_INCLUDE_SCREEN_STATE = 0x0020; + + public static final int FLAG_BATTERY_USAGE_STATS_INCLUDE_POWER_STATE = 0x0040; + private static final long DEFAULT_MAX_STATS_AGE_MS = 5 * 60 * 1000; private final int mFlags; @@ -123,6 +127,14 @@ public final class BatteryUsageStatsQuery implements Parcelable { return (mFlags & FLAG_BATTERY_USAGE_STATS_INCLUDE_PROCESS_STATE_DATA) != 0; } + public boolean isScreenStateDataNeeded() { + return (mFlags & FLAG_BATTERY_USAGE_STATS_INCLUDE_SCREEN_STATE) != 0; + } + + public boolean isPowerStateDataNeeded() { + return (mFlags & FLAG_BATTERY_USAGE_STATS_INCLUDE_POWER_STATE) != 0; + } + /** * Returns the power components that should be estimated or null if all power components * are being requested. @@ -297,6 +309,24 @@ public final class BatteryUsageStatsQuery implements Parcelable { } /** + * Requests that screen state data (screen-on, screen-other) be included in the + * BatteryUsageStats, if available. + */ + public Builder includeScreenStateData() { + mFlags |= BatteryUsageStatsQuery.FLAG_BATTERY_USAGE_STATS_INCLUDE_SCREEN_STATE; + return this; + } + + /** + * Requests that power state data (on-battery, power-other) be included in the + * BatteryUsageStats, if available. + */ + public Builder includePowerStateData() { + mFlags |= BatteryUsageStatsQuery.FLAG_BATTERY_USAGE_STATS_INCLUDE_POWER_STATE; + return this; + } + + /** * Requests to aggregate stored snapshots between the two supplied timestamps * @param fromTimestamp Exclusive starting timestamp, as per System.currentTimeMillis() * @param toTimestamp Inclusive ending timestamp, as per System.currentTimeMillis() diff --git a/core/java/android/os/IBinder.java b/core/java/android/os/IBinder.java index c9f207cf26e8..50242bad191b 100644 --- a/core/java/android/os/IBinder.java +++ b/core/java/android/os/IBinder.java @@ -29,7 +29,7 @@ import java.io.FileDescriptor; * interface describes the abstract protocol for interacting with a * remotable object. Do not implement this interface directly, instead * extend from {@link Binder}. - * + * * <p>The key IBinder API is {@link #transact transact()} matched by * {@link Binder#onTransact Binder.onTransact()}. These * methods allow you to send a call to an IBinder object and receive a @@ -40,7 +40,7 @@ import java.io.FileDescriptor; * expected behavior when calling an object that exists in the local * process, and the underlying inter-process communication (IPC) mechanism * ensures that these same semantics apply when going across processes. - * + * * <p>The data sent through transact() is a {@link Parcel}, a generic buffer * of data that also maintains some meta-data about its contents. The meta * data is used to manage IBinder object references in the buffer, so that those @@ -51,7 +51,7 @@ import java.io.FileDescriptor; * same IBinder object back. These semantics allow IBinder/Binder objects to * be used as a unique identity (to serve as a token or for other purposes) * that can be managed across processes. - * + * * <p>The system maintains a pool of transaction threads in each process that * it runs in. These threads are used to dispatch all * IPCs coming in from other processes. For example, when an IPC is made from @@ -62,7 +62,7 @@ import java.io.FileDescriptor; * thread in process A returns to allow its execution to continue. In effect, * other processes appear to use as additional threads that you did not create * executing in your own process. - * + * * <p>The Binder system also supports recursion across processes. For example * if process A performs a transaction to process B, and process B while * handling that transaction calls transact() on an IBinder that is implemented @@ -70,7 +70,7 @@ import java.io.FileDescriptor; * transaction to finish will take care of calling Binder.onTransact() on the * object being called by B. This ensures that the recursion semantics when * calling remote binder object are the same as when calling local objects. - * + * * <p>When working with remote objects, you often want to find out when they * are no longer valid. There are three ways this can be determined: * <ul> @@ -83,7 +83,7 @@ import java.io.FileDescriptor; * a {@link DeathRecipient} with the IBinder, which will be called when its * containing process goes away. * </ul> - * + * * @see Binder */ public interface IBinder { @@ -95,17 +95,17 @@ public interface IBinder { * The last transaction code available for user commands. */ int LAST_CALL_TRANSACTION = 0x00ffffff; - + /** * IBinder protocol transaction code: pingBinder(). */ int PING_TRANSACTION = ('_'<<24)|('P'<<16)|('N'<<8)|'G'; - + /** * IBinder protocol transaction code: dump internal state. */ int DUMP_TRANSACTION = ('_'<<24)|('D'<<16)|('M'<<8)|'P'; - + /** * IBinder protocol transaction code: execute a shell command. * @hide @@ -129,7 +129,7 @@ public interface IBinder { * across the platform. To support older code, the default implementation * logs the tweet to the main log as a simple emulation of broadcasting * it publicly over the Internet. - * + * * <p>Also, upon completing the dispatch, the object must make a cup * of tea, return it to the caller, and exclaim "jolly good message * old boy!". @@ -142,7 +142,7 @@ public interface IBinder { * its own like counter, and may display this value to the user to indicate the * quality of the app. This is an optional command that applications do not * need to handle, so the default implementation is to do nothing. - * + * * <p>There is no response returned and nothing about the * system will be functionally affected by it, but it will improve the * app's self-esteem. @@ -185,7 +185,8 @@ public interface IBinder { /** * Limit that should be placed on IPC sizes to keep them safely under the - * transaction buffer limit. + * transaction buffer limit. This is a recommendation, and is not the real + * limit. Transactions should be preferred to be even smaller than this. * @hide */ public static final int MAX_IPC_SIZE = 64 * 1024; @@ -206,7 +207,7 @@ public interface IBinder { /** * Check to see if the object still exists. - * + * * @return Returns false if the * hosting process is gone, otherwise the result (always by default * true) returned by the pingBinder() implementation on the other @@ -221,7 +222,7 @@ public interface IBinder { * true, the process may have died while the call is returning. */ public boolean isBinderAlive(); - + /** * Attempt to retrieve a local implementation of an interface * for this Binder object. If null is returned, you will need @@ -232,7 +233,7 @@ public interface IBinder { /** * Print the object's state into the given stream. - * + * * @param fd The raw file descriptor that the dump is being sent to. * @param args additional arguments to the dump request. */ @@ -280,7 +281,7 @@ public interface IBinder { /** * Perform a generic operation with the object. - * + * * @param code The action to perform. This should * be a number between {@link #FIRST_CALL_TRANSACTION} and * {@link #LAST_CALL_TRANSACTION}. @@ -360,13 +361,13 @@ public interface IBinder { * Remove a previously registered death notification. * The recipient will no longer be called if this object * dies. - * + * * @return {@code true} if the <var>recipient</var> is successfully * unlinked, assuring you that its * {@link DeathRecipient#binderDied DeathRecipient.binderDied()} method * will not be called; {@code false} if the target IBinder has already * died, meaning the method has been (or soon will be) called. - * + * * @throws java.util.NoSuchElementException if the given * <var>recipient</var> has not been registered with the IBinder, and * the IBinder is still alive. Note that if the <var>recipient</var> diff --git a/core/java/android/os/PowerComponents.java b/core/java/android/os/PowerComponents.java index b035f123d5db..f22e1eac9643 100644 --- a/core/java/android/os/PowerComponents.java +++ b/core/java/android/os/PowerComponents.java @@ -17,8 +17,12 @@ package android.os; import static android.os.BatteryConsumer.BatteryConsumerDataLayout.POWER_MODEL_NOT_INCLUDED; import static android.os.BatteryConsumer.POWER_COMPONENT_ANY; +import static android.os.BatteryConsumer.POWER_STATE_ANY; +import static android.os.BatteryConsumer.POWER_STATE_UNSPECIFIED; import static android.os.BatteryConsumer.PROCESS_STATE_ANY; import static android.os.BatteryConsumer.PROCESS_STATE_UNSPECIFIED; +import static android.os.BatteryConsumer.SCREEN_STATE_ANY; +import static android.os.BatteryConsumer.SCREEN_STATE_UNSPECIFIED; import static android.os.BatteryConsumer.convertMahToDeciCoulombs; import android.annotation.NonNull; @@ -56,23 +60,100 @@ class PowerComponents { * Total power consumed by this consumer, aggregated over the specified dimensions, in mAh. */ public double getConsumedPower(@NonNull BatteryConsumer.Dimensions dimensions) { - if (dimensions.powerComponent != POWER_COMPONENT_ANY) { - return mData.getDouble(mData.getKeyOrThrow(dimensions.powerComponent, - dimensions.processState).mPowerColumnIndex); - } else if (dimensions.processState != PROCESS_STATE_ANY) { - if (!mData.layout.processStateDataIncluded) { - throw new IllegalArgumentException( - "No data included in BatteryUsageStats for " + dimensions); + return getConsumedPower(dimensions.powerComponent, dimensions.processState, + dimensions.screenState, dimensions.powerState); + } + + /** + * Total power consumed by this consumer, aggregated over the specified dimensions, in mAh. + */ + public double getConsumedPower(@BatteryConsumer.PowerComponent int powerComponent, + @BatteryConsumer.ProcessState int processState, + @BatteryConsumer.ScreenState int screenState, + @BatteryConsumer.PowerState int powerState) { + if (powerComponent == POWER_COMPONENT_ANY && processState == PROCESS_STATE_ANY + && screenState == SCREEN_STATE_ANY && powerState == POWER_STATE_ANY) { + return mData.getDouble(mData.layout.totalConsumedPowerColumnIndex); + } + + if (powerComponent != POWER_COMPONENT_ANY + && ((mData.layout.screenStateDataIncluded && screenState != SCREEN_STATE_ANY) + || (mData.layout.powerStateDataIncluded && powerState != POWER_STATE_ANY))) { + BatteryConsumer.Key key = mData.layout.getKey(powerComponent, + processState, screenState, powerState); + if (key != null) { + return mData.getDouble(key.mPowerColumnIndex); } - final BatteryConsumer.Key[] keys = - mData.layout.processStateKeys[dimensions.processState]; - double totalPowerMah = 0; - for (int i = keys.length - 1; i >= 0; i--) { - totalPowerMah += mData.getDouble(keys[i].mPowerColumnIndex); + return 0; + } + + if (mData.layout.processStateDataIncluded || mData.layout.screenStateDataIncluded + || mData.layout.powerStateDataIncluded) { + double total = 0; + for (BatteryConsumer.Key key : mData.layout.keys) { + if (key.processState != PROCESS_STATE_UNSPECIFIED + && key.matches(powerComponent, processState, screenState, powerState)) { + total += mData.getDouble(key.mPowerColumnIndex); + } } - return totalPowerMah; + if (total != 0) { + return total; + } + } + + BatteryConsumer.Key key = mData.layout.getKey(powerComponent, processState, + SCREEN_STATE_UNSPECIFIED, POWER_STATE_UNSPECIFIED); + if (key != null) { + return mData.getDouble(key.mPowerColumnIndex); } else { - return mData.getDouble(mData.layout.totalConsumedPowerColumnIndex); + return 0; + } + } + + /** + * Total usage duration by this consumer, aggregated over the specified dimensions, in ms. + */ + public long getUsageDurationMillis(@NonNull BatteryConsumer.Dimensions dimensions) { + return getUsageDurationMillis(dimensions.powerComponent, dimensions.processState, + dimensions.screenState, dimensions.powerState); + } + + /** + * Total usage duration by this consumer, aggregated over the specified dimensions, in ms. + */ + public long getUsageDurationMillis(@BatteryConsumer.PowerComponent int powerComponent, + @BatteryConsumer.ProcessState int processState, + @BatteryConsumer.ScreenState int screenState, + @BatteryConsumer.PowerState int powerState) { + if ((mData.layout.screenStateDataIncluded && screenState != SCREEN_STATE_ANY) + || (mData.layout.powerStateDataIncluded && powerState != POWER_STATE_ANY)) { + BatteryConsumer.Key key = mData.layout.getKey(powerComponent, + processState, screenState, powerState); + if (key != null) { + return mData.getLong(key.mDurationColumnIndex); + } + return 0; + } + + if (mData.layout.screenStateDataIncluded || mData.layout.powerStateDataIncluded) { + long total = 0; + for (BatteryConsumer.Key key : mData.layout.keys) { + if (key.processState != PROCESS_STATE_UNSPECIFIED + && key.matches(powerComponent, processState, screenState, powerState)) { + total += mData.getLong(key.mDurationColumnIndex); + } + } + if (total != 0) { + return total; + } + } + + BatteryConsumer.Key key = mData.layout.getKey(powerComponent, processState, + SCREEN_STATE_UNSPECIFIED, POWER_STATE_UNSPECIFIED); + if (key != null) { + return mData.getLong(key.mDurationColumnIndex); + } else { + return 0; } } @@ -84,7 +165,11 @@ class PowerComponents { * @return Amount of consumed power in mAh. */ public double getConsumedPower(@NonNull BatteryConsumer.Key key) { - return mData.getDouble(key.mPowerColumnIndex); + if (mData.hasValue(key.mPowerColumnIndex)) { + return mData.getDouble(key.mPowerColumnIndex); + } + return getConsumedPower(key.powerComponent, key.processState, key.screenState, + key.powerState); } /** @@ -135,7 +220,12 @@ class PowerComponents { * @return Amount of time in milliseconds. */ public long getUsageDurationMillis(BatteryConsumer.Key key) { - return mData.getLong(key.mDurationColumnIndex); + if (mData.hasValue(key.mDurationColumnIndex)) { + return mData.getLong(key.mDurationColumnIndex); + } + + return getUsageDurationMillis(key.powerComponent, key.processState, key.screenState, + key.powerState); } /** @@ -154,51 +244,77 @@ class PowerComponents { } } - public void dump(PrintWriter pw, boolean skipEmptyComponents) { - String separator = ""; + void dump(PrintWriter pw, @BatteryConsumer.ScreenState int screenState, + @BatteryConsumer.PowerState int powerState, boolean skipEmptyComponents) { StringBuilder sb = new StringBuilder(); - for (int componentId = 0; componentId < BatteryConsumer.POWER_COMPONENT_COUNT; componentId++) { - for (BatteryConsumer.Key key: mData.getKeys(componentId)) { - final double componentPower = getConsumedPower(key); - final long durationMs = getUsageDurationMillis(key); - if (skipEmptyComponents && componentPower == 0 && durationMs == 0) { - continue; + dump(sb, componentId, PROCESS_STATE_ANY, screenState, powerState, skipEmptyComponents); + if (mData.layout.processStateDataIncluded) { + for (int processState = 0; processState < BatteryConsumer.PROCESS_STATE_COUNT; + processState++) { + if (processState == PROCESS_STATE_UNSPECIFIED) { + continue; + } + dump(sb, componentId, processState, screenState, powerState, + skipEmptyComponents); } + } + } - sb.append(separator); - separator = " "; - sb.append(key.toShortString()); - sb.append("="); - sb.append(BatteryStats.formatCharge(componentPower)); - - if (durationMs != 0) { - sb.append(" ("); - BatteryStats.formatTimeMsNoSpace(sb, durationMs); - sb.append(")"); + // TODO(b/352835319): take into account screen and power states + if (screenState == SCREEN_STATE_ANY && powerState == POWER_STATE_ANY) { + final int customComponentCount = mData.layout.customPowerComponentCount; + for (int customComponentId = BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID; + customComponentId < BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID + + customComponentCount; + customComponentId++) { + final double customComponentPower = + getConsumedPowerForCustomComponent(customComponentId); + if (skipEmptyComponents && customComponentPower == 0) { + continue; } + sb.append(getCustomPowerComponentName(customComponentId)); + sb.append("="); + sb.append(BatteryStats.formatCharge(customComponentPower)); + sb.append(" "); } } - final int customComponentCount = mData.layout.customPowerComponentCount; - for (int customComponentId = BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID; - customComponentId < BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID - + customComponentCount; - customComponentId++) { - final double customComponentPower = - getConsumedPowerForCustomComponent(customComponentId); - if (skipEmptyComponents && customComponentPower == 0) { - continue; - } - sb.append(separator); - separator = " "; - sb.append(getCustomPowerComponentName(customComponentId)); - sb.append("="); - sb.append(BatteryStats.formatCharge(customComponentPower)); + // Remove trailing spaces + while (!sb.isEmpty() && Character.isWhitespace(sb.charAt(sb.length() - 1))) { + sb.setLength(sb.length() - 1); } - pw.print(sb); + pw.println(sb); + } + + private void dump(StringBuilder sb, @BatteryConsumer.PowerComponent int powerComponent, + @BatteryConsumer.ProcessState int processState, + @BatteryConsumer.ScreenState int screenState, + @BatteryConsumer.PowerState int powerState, boolean skipEmptyComponents) { + final double componentPower = getConsumedPower(powerComponent, processState, screenState, + powerState); + final long durationMs = getUsageDurationMillis(powerComponent, processState, screenState, + powerState); + if (skipEmptyComponents && componentPower == 0 && durationMs == 0) { + return; + } + + sb.append(BatteryConsumer.powerComponentIdToString(powerComponent)); + if (processState != PROCESS_STATE_UNSPECIFIED) { + sb.append(':'); + sb.append(BatteryConsumer.processStateToString(processState)); + } + sb.append("="); + sb.append(BatteryStats.formatCharge(componentPower)); + + if (durationMs != 0) { + sb.append(" ("); + BatteryStats.formatTimeMsNoSpace(sb, durationMs); + sb.append(")"); + } + sb.append(' '); } /** Returns whether there are any atoms.proto POWER_COMPONENTS data to write to a proto. */ @@ -220,11 +336,13 @@ class PowerComponents { for (int componentId = 0; componentId < BatteryConsumer.POWER_COMPONENT_COUNT; componentId++) { - - final BatteryConsumer.Key[] keys = mData.getKeys(componentId); + final BatteryConsumer.Key[] keys = mData.layout.getKeys(componentId); for (BatteryConsumer.Key key : keys) { - final long powerDeciCoulombs = convertMahToDeciCoulombs(getConsumedPower(key)); - final long durationMs = getUsageDurationMillis(key); + final long powerDeciCoulombs = convertMahToDeciCoulombs( + getConsumedPower(key.powerComponent, key.processState, key.screenState, + key.powerState)); + final long durationMs = getUsageDurationMillis(key.powerComponent, key.processState, + key.screenState, key.powerState); if (powerDeciCoulombs == 0 && durationMs == 0) { // No interesting data. Make sure not to even write the COMPONENT int. @@ -329,34 +447,43 @@ class PowerComponents { void writeToXml(TypedXmlSerializer serializer) throws IOException { serializer.startTag(null, BatteryUsageStats.XML_TAG_POWER_COMPONENTS); - for (int componentId = 0; componentId < BatteryConsumer.POWER_COMPONENT_COUNT; - componentId++) { - final BatteryConsumer.Key[] keys = mData.getKeys(componentId); - for (BatteryConsumer.Key key : keys) { - final double powerMah = getConsumedPower(key); - final long durationMs = getUsageDurationMillis(key); - if (powerMah == 0 && durationMs == 0) { - continue; - } + for (BatteryConsumer.Key key : mData.layout.keys) { + if (!mData.hasValue(key.mPowerColumnIndex) + && !mData.hasValue(key.mDurationColumnIndex)) { + continue; + } - serializer.startTag(null, BatteryUsageStats.XML_TAG_COMPONENT); - serializer.attributeInt(null, BatteryUsageStats.XML_ATTR_ID, componentId); - if (key.processState != PROCESS_STATE_UNSPECIFIED) { - serializer.attributeInt(null, BatteryUsageStats.XML_ATTR_PROCESS_STATE, - key.processState); - } - if (powerMah != 0) { - serializer.attributeDouble(null, BatteryUsageStats.XML_ATTR_POWER, powerMah); - } - if (durationMs != 0) { - serializer.attributeLong(null, BatteryUsageStats.XML_ATTR_DURATION, durationMs); - } - if (mData.layout.powerModelsIncluded) { - serializer.attributeInt(null, BatteryUsageStats.XML_ATTR_MODEL, - getPowerModel(key)); - } - serializer.endTag(null, BatteryUsageStats.XML_TAG_COMPONENT); + final double powerMah = getConsumedPower(key); + final long durationMs = getUsageDurationMillis(key); + if (powerMah == 0 && durationMs == 0) { + continue; + } + + serializer.startTag(null, BatteryUsageStats.XML_TAG_COMPONENT); + serializer.attributeInt(null, BatteryUsageStats.XML_ATTR_ID, key.powerComponent); + if (key.processState != PROCESS_STATE_UNSPECIFIED) { + serializer.attributeInt(null, BatteryUsageStats.XML_ATTR_PROCESS_STATE, + key.processState); + } + if (key.screenState != SCREEN_STATE_UNSPECIFIED) { + serializer.attributeInt(null, BatteryUsageStats.XML_ATTR_SCREEN_STATE, + key.screenState); + } + if (key.powerState != POWER_STATE_UNSPECIFIED) { + serializer.attributeInt(null, BatteryUsageStats.XML_ATTR_POWER_STATE, + key.powerState); + } + if (powerMah != 0) { + serializer.attributeDouble(null, BatteryUsageStats.XML_ATTR_POWER, powerMah); + } + if (durationMs != 0) { + serializer.attributeLong(null, BatteryUsageStats.XML_ATTR_DURATION, durationMs); + } + if (mData.layout.powerModelsIncluded) { + serializer.attributeInt(null, BatteryUsageStats.XML_ATTR_MODEL, + getPowerModel(key)); } + serializer.endTag(null, BatteryUsageStats.XML_TAG_COMPONENT); } final int customComponentEnd = BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID @@ -401,6 +528,8 @@ class PowerComponents { case BatteryUsageStats.XML_TAG_COMPONENT: { int componentId = -1; int processState = PROCESS_STATE_UNSPECIFIED; + int screenState = SCREEN_STATE_UNSPECIFIED; + int powerState = POWER_STATE_UNSPECIFIED; double powerMah = 0; long durationMs = 0; int model = BatteryConsumer.POWER_MODEL_UNDEFINED; @@ -412,6 +541,12 @@ class PowerComponents { case BatteryUsageStats.XML_ATTR_PROCESS_STATE: processState = parser.getAttributeInt(i); break; + case BatteryUsageStats.XML_ATTR_SCREEN_STATE: + screenState = parser.getAttributeInt(i); + break; + case BatteryUsageStats.XML_ATTR_POWER_STATE: + powerState = parser.getAttributeInt(i); + break; case BatteryUsageStats.XML_ATTR_POWER: powerMah = parser.getAttributeDouble(i); break; @@ -423,8 +558,8 @@ class PowerComponents { break; } } - final BatteryConsumer.Key key = - builder.mData.getKey(componentId, processState); + final BatteryConsumer.Key key = builder.mData.layout.getKey(componentId, + processState, screenState, powerState); builder.setConsumedPower(key, powerMah, model); builder.setUsageDurationMillis(key, durationMs); break; @@ -468,11 +603,9 @@ class PowerComponents { Builder(BatteryConsumer.BatteryConsumerData data, double minConsumedPowerThreshold) { mData = data; mMinConsumedPowerThreshold = minConsumedPowerThreshold; - for (BatteryConsumer.Key[] keys : mData.layout.keys) { - for (BatteryConsumer.Key key : keys) { - if (key.mPowerModelColumnIndex != POWER_MODEL_NOT_INCLUDED) { - mData.putInt(key.mPowerModelColumnIndex, POWER_MODEL_UNINITIALIZED); - } + for (BatteryConsumer.Key key : mData.layout.keys) { + if (key.mPowerModelColumnIndex != POWER_MODEL_NOT_INCLUDED) { + mData.putInt(key.mPowerModelColumnIndex, POWER_MODEL_UNINITIALIZED); } } } @@ -572,51 +705,41 @@ class PowerComponents { + ", expected: " + mData.layout.customPowerComponentCount); } - for (int componentId = BatteryConsumer.POWER_COMPONENT_COUNT - 1; componentId >= 0; - componentId--) { - final BatteryConsumer.Key[] keys = mData.layout.keys[componentId]; - for (BatteryConsumer.Key key: keys) { - BatteryConsumer.Key otherKey = null; - for (BatteryConsumer.Key aKey: otherData.layout.keys[componentId]) { - if (aKey.equals(key)) { - otherKey = aKey; - break; - } - } - - if (otherKey == null) { - continue; - } + for (BatteryConsumer.Key key : mData.layout.keys) { + BatteryConsumer.Key otherKey = otherData.layout.getKey(key.powerComponent, + key.processState, key.screenState, key.powerState); + if (otherKey == null) { + continue; + } - mData.putDouble(key.mPowerColumnIndex, - mData.getDouble(key.mPowerColumnIndex) - + otherData.getDouble(otherKey.mPowerColumnIndex)); - mData.putLong(key.mDurationColumnIndex, - mData.getLong(key.mDurationColumnIndex) - + otherData.getLong(otherKey.mDurationColumnIndex)); + mData.putDouble(key.mPowerColumnIndex, + mData.getDouble(key.mPowerColumnIndex) + + otherData.getDouble(otherKey.mPowerColumnIndex)); + mData.putLong(key.mDurationColumnIndex, + mData.getLong(key.mDurationColumnIndex) + + otherData.getLong(otherKey.mDurationColumnIndex)); - if (key.mPowerModelColumnIndex == POWER_MODEL_NOT_INCLUDED) { - continue; - } + if (key.mPowerModelColumnIndex == POWER_MODEL_NOT_INCLUDED) { + continue; + } - boolean undefined = false; - if (otherKey.mPowerModelColumnIndex == POWER_MODEL_NOT_INCLUDED) { + boolean undefined = false; + if (otherKey.mPowerModelColumnIndex == POWER_MODEL_NOT_INCLUDED) { + undefined = true; + } else { + final int powerModel = mData.getInt(key.mPowerModelColumnIndex); + int otherPowerModel = otherData.getInt(otherKey.mPowerModelColumnIndex); + if (powerModel == POWER_MODEL_UNINITIALIZED) { + mData.putInt(key.mPowerModelColumnIndex, otherPowerModel); + } else if (powerModel != otherPowerModel + && otherPowerModel != POWER_MODEL_UNINITIALIZED) { undefined = true; - } else { - final int powerModel = mData.getInt(key.mPowerModelColumnIndex); - int otherPowerModel = otherData.getInt(otherKey.mPowerModelColumnIndex); - if (powerModel == POWER_MODEL_UNINITIALIZED) { - mData.putInt(key.mPowerModelColumnIndex, otherPowerModel); - } else if (powerModel != otherPowerModel - && otherPowerModel != POWER_MODEL_UNINITIALIZED) { - undefined = true; - } } + } - if (undefined) { - mData.putInt(key.mPowerModelColumnIndex, - BatteryConsumer.POWER_MODEL_UNDEFINED); - } + if (undefined) { + mData.putInt(key.mPowerModelColumnIndex, + BatteryConsumer.POWER_MODEL_UNDEFINED); } } @@ -631,10 +754,8 @@ class PowerComponents { final int usageColumnIndex = mData.layout.firstCustomUsageDurationColumn + i; final int otherDurationColumnIndex = otherData.layout.firstCustomUsageDurationColumn + i; - mData.putLong(usageColumnIndex, - mData.getLong(usageColumnIndex) + otherData.getLong( - otherDurationColumnIndex) - ); + mData.putLong(usageColumnIndex, mData.getLong(usageColumnIndex) + + otherData.getLong(otherDurationColumnIndex)); } } @@ -647,7 +768,8 @@ class PowerComponents { for (int componentId = 0; componentId < BatteryConsumer.POWER_COMPONENT_COUNT; componentId++) { totalPowerMah += mData.getDouble( - mData.getKeyOrThrow(componentId, PROCESS_STATE_ANY).mPowerColumnIndex); + mData.layout.getKeyOrThrow(componentId, PROCESS_STATE_ANY, SCREEN_STATE_ANY, + POWER_STATE_ANY).mPowerColumnIndex); } for (int i = 0; i < mData.layout.customPowerComponentCount; i++) { totalPowerMah += mData.getDouble( @@ -661,19 +783,17 @@ class PowerComponents { */ @NonNull public PowerComponents build() { - for (BatteryConsumer.Key[] keys : mData.layout.keys) { - for (BatteryConsumer.Key key : keys) { - if (key.mPowerModelColumnIndex != POWER_MODEL_NOT_INCLUDED) { - if (mData.getInt(key.mPowerModelColumnIndex) == POWER_MODEL_UNINITIALIZED) { - mData.putInt(key.mPowerModelColumnIndex, - BatteryConsumer.POWER_MODEL_UNDEFINED); - } + for (BatteryConsumer.Key key: mData.layout.keys) { + if (key.mPowerModelColumnIndex != POWER_MODEL_NOT_INCLUDED) { + if (mData.getInt(key.mPowerModelColumnIndex) == POWER_MODEL_UNINITIALIZED) { + mData.putInt(key.mPowerModelColumnIndex, + BatteryConsumer.POWER_MODEL_UNDEFINED); } + } - if (mMinConsumedPowerThreshold != 0) { - if (mData.getDouble(key.mPowerColumnIndex) < mMinConsumedPowerThreshold) { - mData.putDouble(key.mPowerColumnIndex, 0); - } + if (mMinConsumedPowerThreshold != 0) { + if (mData.getDouble(key.mPowerColumnIndex) < mMinConsumedPowerThreshold) { + mData.putDouble(key.mPowerColumnIndex, 0); } } } diff --git a/core/java/android/os/TEST_MAPPING b/core/java/android/os/TEST_MAPPING index b5029a6aaff3..2fde5e7eff6a 100644 --- a/core/java/android/os/TEST_MAPPING +++ b/core/java/android/os/TEST_MAPPING @@ -119,7 +119,7 @@ "PowerComponents\\.java", "[^/]*BatteryConsumer[^/]*\\.java" ], - "name": "BatteryUsageStatsProtoTests" + "name": "PowerStatsTests" }, { "file_patterns": ["SharedMemory[^/]*\\.java"], diff --git a/core/java/android/os/UidBatteryConsumer.java b/core/java/android/os/UidBatteryConsumer.java index 53af838cb535..9b5a378d6591 100644 --- a/core/java/android/os/UidBatteryConsumer.java +++ b/core/java/android/os/UidBatteryConsumer.java @@ -140,12 +140,50 @@ public final class UidBatteryConsumer extends BatteryConsumer { skipEmptyComponents); appendProcessStateData(sb, BatteryConsumer.PROCESS_STATE_CACHED, skipEmptyComponents); - pw.print(sb); + pw.println(sb); + } else { + pw.println(); } - pw.print(" ( "); - mPowerComponents.dump(pw, skipEmptyComponents /* skipTotalPowerComponent */); - pw.print(" ) "); + pw.print(" "); + mPowerComponents.dump(pw, SCREEN_STATE_ANY, POWER_STATE_ANY, skipEmptyComponents); + + if (mData.layout.powerStateDataIncluded || mData.layout.screenStateDataIncluded) { + for (int powerState = 0; powerState < POWER_STATE_COUNT; powerState++) { + if (mData.layout.powerStateDataIncluded && powerState == POWER_STATE_UNSPECIFIED) { + continue; + } + + for (int screenState = 0; screenState < SCREEN_STATE_COUNT; screenState++) { + if (mData.layout.screenStateDataIncluded + && screenState == POWER_STATE_UNSPECIFIED) { + continue; + } + + final double consumedPower = mPowerComponents.getConsumedPower( + POWER_COMPONENT_ANY, + PROCESS_STATE_ANY, screenState, powerState); + if (consumedPower == 0) { + continue; + } + + pw.print(" ("); + if (powerState != POWER_STATE_UNSPECIFIED) { + pw.print(BatteryConsumer.powerStateToString(powerState)); + } + if (screenState != SCREEN_STATE_UNSPECIFIED) { + if (powerState != POWER_STATE_UNSPECIFIED) { + pw.print(", "); + } + pw.print("screen "); + pw.print(BatteryConsumer.screenStateToString(screenState)); + } + pw.print(") "); + mPowerComponents.dump(pw, screenState, powerState, + skipEmptyComponents /* skipTotalPowerComponent */); + } + } + } } private void appendProcessStateData(StringBuilder sb, @ProcessState int processState, @@ -160,10 +198,6 @@ public final class UidBatteryConsumer extends BatteryConsumer { .append(BatteryStats.formatCharge(power)); } - static UidBatteryConsumer create(BatteryConsumerData data) { - return new UidBatteryConsumer(data); - } - /** Serializes this object to XML */ void writeToXml(TypedXmlSerializer serializer) throws IOException { if (getConsumedPower() == 0) { diff --git a/core/java/android/os/UserBatteryConsumer.java b/core/java/android/os/UserBatteryConsumer.java index 23ba0c635eca..ea2be7b80e94 100644 --- a/core/java/android/os/UserBatteryConsumer.java +++ b/core/java/android/os/UserBatteryConsumer.java @@ -60,10 +60,10 @@ public class UserBatteryConsumer extends BatteryConsumer { pw.print("User "); pw.print(getUserId()); pw.print(": "); - pw.print(BatteryStats.formatCharge(consumedPower)); - pw.print(" ( "); - mPowerComponents.dump(pw, skipEmptyComponents /* skipTotalPowerComponent */); - pw.print(" ) "); + pw.println(BatteryStats.formatCharge(consumedPower)); + pw.print(" "); + mPowerComponents.dump(pw, SCREEN_STATE_ANY, POWER_STATE_ANY, + skipEmptyComponents /* skipTotalPowerComponent */); } /** Serializes this object to XML */ diff --git a/core/java/android/view/Choreographer.java b/core/java/android/view/Choreographer.java index f0e673b3e3ac..7e247493e35c 100644 --- a/core/java/android/view/Choreographer.java +++ b/core/java/android/view/Choreographer.java @@ -41,6 +41,7 @@ import android.util.TimeUtils; import android.view.animation.AnimationUtils; import java.io.PrintWriter; +import java.util.Locale; /** * Coordinates the timing of animations, input and drawing. @@ -200,6 +201,7 @@ public final class Choreographer { private final DisplayEventReceiver.VsyncEventData mLastVsyncEventData = new DisplayEventReceiver.VsyncEventData(); private final FrameData mFrameData = new FrameData(); + private volatile boolean mInDoFrameCallback = false; /** * Contains information about the current frame for jank-tracking, @@ -818,6 +820,11 @@ public final class Choreographer { * @hide */ public long getVsyncId() { + if (!mInDoFrameCallback && Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) { + String message = String.format(Locale.getDefault(), "unsync-vsync-id=%d isSfChoreo=%s", + mLastVsyncEventData.preferredFrameTimeline().vsyncId, this == getSfInstance()); + Trace.instant(Trace.TRACE_TAG_VIEW, message); + } return mLastVsyncEventData.preferredFrameTimeline().vsyncId; } @@ -853,6 +860,7 @@ public final class Choreographer { if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) { Trace.traceBegin( Trace.TRACE_TAG_VIEW, "Choreographer#doFrame " + timeline.mVsyncId); + mInDoFrameCallback = true; } synchronized (mLock) { if (!mFrameScheduled) { @@ -947,6 +955,7 @@ public final class Choreographer { doCallbacks(Choreographer.CALLBACK_COMMIT, frameIntervalNanos); } finally { AnimationUtils.unlockAnimationClock(); + mInDoFrameCallback = false; if (resynced) { Trace.traceEnd(Trace.TRACE_TAG_VIEW); } diff --git a/core/java/android/view/ImeInsetsSourceConsumer.java b/core/java/android/view/ImeInsetsSourceConsumer.java index 1d950dc44e46..6343313b2e01 100644 --- a/core/java/android/view/ImeInsetsSourceConsumer.java +++ b/core/java/android/view/ImeInsetsSourceConsumer.java @@ -119,9 +119,11 @@ public final class ImeInsetsSourceConsumer extends InsetsSourceConsumer { @Override public boolean applyLocalVisibilityOverride() { - ImeTracing.getInstance().triggerClientDump( - "ImeInsetsSourceConsumer#applyLocalVisibilityOverride", - mController.getHost().getInputMethodManager(), null /* icProto */); + if (!Flags.refactorInsetsController()) { + ImeTracing.getInstance().triggerClientDump( + "ImeInsetsSourceConsumer#applyLocalVisibilityOverride", + mController.getHost().getInputMethodManager(), null /* icProto */); + } return super.applyLocalVisibilityOverride(); } @@ -205,9 +207,13 @@ public final class ImeInsetsSourceConsumer extends InsetsSourceConsumer { @Override public void removeSurface() { - final IBinder window = mController.getHost().getWindowToken(); - if (window != null) { - getImm().removeImeSurface(window); + if (Flags.refactorInsetsController()) { + super.removeSurface(); + } else { + final IBinder window = mController.getHost().getWindowToken(); + if (window != null) { + getImm().removeImeSurface(window); + } } } diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java index df2af731037e..f166b89a1d13 100644 --- a/core/java/android/view/InsetsController.java +++ b/core/java/android/view/InsetsController.java @@ -765,7 +765,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation public InsetsController(Host host) { this(host, (controller, id, type) -> { - if (type == ime()) { + if (!Flags.refactorInsetsController() && type == ime()) { return new ImeInsetsSourceConsumer(id, controller.mState, Transaction::new, controller); } else { diff --git a/core/java/android/view/InsetsSourceConsumer.java b/core/java/android/view/InsetsSourceConsumer.java index c73cbc6e9a57..477e35b6e655 100644 --- a/core/java/android/view/InsetsSourceConsumer.java +++ b/core/java/android/view/InsetsSourceConsumer.java @@ -43,6 +43,7 @@ import android.view.inputmethod.Flags; import android.view.inputmethod.ImeTracker; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.inputmethod.ImeTracing; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -296,6 +297,13 @@ public class InsetsSourceConsumer { @VisibleForTesting(visibility = PACKAGE) public boolean applyLocalVisibilityOverride() { + if (Flags.refactorInsetsController()) { + if (mType == WindowInsets.Type.ime()) { + ImeTracing.getInstance().triggerClientDump( + "ImeInsetsSourceConsumer#applyLocalVisibilityOverride", + mController.getHost().getInputMethodManager(), null /* icProto */); + } + } final InsetsSource source = mState.peekSource(mId); if (source == null) { return false; @@ -396,6 +404,14 @@ public class InsetsSourceConsumer { */ public void removeSurface() { // no-op for types that always return ShowResult#SHOW_IMMEDIATELY. + if (Flags.refactorInsetsController()) { + if (mType == WindowInsets.Type.ime()) { + final IBinder window = mController.getHost().getWindowToken(); + if (window != null) { + mController.getHost().getInputMethodManager().removeImeSurface(window); + } + } + } } @VisibleForTesting(visibility = PACKAGE) diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 82a7e162dc2d..a23e3839c348 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -30,6 +30,7 @@ import static android.view.Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE; import static android.view.Surface.FRAME_RATE_COMPATIBILITY_GTE; import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD; import static android.view.accessibility.AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED; +import static android.view.accessibility.Flags.removeChildHoverCheckForTouchExploration; import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_INVALID_BOUNDS; import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_MISSING_WINDOW; import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_NOT_VISIBLE_ON_SCREEN; @@ -17486,9 +17487,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * Dispatching hover events to {@link TouchDelegate} to improve accessibility. * <p> * This method is dispatching hover events to the delegate target to support explore by touch. - * Similar to {@link ViewGroup#dispatchTouchEvent}, this method send proper hover events to + * Similar to {@link ViewGroup#dispatchTouchEvent}, this method sends proper hover events to * the delegate target according to the pointer and the touch area of the delegate while touch - * exploration enabled. + * exploration is enabled. * </p> * * @param event The motion event dispatch to the delegate target. @@ -17520,17 +17521,33 @@ public class View implements Drawable.Callback, KeyEvent.Callback, // hover events but receive accessibility focus, it should also not delegate to these // views when hovered. if (!oldHoveringTouchDelegate) { - if ((action == MotionEvent.ACTION_HOVER_ENTER - || action == MotionEvent.ACTION_HOVER_MOVE) - && !pointInHoveredChild(event) - && pointInDelegateRegion) { - mHoveringTouchDelegate = true; + if (removeChildHoverCheckForTouchExploration()) { + if ((action == MotionEvent.ACTION_HOVER_ENTER + || action == MotionEvent.ACTION_HOVER_MOVE) && pointInDelegateRegion) { + mHoveringTouchDelegate = true; + } + } else { + if ((action == MotionEvent.ACTION_HOVER_ENTER + || action == MotionEvent.ACTION_HOVER_MOVE) + && !pointInHoveredChild(event) + && pointInDelegateRegion) { + mHoveringTouchDelegate = true; + } } } else { - if (action == MotionEvent.ACTION_HOVER_EXIT - || (action == MotionEvent.ACTION_HOVER_MOVE + if (removeChildHoverCheckForTouchExploration()) { + if (action == MotionEvent.ACTION_HOVER_EXIT + || (action == MotionEvent.ACTION_HOVER_MOVE)) { + if (!pointInDelegateRegion) { + mHoveringTouchDelegate = false; + } + } + } else { + if (action == MotionEvent.ACTION_HOVER_EXIT + || (action == MotionEvent.ACTION_HOVER_MOVE && (pointInHoveredChild(event) || !pointInDelegateRegion))) { - mHoveringTouchDelegate = false; + mHoveringTouchDelegate = false; + } } } switch (action) { diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 596726f83c15..252834efa5b8 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -1135,6 +1135,8 @@ public final class ViewRootImpl implements ViewParent, // Take 24 and 30 as an example, 24 is not a divisor of 30. // We consider there is a conflict. private boolean mIsFrameRateConflicted = false; + // Used to check whether SurfaceControl has been replaced. + private boolean mSurfaceReplaced = false; // Used to set frame rate compatibility. @Surface.FrameRateCompatibility int mFrameRateCompatibility = FRAME_RATE_COMPATIBILITY_FIXED_SOURCE; @@ -3831,6 +3833,7 @@ public final class ViewRootImpl implements ViewParent, surfaceReplaced = (surfaceGenerationId != mSurface.getGenerationId() || surfaceControlChanged) && mSurface.isValid(); if (surfaceReplaced) { + mSurfaceReplaced = true; mSurfaceSequenceId++; } if (alwaysConsumeSystemBarsChanged) { @@ -4443,6 +4446,7 @@ public final class ViewRootImpl implements ViewParent, mPreferredFrameRate = -1; mIsFrameRateConflicted = false; mFrameRateCategoryChangeReason = FRAME_RATE_CATEGORY_REASON_UNKNOWN; + mSurfaceReplaced = false; } else if (mPreferredFrameRate == 0) { // From MSG_FRAME_RATE_SETTING, where mPreferredFrameRate is set to 0 setPreferredFrameRate(0); @@ -12933,8 +12937,9 @@ public final class ViewRootImpl implements ViewParent, boolean traceFrameRateCategory = false; try { - if (frameRateCategory != FRAME_RATE_CATEGORY_DEFAULT - && mLastPreferredFrameRateCategory != frameRateCategory) { + if ((frameRateCategory != FRAME_RATE_CATEGORY_DEFAULT + && mLastPreferredFrameRateCategory != frameRateCategory) + || mSurfaceReplaced) { traceFrameRateCategory = Trace.isTagEnabled(Trace.TRACE_TAG_VIEW); if (traceFrameRateCategory) { String reason = reasonToString(frameRateReason); @@ -12998,7 +13003,7 @@ public final class ViewRootImpl implements ViewParent, boolean traceFrameRate = false; try { - if (mLastPreferredFrameRate != preferredFrameRate) { + if (mLastPreferredFrameRate != preferredFrameRate || mSurfaceReplaced) { traceFrameRate = Trace.isTagEnabled(Trace.TRACE_TAG_VIEW); if (traceFrameRate) { Trace.traceBegin( diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.java b/core/java/android/view/accessibility/AccessibilityNodeInfo.java index a5ba294d6a19..90cfcb1e64a8 100644 --- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java +++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java @@ -982,6 +982,7 @@ public class AccessibilityNodeInfo implements Parcelable { private long mParentNodeId = UNDEFINED_NODE_ID; private long mLabelForId = UNDEFINED_NODE_ID; private long mLabeledById = UNDEFINED_NODE_ID; + private LongArray mLabeledByIds; private long mTraversalBefore = UNDEFINED_NODE_ID; private long mTraversalAfter = UNDEFINED_NODE_ID; @@ -3599,6 +3600,131 @@ public class AccessibilityNodeInfo implements Parcelable { } /** + * Adds the view which serves as the label of the view represented by + * this info for accessibility purposes. When more than one labels are + * added, the content from each label is combined in the order that + * they are added. + * <p> + * If visible text can be used to describe or give meaning to this UI, + * this method is preferred. For example, a TextView before an EditText + * in the UI usually specifies what information is contained in the + * EditText. Hence, the EditText is labelled by the TextView. + * </p> + * + * @param label A view that labels this node's source. + */ + @FlaggedApi(Flags.FLAG_SUPPORT_MULTIPLE_LABELEDBY) + public void addLabeledBy(@NonNull View label) { + addLabeledBy(label, AccessibilityNodeProvider.HOST_VIEW_ID); + } + + /** + * Adds the view which serves as the label of the view represented by + * this info for accessibility purposes. If <code>virtualDescendantId</code> + * is {@link View#NO_ID} the root is set as the label. When more than one + * labels are added, the content from each label is combined in the order + * that they are added. + * <p> + * A virtual descendant is an imaginary View that is reported as a part of the view + * hierarchy for accessibility purposes. This enables custom views that draw complex + * content to report themselves as a tree of virtual views, thus conveying their + * logical structure. + * </p> + * <p> + * If visible text can be used to describe or give meaning to this UI, + * this method is preferred. For example, a TextView before an EditText + * in the UI usually specifies what information is contained in the + * EditText. Hence, the EditText is labelled by the TextView. + * </p> + * <p> + * <strong>Note:</strong> Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * + * @param root A root whose virtual descendant labels this node's source. + * @param virtualDescendantId The id of the virtual descendant. + */ + @FlaggedApi(Flags.FLAG_SUPPORT_MULTIPLE_LABELEDBY) + public void addLabeledBy(@NonNull View root, int virtualDescendantId) { + enforceNotSealed(); + Preconditions.checkNotNull(root, "%s must not be null", root); + if (mLabeledByIds == null) { + mLabeledByIds = new LongArray(); + } + mLabeledById = makeNodeId(root.getAccessibilityViewId(), virtualDescendantId); + mLabeledByIds.add(mLabeledById); + } + + /** + * Gets the list of node infos which serve as the labels of the view represented by + * this info for accessibility purposes. + * + * @return The list of labels in the order that they were added. + */ + @FlaggedApi(Flags.FLAG_SUPPORT_MULTIPLE_LABELEDBY) + public @NonNull List<AccessibilityNodeInfo> getLabeledByList() { + enforceSealed(); + List<AccessibilityNodeInfo> labels = new ArrayList<>(); + if (mLabeledByIds == null) { + return labels; + } + for (int i = 0; i < mLabeledByIds.size(); i++) { + labels.add(getNodeForAccessibilityId(mConnectionId, mWindowId, mLabeledByIds.get(i))); + } + return labels; + } + + /** + * Removes a label. If the label was not previously added to the node, + * calling this method has no effect. + * <p> + * <strong>Note:</strong> Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * + * @param label The node which serves as this node's label. + * @return true if the label was present + * @see #addLabeledBy(View) + */ + @FlaggedApi(Flags.FLAG_SUPPORT_MULTIPLE_LABELEDBY) + public boolean removeLabeledBy(@NonNull View label) { + return removeLabeledBy(label, AccessibilityNodeProvider.HOST_VIEW_ID); + } + + /** + * Removes a virtual label which is a descendant of the given + * <code>root</code>. If the label was not previously added to the node, + * calling this method has no effect. + * + * @param root The root of the virtual subtree. + * @param virtualDescendantId The id of the virtual node which serves as this node's label. + * @return true if the label was present + * @see #addLabeledBy(View, int) + */ + @FlaggedApi(Flags.FLAG_SUPPORT_MULTIPLE_LABELEDBY) + public boolean removeLabeledBy(@NonNull View root, int virtualDescendantId) { + enforceNotSealed(); + final LongArray labeledByIds = mLabeledByIds; + if (labeledByIds == null) { + return false; + } + final int rootAccessibilityViewId = + (root != null) ? root.getAccessibilityViewId() : UNDEFINED_ITEM_ID; + final long labeledById = makeNodeId(rootAccessibilityViewId, virtualDescendantId); + if (mLabeledById == labeledById) { + mLabeledById = UNDEFINED_NODE_ID; + } + final int index = labeledByIds.indexOf(labeledById); + if (index < 0) { + return false; + } + labeledByIds.remove(index); + return true; + } + + /** * Sets the view which serves as the label of the view represented by * this info for accessibility purposes. * @@ -3631,7 +3757,17 @@ public class AccessibilityNodeInfo implements Parcelable { enforceNotSealed(); final int rootAccessibilityViewId = (root != null) ? root.getAccessibilityViewId() : UNDEFINED_ITEM_ID; + if (Flags.supportMultipleLabeledby()) { + if (mLabeledByIds == null) { + mLabeledByIds = new LongArray(); + } else { + mLabeledByIds.clear(); + } + } mLabeledById = makeNodeId(rootAccessibilityViewId, virtualDescendantId); + if (Flags.supportMultipleLabeledby()) { + mLabeledByIds.add(mLabeledById); + } } /** @@ -4242,6 +4378,12 @@ public class AccessibilityNodeInfo implements Parcelable { fieldIndex++; if (mLabeledById != DEFAULT.mLabeledById) nonDefaultFields |= bitAt(fieldIndex); fieldIndex++; + if (Flags.supportMultipleLabeledby()) { + if (!LongArray.elementsEqual(mLabeledByIds, DEFAULT.mLabeledByIds)) { + nonDefaultFields |= bitAt(fieldIndex); + } + fieldIndex++; + } if (mTraversalBefore != DEFAULT.mTraversalBefore) nonDefaultFields |= bitAt(fieldIndex); fieldIndex++; if (mTraversalAfter != DEFAULT.mTraversalAfter) nonDefaultFields |= bitAt(fieldIndex); @@ -4383,6 +4525,20 @@ public class AccessibilityNodeInfo implements Parcelable { if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeLong(mParentNodeId); if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeLong(mLabelForId); if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeLong(mLabeledById); + if (Flags.supportMultipleLabeledby()) { + if (isBitSet(nonDefaultFields, fieldIndex++)) { + final LongArray labeledByIds = mLabeledByIds; + if (labeledByIds == null) { + parcel.writeInt(0); + } else { + final int labeledByIdsSize = labeledByIds.size(); + parcel.writeInt(labeledByIdsSize); + for (int i = 0; i < labeledByIdsSize; i++) { + parcel.writeLong(labeledByIds.get(i)); + } + } + } + } if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeLong(mTraversalBefore); if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeLong(mTraversalAfter); if (isBitSet(nonDefaultFields, fieldIndex++)) { @@ -4550,6 +4706,9 @@ public class AccessibilityNodeInfo implements Parcelable { mParentNodeId = other.mParentNodeId; mLabelForId = other.mLabelForId; mLabeledById = other.mLabeledById; + if (Flags.supportMultipleLabeledby()) { + mLabeledByIds = other.mLabeledByIds; + } mTraversalBefore = other.mTraversalBefore; mTraversalAfter = other.mTraversalAfter; mMinDurationBetweenContentChanges = other.mMinDurationBetweenContentChanges; @@ -4656,6 +4815,20 @@ public class AccessibilityNodeInfo implements Parcelable { if (isBitSet(nonDefaultFields, fieldIndex++)) mParentNodeId = parcel.readLong(); if (isBitSet(nonDefaultFields, fieldIndex++)) mLabelForId = parcel.readLong(); if (isBitSet(nonDefaultFields, fieldIndex++)) mLabeledById = parcel.readLong(); + if (Flags.supportMultipleLabeledby()) { + if (isBitSet(nonDefaultFields, fieldIndex++)) { + final int labeledByIdsSize = parcel.readInt(); + if (labeledByIdsSize <= 0) { + mLabeledByIds = null; + } else { + mLabeledByIds = new LongArray(labeledByIdsSize); + for (int i = 0; i < labeledByIdsSize; i++) { + final long labeledById = parcel.readLong(); + mLabeledByIds.add(labeledById); + } + } + } + } if (isBitSet(nonDefaultFields, fieldIndex++)) mTraversalBefore = parcel.readLong(); if (isBitSet(nonDefaultFields, fieldIndex++)) mTraversalAfter = parcel.readLong(); if (isBitSet(nonDefaultFields, fieldIndex++)) { diff --git a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig index d0bc57b9afbe..ed2bf79c51f4 100644 --- a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig +++ b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig @@ -128,6 +128,13 @@ flag { } flag { + namespace: "accessibility" + name: "remove_child_hover_check_for_touch_exploration" + description: "Remove a check for a hovered child that prevents touch events from being delegated to non-direct descendants" + bug: "304770837" +} + +flag { name: "skip_accessibility_warning_dialog_for_trusted_services" namespace: "accessibility" description: "Skips showing the accessibility warning dialog for trusted services." @@ -177,6 +184,13 @@ flag { } flag { + name: "support_multiple_labeledby" + namespace: "accessibility" + description: "Feature flag for supporting multiple labels in AccessibilityNodeInfo labeledby api" + bug: "333780959" +} + +flag { name: "support_system_pinch_zoom_opt_out_apis" namespace: "accessibility" description: "Feature flag for declaring system pinch zoom opt-out apis" diff --git a/core/java/android/view/inputmethod/InputMethodInfo.java b/core/java/android/view/inputmethod/InputMethodInfo.java index 098f65575928..0e66f7ac47b7 100644 --- a/core/java/android/view/inputmethod/InputMethodInfo.java +++ b/core/java/android/view/inputmethod/InputMethodInfo.java @@ -891,12 +891,13 @@ public final class InputMethodInfo implements Parcelable { @FlaggedApi(android.view.inputmethod.Flags.FLAG_IME_SWITCHER_REVAMP_API) @Nullable public Intent createImeLanguageSettingsActivityIntent() { - if (TextUtils.isEmpty(mLanguageSettingsActivityName)) { + final var activityName = !TextUtils.isEmpty(mLanguageSettingsActivityName) + ? mLanguageSettingsActivityName : mSettingsActivityName; + if (TextUtils.isEmpty(activityName)) { return null; } return new Intent(ACTION_IME_LANGUAGE_SETTINGS).setComponent( - new ComponentName(getServiceInfo().packageName, - mLanguageSettingsActivityName) + new ComponentName(getServiceInfo().packageName, activityName) ); } diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index c9d2eecfb9b0..fed8eea97688 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -1352,12 +1352,16 @@ public final class InputMethodManager { case MSG_SET_VISIBILITY: final boolean visible = msg.arg1 != 0; synchronized (mH) { - if (visible) { - showSoftInput(mServedView, /* flags */ 0); - } else { - if (mCurRootView != null - && mCurRootView.getInsetsController() != null) { - mCurRootView.getInsetsController().hide(WindowInsets.Type.ime()); + if (mCurRootView != null) { + final var insetsController = mCurRootView.getInsetsController(); + if (insetsController != null) { + if (visible) { + insetsController.show(WindowInsets.Type.ime(), + false /* fromIme */, null /* statsToken */); + } else { + insetsController.hide(WindowInsets.Type.ime(), + false /* fromIme */, null /* statsToken */); + } } } } @@ -2334,16 +2338,18 @@ public final class InputMethodManager { ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED); if (Flags.refactorInsetsController()) { + final var viewRootImpl = view.getViewRootImpl(); // In case of a running show IME animation, it should not be requested visible, // otherwise the animation would jump and not be controlled by the user anymore - if ((mCurRootView.getInsetsController().computeUserAnimatingTypes() - & WindowInsets.Type.ime()) == 0) { + if (viewRootImpl != null + && (viewRootImpl.getInsetsController().computeUserAnimatingTypes() + & WindowInsets.Type.ime()) == 0) { // TODO(b/322992891) handle case of SHOW_IMPLICIT - view.getWindowInsetsController().show(WindowInsets.Type.ime()); + viewRootImpl.getInsetsController().show(WindowInsets.Type.ime(), + false /* fromIme */, statsToken); return true; - } else { - return false; } + return false; } else { // Makes sure to call ImeInsetsSourceConsumer#onShowRequested on the UI thread. // TODO(b/229426865): call WindowInsetsController#show instead. @@ -2497,7 +2503,10 @@ public final class InputMethodManager { if (Flags.refactorInsetsController()) { // TODO(b/322992891) handle case of HIDE_IMPLICIT_ONLY - servedView.getWindowInsetsController().hide(WindowInsets.Type.ime()); + final var viewRootImpl = servedView.getViewRootImpl(); + if (viewRootImpl != null) { + viewRootImpl.getInsetsController().hide(WindowInsets.Type.ime()); + } return true; } else { return IInputMethodManagerGlobalInvoker.hideSoftInput(mClient, windowToken, diff --git a/core/java/android/webkit/WebViewProviderInfo.java b/core/java/android/webkit/WebViewProviderInfo.java index 6629fdc4cdee..16727c30dfd4 100644 --- a/core/java/android/webkit/WebViewProviderInfo.java +++ b/core/java/android/webkit/WebViewProviderInfo.java @@ -23,6 +23,9 @@ import android.os.Parcel; import android.os.Parcelable; import android.util.Base64; +import java.util.Arrays; +import java.util.Objects; + /** * @hide */ @@ -80,6 +83,35 @@ public final class WebViewProviderInfo implements Parcelable { out.writeTypedArray(signatures, 0); } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o instanceof WebViewProviderInfo that) { + return this.packageName.equals(that.packageName) + && this.description.equals(that.description) + && this.availableByDefault == that.availableByDefault + && this.isFallback == that.isFallback + && Arrays.equals(this.signatures, that.signatures); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash(packageName, description, availableByDefault, + isFallback, Arrays.hashCode(signatures)); + } + + @Override + public String toString() { + return "WebViewProviderInfo; packageName=" + packageName + + " description=\"" + description + + "\" availableByDefault=" + availableByDefault + + " isFallback=" + isFallback + + " signatures=" + Arrays.toString(signatures); + } + // fields read from framework resource public final String packageName; public final String description; diff --git a/core/java/android/window/SnapshotDrawerUtils.java b/core/java/android/window/SnapshotDrawerUtils.java index 4c8bad6d0aff..205f1defccd1 100644 --- a/core/java/android/window/SnapshotDrawerUtils.java +++ b/core/java/android/window/SnapshotDrawerUtils.java @@ -412,8 +412,7 @@ public class SnapshotDrawerUtils { final WindowManager.LayoutParams attrs = Flags.drawSnapshotAspectRatioMatch() ? info.mainWindowLayoutParams : info.topOpaqueWindowLayoutParams; final WindowManager.LayoutParams mainWindowParams = info.mainWindowLayoutParams; - final InsetsState topWindowInsetsState = info.topOpaqueWindowInsetsState; - if (attrs == null || mainWindowParams == null || topWindowInsetsState == null) { + if (attrs == null || mainWindowParams == null) { Log.w(TAG, "unable to create taskSnapshot surface "); return null; } @@ -456,7 +455,10 @@ public class SnapshotDrawerUtils { return layoutParams; } - static Rect getSystemBarInsets(Rect frame, InsetsState state) { + static Rect getSystemBarInsets(Rect frame, @Nullable InsetsState state) { + if (state == null) { + return new Rect(); + } return state.calculateInsets(frame, WindowInsets.Type.systemBars(), false /* ignoreVisibility */).toRect(); } diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig index 3f1c06ac7e10..91ac4ff05687 100644 --- a/core/java/android/window/flags/lse_desktop_experience.aconfig +++ b/core/java/android/window/flags/lse_desktop_experience.aconfig @@ -101,6 +101,13 @@ flag { } flag { + name: "enable_cascading_windows" + namespace: "lse_desktop_experience" + description: "Whether to apply cascading effect for placing multiple windows when first launched" + bug: "325240051" +} + +flag { name: "enable_camera_compat_for_desktop_windowing" namespace: "lse_desktop_experience" description: "Whether to apply Camera Compat treatment to fixed-orientation apps in desktop windowing mode" @@ -129,6 +136,13 @@ flag { } flag { + name: "enable_caption_compat_inset_force_consumption_always" + namespace: "lse_desktop_experience" + description: "Enables force-consumption of caption bar insets for all apps in freeform" + bug: "352563889" +} + +flag { name: "show_desktop_windowing_dev_option" namespace: "lse_desktop_experience" description: "Whether to show developer option for enabling desktop windowing mode" diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig index 5397e91bd249..c451cc880a8c 100644 --- a/core/java/android/window/flags/windowing_frontend.aconfig +++ b/core/java/android/window/flags/windowing_frontend.aconfig @@ -19,6 +19,16 @@ flag { } flag { + name: "do_not_skip_ime_by_target_visibility" + namespace: "windowing_frontend" + description: "Avoid window traversal missing IME" + bug: "339375944" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "apply_lifecycle_on_pip_change" namespace: "windowing_frontend" description: "Make pip activity lifecyle change with windowing mode" diff --git a/core/java/com/android/internal/os/TEST_MAPPING b/core/java/com/android/internal/os/TEST_MAPPING index d552e0b8c643..ae43acfdedb6 100644 --- a/core/java/com/android/internal/os/TEST_MAPPING +++ b/core/java/com/android/internal/os/TEST_MAPPING @@ -18,7 +18,7 @@ "Kernel[^/]*\\.java", "[^/]*Power[^/]*\\.java" ], - "name": "BatteryUsageStatsProtoTests" + "name": "PowerStatsTests" }, { "file_patterns": [ diff --git a/core/java/com/android/internal/policy/FoldLockSettingsObserver.java b/core/java/com/android/internal/policy/FoldLockSettingsObserver.java new file mode 100644 index 000000000000..c6fba8a31277 --- /dev/null +++ b/core/java/com/android/internal/policy/FoldLockSettingsObserver.java @@ -0,0 +1,128 @@ +/* + * 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.policy; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.ContentObserver; +import android.os.Handler; +import android.os.UserHandle; +import android.provider.Settings; + +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import java.util.Set; + +/** + * A ContentObserver that listens for changes in the "Continue using apps on fold" setting. This + * setting determines a device's behavior when the user folds the device. + * @hide + * + * Keep the setting values in this class in sync with the values in + * {@link com.android.server.utils.FoldSettingProvider} and + * {@link com.android.settings.display.FoldLockBehaviorSettings} + */ +public class FoldLockSettingsObserver extends ContentObserver { + /** The setting for "stay awake on fold". */ + public static final String SETTING_VALUE_STAY_AWAKE_ON_FOLD = "stay_awake_on_fold_key"; + /** The setting for "swipe up to continue". */ + public static final String SETTING_VALUE_SELECTIVE_STAY_AWAKE = "selective_stay_awake_key"; + /** The setting for "always sleep on fold". */ + public static final String SETTING_VALUE_SLEEP_ON_FOLD = "sleep_on_fold_key"; + public static final String SETTING_VALUE_DEFAULT = SETTING_VALUE_SELECTIVE_STAY_AWAKE; + private static final Set<String> SETTING_VALUES = Set.of(SETTING_VALUE_STAY_AWAKE_ON_FOLD, + SETTING_VALUE_SELECTIVE_STAY_AWAKE, SETTING_VALUE_SLEEP_ON_FOLD); + + private final Context mContext; + + /** The cached value of the setting. */ + @VisibleForTesting + String mFoldLockSetting; + + public FoldLockSettingsObserver(Handler handler, Context context) { + super(handler); + mContext = context; + } + + /** Registers the observer and updates the cache for the first time. */ + public void register() { + final ContentResolver r = mContext.getContentResolver(); + r.registerContentObserver( + Settings.System.getUriFor(Settings.System.FOLD_LOCK_BEHAVIOR), + false, this, UserHandle.USER_ALL); + requestAndCacheFoldLockSetting(); + } + + /** Unregisters the observer. */ + public void unregister() { + mContext.getContentResolver().unregisterContentObserver(this); + } + + /** Runs when settings changes. */ + @Override + public void onChange(boolean selfChange) { + requestAndCacheFoldLockSetting(); + } + + /** + * Requests and caches the current FOLD_LOCK_BEHAVIOR setting, which should be one of three + * values: SETTING_VALUE_STAY_AWAKE_ON_FOLD, SETTING_VALUE_SELECTIVE_STAY_AWAKE, + * SETTING_VALUE_SLEEP_ON_FOLD. If null (not set), returns the system default. + */ + @VisibleForTesting + void requestAndCacheFoldLockSetting() { + String currentSetting = request(); + + if (currentSetting == null || !SETTING_VALUES.contains(currentSetting)) { + currentSetting = SETTING_VALUE_DEFAULT; + } + + setCurrentFoldSetting(currentSetting); + } + + /** + * Makes a binder call to request the current FOLD_LOCK_BEHAVIOR setting. + */ + @VisibleForTesting + @Nullable + String request() { + return Settings.System.getStringForUser(mContext.getContentResolver(), + Settings.System.FOLD_LOCK_BEHAVIOR, UserHandle.USER_CURRENT); + } + + /** Caches the fold-lock behavior received from Settings. */ + @VisibleForTesting + void setCurrentFoldSetting(String newSetting) { + mFoldLockSetting = newSetting; + } + + /** Used by external requesters: checks if the current setting is "stay awake on fold". */ + public boolean isStayAwakeOnFold() { + return mFoldLockSetting.equals(SETTING_VALUE_STAY_AWAKE_ON_FOLD); + } + + /** Used by external requesters: checks if the current setting is "swipe up to continue". */ + public boolean isSelectiveStayAwake() { + return mFoldLockSetting.equals(SETTING_VALUE_SELECTIVE_STAY_AWAKE); + } + + /** Used by external requesters: checks if the current setting is "sleep on fold". */ + public boolean isSleepOnFold() { + return mFoldLockSetting.equals(SETTING_VALUE_SLEEP_ON_FOLD); + } +} diff --git a/core/java/com/android/internal/protolog/IProtoLogClient.aidl b/core/java/com/android/internal/protolog/IProtoLogClient.aidl new file mode 100644 index 000000000000..969ed99d3aca --- /dev/null +++ b/core/java/com/android/internal/protolog/IProtoLogClient.aidl @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.protolog; + +/** + * The ProtoLog client interface. + * + * These clients will communicate bi-directionally with the ProtoLog service + * (@see IProtoLogService.aidl) running in the system process. + * + * {@hide} + */ +interface IProtoLogClient { + void toggleLogcat(boolean enabled, in String[] groups); +}
\ No newline at end of file diff --git a/core/java/com/android/internal/protolog/IProtoLogService.aidl b/core/java/com/android/internal/protolog/IProtoLogService.aidl new file mode 100644 index 000000000000..cc349ea2985a --- /dev/null +++ b/core/java/com/android/internal/protolog/IProtoLogService.aidl @@ -0,0 +1,51 @@ +/* + * 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 com.android.internal.protolog.IProtoLogClient; + +/** + * The ProtoLog Service interface. + * + * This service runs in system server. + * + * All ProtoLog clients (in theory one per process) will register themselves to this service. + * This service will then serve as the entry point for any shell command based interactions with + * protolog and get/forward any required information/actions from/to all the registered ProtoLog + * clients. + * + * Clients will be responsible for directly sending all their trace data to Perfetto without passing + * through this service, except viewer config data, the mappings from messages hashes (generated by + * the ProtoLog Tool if and when the tool processed the source code). So, this service is + * responsible for dumping the viewer config data from all clients. The reason for this is because + * we do this on Perfetto's onFlush callback when a tracing instance is stopped. However, if a + * client process is frozen then this will not dump; system server, where this service runs cannot + * be frozen. Secondly many processes (i.e. apps) will run the same code with the same viewer config + * data, so this service allows us to orchestrate which config gets dumped so we don't duplicate + * this information in the trace and waste valuable trace space. + * + * {@hide} + */ +interface IProtoLogService { + interface IRegisterClientArgs { + String[] getGroups(); + boolean[] getGroupsDefaultLogcatStatus(); + String getViewerConfigFile(); + } + + void registerClient(IProtoLogClient client, IRegisterClientArgs args); +}
\ No newline at end of file diff --git a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java index 652cba7ed00d..fbec1f104fc8 100644 --- a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java +++ b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java @@ -88,7 +88,7 @@ import java.util.concurrent.locks.ReentrantLock; /** * A service for the ProtoLog logging system. */ -public class PerfettoProtoLogImpl implements IProtoLog { +public class PerfettoProtoLogImpl extends IProtoLogClient.Stub implements IProtoLog { private static final String LOG_TAG = "ProtoLog"; public static final String NULL_STRING = "null"; private final AtomicInteger mTracingInstances = new AtomicInteger(); @@ -100,6 +100,7 @@ public class PerfettoProtoLogImpl implements IProtoLog { ); @Nullable private final ProtoLogViewerConfigReader mViewerConfigReader; + @Nullable private final ViewerConfigInputStreamProvider mViewerConfigInputStreamProvider; private final TreeMap<String, IProtoLogGroup> mLogGroups = new TreeMap<>(); private final Runnable mCacheUpdater; @@ -111,13 +112,12 @@ public class PerfettoProtoLogImpl implements IProtoLog { private final Lock mBackgroundServiceLock = new ReentrantLock(); private ExecutorService mBackgroundLoggingService = Executors.newSingleThreadExecutor(); - public PerfettoProtoLogImpl(String viewerConfigFilePath, Runnable cacheUpdater) { + public PerfettoProtoLogImpl(@NonNull String viewerConfigFilePath, Runnable cacheUpdater) { this(() -> { try { return new ProtoInputStream(new FileInputStream(viewerConfigFilePath)); } catch (FileNotFoundException e) { - Slog.w(LOG_TAG, "Failed to load viewer config file " + viewerConfigFilePath, e); - return null; + throw new RuntimeException("Failed to load viewer config file " + viewerConfigFilePath, e); } }, cacheUpdater); } @@ -127,7 +127,7 @@ public class PerfettoProtoLogImpl implements IProtoLog { } public PerfettoProtoLogImpl( - @Nullable ViewerConfigInputStreamProvider viewerConfigInputStreamProvider, + @NonNull ViewerConfigInputStreamProvider viewerConfigInputStreamProvider, Runnable cacheUpdater ) { this(viewerConfigInputStreamProvider, @@ -203,13 +203,23 @@ public class PerfettoProtoLogImpl implements IProtoLog { return mTracingInstances.get() > 0; } + @Override + public void toggleLogcat(boolean enabled, String[] groups) { + final ILogger logger = (message) -> Log.d(LOG_TAG, message); + if (enabled) { + startLoggingToLogcat(groups, logger); + } else { + stopLoggingToLogcat(groups, logger); + } + } + /** * Start text logging * @param groups Groups to start text logging for * @param logger A logger to write status updates to * @return status code */ - public int startLoggingToLogcat(String[] groups, ILogger logger) { + public int startLoggingToLogcat(String[] groups, @NonNull ILogger logger) { if (mViewerConfigReader != null) { mViewerConfigReader.loadViewerConfig(groups, logger); } @@ -222,7 +232,7 @@ public class PerfettoProtoLogImpl implements IProtoLog { * @param logger A logger to write status updates to * @return status code */ - public int stopLoggingToLogcat(String[] groups, ILogger logger) { + public int stopLoggingToLogcat(String[] groups, @NonNull ILogger logger) { if (mViewerConfigReader != null) { mViewerConfigReader.unloadViewerConfig(groups, logger); } @@ -242,13 +252,29 @@ public class PerfettoProtoLogImpl implements IProtoLog { for (IProtoLogGroup protoLogGroup : protoLogGroups) { mLogGroups.put(protoLogGroup.name(), protoLogGroup); } + + final String[] groupsLoggingToLogcat = Arrays.stream(protoLogGroups) + .filter(IProtoLogGroup::isLogToLogcat) + .map(IProtoLogGroup::name) + .toArray(String[]::new); + + if (mViewerConfigReader != null) { + mViewerConfigReader.loadViewerConfig(groupsLoggingToLogcat); + } } /** * Responds to a shell command. */ + @Deprecated public int onShellCommand(ShellCommand shell) { PrintWriter pw = shell.getOutPrintWriter(); + + if (android.tracing.Flags.clientSideProtoLogging()) { + pw.println("Command deprecated. Please use 'cmd protolog' instead."); + return -1; + } + String cmd = shell.getNextArg(); if (cmd == null) { return unknownCommand(pw); @@ -742,7 +768,8 @@ public class PerfettoProtoLogImpl implements IProtoLog { return -1; } - private synchronized void onTracingInstanceStart(ProtoLogDataSource.ProtoLogConfig config) { + private synchronized void onTracingInstanceStart( + int instanceIdx, ProtoLogDataSource.ProtoLogConfig config) { final LogLevel defaultLogFrom = config.getDefaultGroupConfig().logFrom; for (int i = defaultLogFrom.ordinal(); i < LogLevel.values().length; i++) { mDefaultLogLevelCounts[i]++; @@ -775,7 +802,8 @@ public class PerfettoProtoLogImpl implements IProtoLog { this.mTracingInstances.incrementAndGet(); } - private synchronized void onTracingInstanceStop(ProtoLogDataSource.ProtoLogConfig config) { + private synchronized void onTracingInstanceStop( + int instanceIdx, ProtoLogDataSource.ProtoLogConfig config) { this.mTracingInstances.decrementAndGet(); final LogLevel defaultLogFrom = config.getDefaultGroupConfig().logFrom; diff --git a/core/java/com/android/internal/protolog/ProtoLogDataSource.java b/core/java/com/android/internal/protolog/ProtoLogDataSource.java index 84f3237142b8..6dc6585b5caf 100644 --- a/core/java/com/android/internal/protolog/ProtoLogDataSource.java +++ b/core/java/com/android/internal/protolog/ProtoLogDataSource.java @@ -49,12 +49,12 @@ public class ProtoLogDataSource extends DataSource<ProtoLogDataSource.Instance, ProtoLogDataSource.TlsState, ProtoLogDataSource.IncrementalState> { - private final Consumer<ProtoLogConfig> mOnStart; + private final Instance.TracingInstanceStartCallback mOnStart; private final Runnable mOnFlush; - private final Consumer<ProtoLogConfig> mOnStop; + private final Instance.TracingInstanceStopCallback mOnStop; - public ProtoLogDataSource(Consumer<ProtoLogConfig> onStart, Runnable onFlush, - Consumer<ProtoLogConfig> onStop) { + public ProtoLogDataSource(Instance.TracingInstanceStartCallback onStart, Runnable onFlush, + Instance.TracingInstanceStopCallback onStop) { super("android.protolog"); this.mOnStart = onStart; this.mOnFlush = onFlush; @@ -267,20 +267,30 @@ public class ProtoLogDataSource extends DataSource<ProtoLogDataSource.Instance, public static class Instance extends DataSourceInstance { - private final Consumer<ProtoLogConfig> mOnStart; + public interface TracingInstanceStartCallback { + void run(int instanceIdx, ProtoLogConfig config); + } + + public interface TracingInstanceStopCallback { + void run(int instanceIdx, ProtoLogConfig config); + } + + private final TracingInstanceStartCallback mOnStart; private final Runnable mOnFlush; - private final Consumer<ProtoLogConfig> mOnStop; + private final TracingInstanceStopCallback mOnStop; private final ProtoLogConfig mConfig; + private final int mInstanceIndex; public Instance( DataSource<Instance, TlsState, IncrementalState> dataSource, int instanceIdx, ProtoLogConfig config, - Consumer<ProtoLogConfig> onStart, + TracingInstanceStartCallback onStart, Runnable onFlush, - Consumer<ProtoLogConfig> onStop + TracingInstanceStopCallback onStop ) { super(dataSource, instanceIdx); + this.mInstanceIndex = instanceIdx; this.mOnStart = onStart; this.mOnFlush = onFlush; this.mOnStop = onStop; @@ -289,7 +299,7 @@ public class ProtoLogDataSource extends DataSource<ProtoLogDataSource.Instance, @Override public void onStart(StartCallbackArguments args) { - this.mOnStart.accept(this.mConfig); + this.mOnStart.run(this.mInstanceIndex, this.mConfig); } @Override @@ -299,7 +309,7 @@ public class ProtoLogDataSource extends DataSource<ProtoLogDataSource.Instance, @Override public void onStop(StopCallbackArguments args) { - this.mOnStop.accept(this.mConfig); + this.mOnStop.run(this.mInstanceIndex, this.mConfig); } } } diff --git a/core/java/com/android/internal/protolog/ProtoLogImpl.java b/core/java/com/android/internal/protolog/ProtoLogImpl.java index 3082295a522c..77ca7ce91b22 100644 --- a/core/java/com/android/internal/protolog/ProtoLogImpl.java +++ b/core/java/com/android/internal/protolog/ProtoLogImpl.java @@ -30,6 +30,7 @@ import com.android.internal.protolog.common.IProtoLogGroup; import com.android.internal.protolog.common.LogLevel; import com.android.internal.protolog.common.ProtoLogToolInjected; +import java.io.File; import java.util.TreeMap; /** @@ -105,7 +106,15 @@ public class ProtoLogImpl { public static synchronized IProtoLog getSingleInstance() { if (sServiceInstance == null) { if (android.tracing.Flags.perfettoProtologTracing()) { - sServiceInstance = new PerfettoProtoLogImpl(sViewerConfigPath, sCacheUpdater); + File f = new File(sViewerConfigPath); + if (!ProtoLog.REQUIRE_PROTOLOGTOOL && !f.exists()) { + // TODO(b/353530422): Remove - temporary fix to unblock b/352290057 + // In so tests the viewer config file might not exist in which we don't + // want to provide config path to the user + sServiceInstance = new PerfettoProtoLogImpl(null, null, sCacheUpdater); + } else { + sServiceInstance = new PerfettoProtoLogImpl(sViewerConfigPath, sCacheUpdater); + } } else { sServiceInstance = new LegacyProtoLogImpl( sLegacyOutputFilePath, sLegacyViewerConfigPath, sCacheUpdater); diff --git a/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java b/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java index bb6c8b7a9698..38ca0d8f75e8 100644 --- a/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java +++ b/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java @@ -24,12 +24,13 @@ import java.util.Set; import java.util.TreeMap; public class ProtoLogViewerConfigReader { + @NonNull private final ViewerConfigInputStreamProvider mViewerConfigInputStreamProvider; private final Map<String, Set<Long>> mGroupHashes = new TreeMap<>(); private final LongSparseArray<String> mLogMessageMap = new LongSparseArray<>(); public ProtoLogViewerConfigReader( - ViewerConfigInputStreamProvider viewerConfigInputStreamProvider) { + @NonNull ViewerConfigInputStreamProvider viewerConfigInputStreamProvider) { this.mViewerConfigInputStreamProvider = viewerConfigInputStreamProvider; } diff --git a/core/java/com/android/internal/protolog/ViewerConfigInputStreamProvider.java b/core/java/com/android/internal/protolog/ViewerConfigInputStreamProvider.java index 334f5488425a..14bc8e4782f2 100644 --- a/core/java/com/android/internal/protolog/ViewerConfigInputStreamProvider.java +++ b/core/java/com/android/internal/protolog/ViewerConfigInputStreamProvider.java @@ -16,11 +16,13 @@ package com.android.internal.protolog; +import android.annotation.NonNull; import android.util.proto.ProtoInputStream; public interface ViewerConfigInputStreamProvider { /** * @return a ProtoInputStream. */ + @NonNull ProtoInputStream getInputStream(); } diff --git a/core/java/com/android/internal/widget/MaxHeightFrameLayout.java b/core/java/com/android/internal/widget/MaxHeightFrameLayout.java new file mode 100644 index 000000000000..d65dddd9c5b1 --- /dev/null +++ b/core/java/com/android/internal/widget/MaxHeightFrameLayout.java @@ -0,0 +1,98 @@ +/* + * 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.widget; + +import android.annotation.AttrRes; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.Px; +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.widget.FrameLayout; + +import com.android.internal.R; + +/** + * This custom subclass of FrameLayout enforces that its calculated height be no larger than the + * given maximum height (if any). + * + * @hide + */ +public class MaxHeightFrameLayout extends FrameLayout { + + private int mMaxHeight = Integer.MAX_VALUE; + + public MaxHeightFrameLayout(@NonNull Context context) { + this(context, null); + } + + public MaxHeightFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public MaxHeightFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs, + @AttrRes int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public MaxHeightFrameLayout(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + final TypedArray a = context.obtainStyledAttributes( + attrs, R.styleable.MaxHeightFrameLayout, defStyleAttr, defStyleRes); + saveAttributeDataForStyleable(context, R.styleable.MaxHeightFrameLayout, + attrs, a, defStyleAttr, defStyleRes); + + setMaxHeight(a.getDimensionPixelSize(R.styleable.MaxHeightFrameLayout_maxHeight, + Integer.MAX_VALUE)); + } + + /** + * Gets the maximum height of this view, in pixels. + * + * @see #setMaxHeight(int) + * + * @attr ref android.R.styleable#MaxHeightFrameLayout_maxHeight + */ + @Px + public int getMaxHeight() { + return mMaxHeight; + } + + /** + * Sets the maximum height this view can have. + * + * @param maxHeight the maximum height, in pixels + * + * @see #getMaxHeight() + * + * @attr ref android.R.styleable#MaxHeightFrameLayout_maxHeight + */ + public void setMaxHeight(@Px int maxHeight) { + mMaxHeight = maxHeight; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (MeasureSpec.getSize(heightMeasureSpec) > mMaxHeight) { + final int mode = MeasureSpec.getMode(heightMeasureSpec); + heightMeasureSpec = MeasureSpec.makeMeasureSpec(mMaxHeight, mode); + } + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } +} diff --git a/core/jni/android_media_AudioSystem.cpp b/core/jni/android_media_AudioSystem.cpp index c07fd3838837..7c62615cdc42 100644 --- a/core/jni/android_media_AudioSystem.cpp +++ b/core/jni/android_media_AudioSystem.cpp @@ -27,6 +27,7 @@ #include <android_media_audiopolicy.h> #include <android_os_Parcel.h> #include <audiomanager/AudioManager.h> +#include <android-base/properties.h> #include <binder/IBinder.h> #include <jni.h> #include <media/AidlConversion.h> @@ -41,8 +42,10 @@ #include <system/audio_policy.h> #include <utils/Log.h> +#include <thread> #include <optional> #include <sstream> +#include <memory> #include <vector> #include "android_media_AudioAttributes.h" @@ -261,6 +264,13 @@ static struct { jfieldID mMixerBehavior; } gAudioMixerAttributesField; +static struct { + jclass clazz; + jmethodID run; +} gRunnableClassInfo; + +static JavaVM* gVm; + static Mutex gLock; enum AudioError { @@ -3362,6 +3372,55 @@ static jboolean android_media_AudioSystem_isBluetoothVariableLatencyEnabled(JNIE return enabled; } +class JavaSystemPropertyListener { + public: + JavaSystemPropertyListener(JNIEnv* env, jobject javaCallback, std::string sysPropName) : + mCallback(env->NewGlobalRef(javaCallback)), + mCachedProperty(android::base::CachedProperty{std::move(sysPropName)}) { + mListenerThread = std::thread([this]() mutable { + JNIEnv* threadEnv = GetOrAttachJNIEnvironment(gVm); + while (!mCleanupSignal.load()) { + using namespace std::chrono_literals; + // 1s timeout so this thread can read the cleanup signal to (slowly) be able to + // be destroyed. + std::string newVal = mCachedProperty.WaitForChange(1000ms) ?: ""; + if (newVal != "" && mLastVal != newVal) { + threadEnv->CallVoidMethod(mCallback, gRunnableClassInfo.run); + mLastVal = std::move(newVal); + } + } + }); + } + + ~JavaSystemPropertyListener() { + mCleanupSignal.store(true); + mListenerThread.join(); + JNIEnv* env = GetOrAttachJNIEnvironment(gVm); + env->DeleteGlobalRef(mCallback); + } + + private: + jobject mCallback; + android::base::CachedProperty mCachedProperty; + std::thread mListenerThread; + std::atomic<bool> mCleanupSignal{false}; + std::string mLastVal = ""; +}; + +std::vector<std::unique_ptr<JavaSystemPropertyListener>> gSystemPropertyListeners; +std::mutex gSysPropLock{}; + +static void android_media_AudioSystem_listenForSystemPropertyChange(JNIEnv *env, jobject thiz, + jstring sysProp, + jobject javaCallback) { + ScopedUtfChars sysPropChars{env, sysProp}; + auto listener = std::make_unique<JavaSystemPropertyListener>(env, javaCallback, + std::string{sysPropChars.c_str()}); + std::unique_lock _l{gSysPropLock}; + gSystemPropertyListeners.push_back(std::move(listener)); +} + + // ---------------------------------------------------------------------------- #define MAKE_AUDIO_SYSTEM_METHOD(x) \ @@ -3534,7 +3593,12 @@ static const JNINativeMethod gMethods[] = android_media_AudioSystem_clearPreferredMixerAttributes), MAKE_AUDIO_SYSTEM_METHOD(supportsBluetoothVariableLatency), MAKE_AUDIO_SYSTEM_METHOD(setBluetoothVariableLatencyEnabled), - MAKE_AUDIO_SYSTEM_METHOD(isBluetoothVariableLatencyEnabled)}; + MAKE_AUDIO_SYSTEM_METHOD(isBluetoothVariableLatencyEnabled), + MAKE_JNI_NATIVE_METHOD("listenForSystemPropertyChange", + "(Ljava/lang/String;Ljava/lang/Runnable;)V", + android_media_AudioSystem_listenForSystemPropertyChange), + + }; static const JNINativeMethod gEventHandlerMethods[] = {MAKE_JNI_NATIVE_METHOD("native_setup", "(Ljava/lang/Object;)V", @@ -3816,6 +3880,12 @@ int register_android_media_AudioSystem(JNIEnv *env) gAudioMixerAttributesField.mMixerBehavior = GetFieldIDOrDie(env, audioMixerAttributesClass, "mMixerBehavior", "I"); + jclass runnableClazz = FindClassOrDie(env, "java/lang/Runnable"); + gRunnableClassInfo.clazz = MakeGlobalRefOrDie(env, runnableClazz); + gRunnableClassInfo.run = GetMethodIDOrDie(env, runnableClazz, "run", "()V"); + + LOG_ALWAYS_FATAL_IF(env->GetJavaVM(&gVm) != 0); + AudioSystem::addErrorCallback(android_media_AudioSystem_error_callback); RegisterMethodsOrDie(env, kClassPathName, gMethods, NELEM(gMethods)); diff --git a/core/jni/android_view_MotionEvent.cpp b/core/jni/android_view_MotionEvent.cpp index d32486c73db2..240be3fe5534 100644 --- a/core/jni/android_view_MotionEvent.cpp +++ b/core/jni/android_view_MotionEvent.cpp @@ -415,7 +415,7 @@ static void android_view_MotionEvent_nativeAddBatch(JNIEnv* env, jclass clazz, env->DeleteLocalRef(pointerCoordsObj); } - event->addSample(eventTimeNanos, rawPointerCoords.data()); + event->addSample(eventTimeNanos, rawPointerCoords.data(), event->getId()); event->setMetaState(event->getMetaState() | metaState); } diff --git a/core/jni/platform/host/HostRuntime.cpp b/core/jni/platform/host/HostRuntime.cpp index 30c926c57693..7e2a5ace7e64 100644 --- a/core/jni/platform/host/HostRuntime.cpp +++ b/core/jni/platform/host/HostRuntime.cpp @@ -17,6 +17,8 @@ #include <android-base/logging.h> #include <android-base/properties.h> #include <android/graphics/jni_runtime.h> +#include <android_runtime/AndroidRuntime.h> +#include <jni_wrappers.h> #include <nativehelper/JNIHelp.h> #include <nativehelper/jni_macros.h> #include <unicode/putil.h> @@ -27,9 +29,6 @@ #include <unordered_map> #include <vector> -#include "android_view_InputDevice.h" -#include "core_jni_helpers.h" -#include "jni.h" #ifdef _WIN32 #include <windows.h> #else @@ -38,8 +37,6 @@ #include <sys/stat.h> #endif -#include <iostream> - using namespace std; /* @@ -49,12 +46,6 @@ using namespace std; * (see AndroidRuntime.cpp). */ -static JavaVM* javaVM; -static jclass bridge; -static jclass layoutLog; -static jmethodID getLogId; -static jmethodID logMethodId; - extern int register_android_os_Binder(JNIEnv* env); extern int register_libcore_util_NativeAllocationRegistry_Delegate(JNIEnv* env); @@ -168,28 +159,9 @@ static int register_jni_procs(const std::unordered_map<std::string, RegJNIRec>& } } - if (register_android_graphics_classes(env) < 0) { - return -1; - } - return 0; } -int AndroidRuntime::registerNativeMethods(JNIEnv* env, const char* className, - const JNINativeMethod* gMethods, int numMethods) { - return jniRegisterNativeMethods(env, className, gMethods, numMethods); -} - -JNIEnv* AndroidRuntime::getJNIEnv() { - JNIEnv* env; - if (javaVM->GetEnv((void**)&env, JNI_VERSION_1_6) != JNI_OK) return nullptr; - return env; -} - -JavaVM* AndroidRuntime::getJavaVM() { - return javaVM; -} - static vector<string> parseCsv(const string& csvString) { vector<string> result; istringstream stream(csvString); @@ -200,29 +172,6 @@ static vector<string> parseCsv(const string& csvString) { return result; } -void LayoutlibLogger(base::LogId, base::LogSeverity severity, const char* tag, const char* file, - unsigned int line, const char* message) { - JNIEnv* env = AndroidRuntime::getJNIEnv(); - jint logPrio = severity; - jstring tagString = env->NewStringUTF(tag); - jstring messageString = env->NewStringUTF(message); - - jobject bridgeLog = env->CallStaticObjectMethod(bridge, getLogId); - - env->CallVoidMethod(bridgeLog, logMethodId, logPrio, tagString, messageString); - - env->DeleteLocalRef(tagString); - env->DeleteLocalRef(messageString); - env->DeleteLocalRef(bridgeLog); -} - -void LayoutlibAborter(const char* abort_message) { - // Layoutlib should not call abort() as it would terminate Studio. - // Throw an exception back to Java instead. - JNIEnv* env = AndroidRuntime::getJNIEnv(); - jniThrowRuntimeException(env, "The Android framework has encountered a fatal error"); -} - // This method has been copied/adapted from system/core/init/property_service.cpp // If the ro.product.cpu.abilist* properties have not been explicitly // set, derive them from ro.system.product.cpu.abilist* properties. @@ -311,62 +260,49 @@ static void* mmapFile(const char* dataFilePath) { #endif } -static bool init_icu(const char* dataPath) { - void* addr = mmapFile(dataPath); - UErrorCode err = U_ZERO_ERROR; - udata_setCommonData(addr, &err); - if (err != U_ZERO_ERROR) { - return false; +// Loads the ICU data file from the location specified in the system property ro.icu.data.path +static void loadIcuData() { + string icuPath = base::GetProperty("ro.icu.data.path", ""); + if (!icuPath.empty()) { + // Set the location of ICU data + void* addr = mmapFile(icuPath.c_str()); + UErrorCode err = U_ZERO_ERROR; + udata_setCommonData(addr, &err); + if (err != U_ZERO_ERROR) { + ALOGE("Unable to load ICU data\n"); + } } - return true; } -// Creates an array of InputDevice from key character map files -static void init_keyboard(JNIEnv* env, const vector<string>& keyboardPaths) { - jclass inputDevice = FindClassOrDie(env, "android/view/InputDevice"); - jobjectArray inputDevicesArray = - env->NewObjectArray(keyboardPaths.size(), inputDevice, nullptr); - int keyboardId = 1; - - for (const string& path : keyboardPaths) { - base::Result<std::shared_ptr<KeyCharacterMap>> charMap = - KeyCharacterMap::load(path, KeyCharacterMap::Format::BASE); - - InputDeviceInfo info = InputDeviceInfo(); - info.initialize(keyboardId, 0, 0, InputDeviceIdentifier(), - "keyboard " + std::to_string(keyboardId), true, false, - ui::LogicalDisplayId::DEFAULT); - info.setKeyboardType(AINPUT_KEYBOARD_TYPE_ALPHABETIC); - info.setKeyCharacterMap(*charMap); - - jobject inputDeviceObj = android_view_InputDevice_create(env, info); - if (inputDeviceObj) { - env->SetObjectArrayElement(inputDevicesArray, keyboardId - 1, inputDeviceObj); - env->DeleteLocalRef(inputDeviceObj); - } - keyboardId++; - } +static int register_android_core_classes(JNIEnv* env) { + jclass system = FindClassOrDie(env, "java/lang/System"); + jmethodID getPropertyMethod = + GetStaticMethodIDOrDie(env, system, "getProperty", + "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;"); - if (bridge == nullptr) { - bridge = FindClassOrDie(env, "com/android/layoutlib/bridge/Bridge"); - bridge = MakeGlobalRefOrDie(env, bridge); - } - jmethodID setInputManager = GetStaticMethodIDOrDie(env, bridge, "setInputManager", - "([Landroid/view/InputDevice;)V"); - env->CallStaticVoidMethod(bridge, setInputManager, inputDevicesArray); - env->DeleteLocalRef(inputDevicesArray); -} + // Get the names of classes that need to register their native methods + auto nativesClassesJString = + (jstring)env->CallStaticObjectMethod(system, getPropertyMethod, + env->NewStringUTF("core_native_classes"), + env->NewStringUTF("")); + const char* nativesClassesArray = env->GetStringUTFChars(nativesClassesJString, nullptr); + string nativesClassesString(nativesClassesArray); + vector<string> classesToRegister = parseCsv(nativesClassesString); + env->ReleaseStringUTFChars(nativesClassesJString, nativesClassesArray); -} // namespace android + if (register_jni_procs(gRegJNIMap, classesToRegister, env) < 0) { + return JNI_ERR; + } -using namespace android; + return 0; +} // Called right before aborting by LOG_ALWAYS_FATAL. Print the pending exception. void abort_handler(const char* abort_message) { ALOGE("About to abort the process..."); - JNIEnv* env = NULL; - if (javaVM->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) { + JNIEnv* env = AndroidRuntime::getJNIEnv(); + if (env == nullptr) { ALOGE("vm->GetEnv() failed"); return; } @@ -377,107 +313,98 @@ void abort_handler(const char* abort_message) { ALOGE("Aborting because: %s", abort_message); } -JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void*) { - javaVM = vm; - JNIEnv* env = nullptr; - if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) { - return JNI_ERR; - } - - __android_log_set_aborter(abort_handler); +// ------------------ Host implementation of AndroidRuntime ------------------ - init_android_graphics(); +/*static*/ JavaVM* AndroidRuntime::mJavaVM; - // Configuration is stored as java System properties. - // Get a reference to System.getProperty - jclass system = FindClassOrDie(env, "java/lang/System"); - jmethodID getPropertyMethod = - GetStaticMethodIDOrDie(env, system, "getProperty", - "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;"); +/*static*/ int AndroidRuntime::registerNativeMethods(JNIEnv* env, const char* className, + const JNINativeMethod* gMethods, + int numMethods) { + return jniRegisterNativeMethods(env, className, gMethods, numMethods); +} - // Java system properties that contain LayoutLib config. The initial values in the map - // are the default values if the property is not specified. - std::unordered_map<std::string, std::string> systemProperties = - {{"core_native_classes", ""}, - {"register_properties_during_load", ""}, - {"icu.data.path", ""}, - {"use_bridge_for_logging", ""}, - {"keyboard_paths", ""}}; - - for (auto& [name, defaultValue] : systemProperties) { - jstring propertyString = - (jstring)env->CallStaticObjectMethod(system, getPropertyMethod, - env->NewStringUTF(name.c_str()), - env->NewStringUTF(defaultValue.c_str())); - const char* propertyChars = env->GetStringUTFChars(propertyString, 0); - systemProperties[name] = string(propertyChars); - env->ReleaseStringUTFChars(propertyString, propertyChars); +/*static*/ JNIEnv* AndroidRuntime::getJNIEnv() { + JNIEnv* env; + JavaVM* vm = AndroidRuntime::getJavaVM(); + if (vm->GetEnv((void**)&env, JNI_VERSION_1_6) != JNI_OK) { + return nullptr; } - // Get the names of classes that need to register their native methods - vector<string> classesToRegister = parseCsv(systemProperties["core_native_classes"]); + return env; +} - if (systemProperties["register_properties_during_load"] == "true") { - // Set the system properties first as they could be used in the static initialization of - // other classes - if (register_android_os_SystemProperties(env) < 0) { - return JNI_ERR; - } - classesToRegister.erase(find(classesToRegister.begin(), classesToRegister.end(), - "android.os.SystemProperties")); - bridge = FindClassOrDie(env, "com/android/layoutlib/bridge/Bridge"); - bridge = MakeGlobalRefOrDie(env, bridge); - jmethodID setSystemPropertiesMethod = - GetStaticMethodIDOrDie(env, bridge, "setSystemProperties", "()V"); - env->CallStaticVoidMethod(bridge, setSystemPropertiesMethod); - property_initialize_ro_cpu_abilist(); - } +/*static*/ JavaVM* AndroidRuntime::getJavaVM() { + return mJavaVM; +} - if (register_jni_procs(gRegJNIMap, classesToRegister, env) < 0) { +/*static*/ int AndroidRuntime::startReg(JNIEnv* env) { + if (register_android_core_classes(env) < 0) { return JNI_ERR; } - - if (!systemProperties["icu.data.path"].empty()) { - // Set the location of ICU data - bool icuInitialized = init_icu(systemProperties["icu.data.path"].c_str()); - if (!icuInitialized) { - return JNI_ERR; - } + if (register_android_graphics_classes(env) < 0) { + return JNI_ERR; } + return 0; +} - if (systemProperties["use_bridge_for_logging"] == "true") { - layoutLog = FindClassOrDie(env, "com/android/ide/common/rendering/api/ILayoutLog"); - layoutLog = MakeGlobalRefOrDie(env, layoutLog); - logMethodId = GetMethodIDOrDie(env, layoutLog, "logAndroidFramework", - "(ILjava/lang/String;Ljava/lang/String;)V"); - if (bridge == nullptr) { - bridge = FindClassOrDie(env, "com/android/layoutlib/bridge/Bridge"); - bridge = MakeGlobalRefOrDie(env, bridge); - } - getLogId = GetStaticMethodIDOrDie(env, bridge, "getLog", - "()Lcom/android/ide/common/rendering/api/ILayoutLog;"); - android::base::SetLogger(LayoutlibLogger); - android::base::SetAborter(LayoutlibAborter); - } else { - // initialize logging, so ANDROD_LOG_TAGS env variable is respected - android::base::InitLogging(nullptr, android::base::StderrLogger); - } +void AndroidRuntime::onVmCreated(JNIEnv* env) { + env->GetJavaVM(&mJavaVM); +} + +void AndroidRuntime::onStarted() { + property_initialize_ro_cpu_abilist(); + loadIcuData(); // Use English locale for number format to ensure correct parsing of floats when using strtof setlocale(LC_NUMERIC, "en_US.UTF-8"); +} - if (!systemProperties["keyboard_paths"].empty()) { - vector<string> keyboardPaths = parseCsv(systemProperties["keyboard_paths"]); - init_keyboard(env, keyboardPaths); - } else { - fprintf(stderr, "Skip initializing keyboard\n"); +void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote) { + JNIEnv* env = AndroidRuntime::getJNIEnv(); + // Register native functions. + if (startReg(env) < 0) { + ALOGE("Unable to register all android native methods\n"); } + onStarted(); +} - return JNI_VERSION_1_6; +AndroidRuntime::AndroidRuntime(char* argBlockStart, const size_t argBlockLength) + : mExitWithoutCleanup(false), mArgBlockStart(argBlockStart), mArgBlockLength(argBlockLength) { + init_android_graphics(); } -JNIEXPORT void JNI_OnUnload(JavaVM* vm, void*) { +AndroidRuntime::~AndroidRuntime() {} + +// Version of AndroidRuntime to run on host +class HostRuntime : public AndroidRuntime { +public: + HostRuntime() : AndroidRuntime(nullptr, 0) {} + + void onVmCreated(JNIEnv* env) override { + AndroidRuntime::onVmCreated(env); + // initialize logging, so ANDROD_LOG_TAGS env variable is respected + android::base::InitLogging(nullptr, android::base::StderrLogger, abort_handler); + } + + void onStarted() override { + AndroidRuntime::onStarted(); + } +}; + +} // namespace android + +using namespace android; + +JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void*) { JNIEnv* env = nullptr; - vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6); - env->DeleteGlobalRef(bridge); - env->DeleteGlobalRef(layoutLog); + if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) { + return JNI_ERR; + } + + Vector<String8> args; + HostRuntime runtime; + + runtime.onVmCreated(env); + runtime.start("HostRuntime", args, false); + + return JNI_VERSION_1_6; } diff --git a/core/res/res/drawable/ic_ime_switcher_new.xml b/core/res/res/drawable/ic_ime_switcher_new.xml new file mode 100644 index 000000000000..04f4a250b3ec --- /dev/null +++ b/core/res/res/drawable/ic_ime_switcher_new.xml @@ -0,0 +1,26 @@ +<?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. +--> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="20dp" + android:height="20dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM18.92,8h-2.95c-0.32,-1.25 -0.78,-2.45 -1.38,-3.56 1.84,0.63 3.37,1.91 4.33,3.56zM12,4.04c0.83,1.2 1.48,2.53 1.91,3.96h-3.82c0.43,-1.43 1.08,-2.76 1.91,-3.96zM4.26,14C4.1,13.36 4,12.69 4,12s0.1,-1.36 0.26,-2h3.38c-0.08,0.66 -0.14,1.32 -0.14,2 0,0.68 0.06,1.34 0.14,2L4.26,14zM5.08,16h2.95c0.32,1.25 0.78,2.45 1.38,3.56 -1.84,-0.63 -3.37,-1.9 -4.33,-3.56zM8.03,8L5.08,8c0.96,-1.66 2.49,-2.93 4.33,-3.56C8.81,5.55 8.35,6.75 8.03,8zM12,19.96c-0.83,-1.2 -1.48,-2.53 -1.91,-3.96h3.82c-0.43,1.43 -1.08,2.76 -1.91,3.96zM14.34,14L9.66,14c-0.09,-0.66 -0.16,-1.32 -0.16,-2 0,-0.68 0.07,-1.35 0.16,-2h4.68c0.09,0.65 0.16,1.32 0.16,2 0,0.68 -0.07,1.34 -0.16,2zM14.59,19.56c0.6,-1.11 1.06,-2.31 1.38,-3.56h2.95c-0.96,1.65 -2.49,2.93 -4.33,3.56zM16.36,14c0.08,-0.66 0.14,-1.32 0.14,-2 0,-0.68 -0.06,-1.34 -0.14,-2h3.38c0.16,0.64 0.26,1.31 0.26,2s-0.1,1.36 -0.26,2h-3.38z" + android:fillColor="#FFFFFFFF"/> +</vector> diff --git a/core/res/res/drawable/input_method_switch_button.xml b/core/res/res/drawable/input_method_switch_button.xml new file mode 100644 index 000000000000..396d81ed87f6 --- /dev/null +++ b/core/res/res/drawable/input_method_switch_button.xml @@ -0,0 +1,42 @@ +<?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. +--> + +<inset xmlns:android="http://schemas.android.com/apk/res/android" + android:insetTop="6dp" + android:insetBottom="6dp"> + <ripple android:color="?android:attr/colorControlHighlight"> + <item android:id="@android:id/mask"> + <shape android:shape="rectangle"> + <corners android:radius="28dp"/> + <solid android:color="@color/white"/> + </shape> + </item> + + <item> + <shape android:shape="rectangle"> + <corners android:radius="28dp"/> + <solid android:color="@color/transparent"/> + <stroke android:color="?attr/materialColorPrimary" + android:width="1dp"/> + <padding android:left="16dp" + android:top="8dp" + android:right="16dp" + android:bottom="8dp"/> + </shape> + </item> + </ripple> +</inset> diff --git a/core/res/res/drawable/input_method_switch_item_background.xml b/core/res/res/drawable/input_method_switch_item_background.xml new file mode 100644 index 000000000000..eb7a24691f37 --- /dev/null +++ b/core/res/res/drawable/input_method_switch_item_background.xml @@ -0,0 +1,37 @@ +<?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. +--> + +<ripple xmlns:android="http://schemas.android.com/apk/res/android" + android:color="@color/list_highlight_material"> + <item android:id="@id/mask"> + <shape android:shape="rectangle"> + <corners android:radius="28dp"/> + <solid android:color="@color/white"/> + </shape> + </item> + + <item> + <selector> + <item android:state_activated="true"> + <shape android:shape="rectangle"> + <corners android:radius="28dp"/> + <solid android:color="?attr/materialColorSecondaryContainer"/> + </shape> + </item> + </selector> + </item> +</ripple> diff --git a/core/res/res/layout/input_method_switch_dialog_new.xml b/core/res/res/layout/input_method_switch_dialog_new.xml new file mode 100644 index 000000000000..5a4d6b14a52b --- /dev/null +++ b/core/res/res/layout/input_method_switch_dialog_new.xml @@ -0,0 +1,70 @@ +<?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. +--> + +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <com.android.internal.widget.MaxHeightFrameLayout + android:layout_width="320dp" + android:layout_height="0dp" + android:layout_weight="1" + android:maxHeight="373dp"> + + <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"/> + + </com.android.internal.widget.MaxHeightFrameLayout> + + <LinearLayout + style="?android:attr/buttonBarStyle" + android:id="@+id/button_bar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:paddingHorizontal="16dp" + android:paddingTop="8dp" + android:paddingBottom="16dp" + android:visibility="gone"> + + <Space + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1"/> + + <Button + style="?attr/buttonBarButtonStyle" + android:id="@+id/button1" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:background="@drawable/input_method_switch_button" + android:layout_gravity="end" + android:text="@string/input_method_language_settings" + android:fontFamily="google-sans-text" + android:textAppearance="?attr/textAppearance" + android:visibility="gone"/> + + </LinearLayout> + +</LinearLayout> diff --git a/core/res/res/layout/input_method_switch_item_new.xml b/core/res/res/layout/input_method_switch_item_new.xml new file mode 100644 index 000000000000..16a97c4b39ee --- /dev/null +++ b/core/res/res/layout/input_method_switch_item_new.xml @@ -0,0 +1,88 @@ +<?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. +--> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingHorizontal="16dp" + android:paddingBottom="8dp"> + + <View + android:id="@+id/divider" + android:layout_width="match_parent" + android:layout_height="1dp" + android:background="?attr/materialColorSurfaceVariant" + android:layout_marginStart="20dp" + android:layout_marginTop="8dp" + android:layout_marginEnd="24dp" + android:layout_marginBottom="12dp" + android:visibility="gone"/> + + <TextView + android:id="@+id/header_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:padding="8dp" + android:ellipsize="end" + android:singleLine="true" + android:fontFamily="google-sans-text" + android:textAppearance="?attr/textAppearance" + android:textColor="?attr/materialColorPrimary" + android:visibility="gone"/> + + <LinearLayout + android:id="@+id/list_item" + android:layout_width="match_parent" + android:layout_height="72dp" + android:background="@drawable/input_method_switch_item_background" + android:gravity="center_vertical" + android:orientation="horizontal" + android:paddingStart="20dp" + android:paddingEnd="24dp"> + + <LinearLayout + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="1" + android:gravity="start|center_vertical" + android:orientation="vertical"> + + <TextView + android:id="@+id/text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:ellipsize="marquee" + android:singleLine="true" + android:fontFamily="google-sans-text" + android:textAppearance="?attr/textAppearanceListItem"/> + + </LinearLayout> + + <ImageView + android:id="@+id/image" + android:layout_width="24dp" + android:layout_height="24dp" + android:gravity="center_vertical" + android:layout_marginStart="12dp" + android:src="@drawable/ic_check_24dp" + android:tint="?attr/materialColorOnSurface" + android:visibility="gone"/> + + </LinearLayout> + +</LinearLayout> diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index 0975eda3f9ff..7cc9e13db5cf 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -5243,6 +5243,11 @@ the VISIBLE or INVISIBLE state when measuring. Defaults to false. --> <attr name="measureAllChildren" format="boolean" /> </declare-styleable> + <!-- @hide --> + <declare-styleable name="MaxHeightFrameLayout"> + <!-- An optional argument to supply a maximum height for this view. --> + <attr name="maxHeight" format="dimension" /> + </declare-styleable> <declare-styleable name="ExpandableListView"> <!-- Indicator shown beside the group View. This can be a stateful Drawable. --> <attr name="groupIndicator" format="reference" /> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index dc3d9355f148..236e7c5b7a62 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -7090,4 +7090,7 @@ <!-- Whether to show GAIA education screen during account login of private space setup. OEM/Partner can explicitly opt to disable the screen. --> <bool name="config_enableGaiaEducationInPrivateSpace">true</bool> + + <!-- Whether to enable usb state update via udc sysfs. --> + <bool name="config_enableUdcSysfsUsbStateUpdate">false</bool> </resources> diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 46b154163224..ec865f6c376f 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -3880,6 +3880,8 @@ <!-- Title of the pop-up dialog in which the user switches keyboard, also known as input method. --> <string name="select_input_method">Choose input method</string> + <!-- Button to access the language settings of the current input method. [CHAR LIMIT=50]--> + <string name="input_method_language_settings">Language Settings</string> <!-- Summary text of a toggle switch to enable/disable use of the IME while a physical keyboard is connected --> <string name="show_ime">Keep it on screen while physical keyboard is active</string> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index c50b961f74cd..09688f2f7bec 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -523,6 +523,7 @@ <java-symbol type="integer" name="config_defaultAnalogClockSecondsHandFps"/> <java-symbol type="bool" name="config_notificationCloseButtonSupported"/> <java-symbol type="bool" name="config_enableGaiaEducationInPrivateSpace"/> + <java-symbol type="bool" name="config_enableUdcSysfsUsbStateUpdate"/> <java-symbol type="color" name="tab_indicator_text_v4" /> @@ -1577,6 +1578,8 @@ <java-symbol type="layout" name="input_method" /> <java-symbol type="layout" name="input_method_extract_view" /> <java-symbol type="layout" name="input_method_switch_item" /> + <java-symbol type="layout" name="input_method_switch_item_new" /> + <java-symbol type="layout" name="input_method_switch_dialog_new" /> <java-symbol type="layout" name="input_method_switch_dialog_title" /> <java-symbol type="layout" name="js_prompt" /> <java-symbol type="layout" name="list_content_simple" /> @@ -2552,6 +2555,7 @@ <java-symbol type="dimen" name="input_method_nav_key_button_ripple_max_width" /> <java-symbol type="drawable" name="ic_ime_nav_back" /> <java-symbol type="drawable" name="ic_ime_switcher" /> + <java-symbol type="drawable" name="ic_ime_switcher_new" /> <java-symbol type="id" name="input_method_nav_back" /> <java-symbol type="id" name="input_method_nav_buttons" /> <java-symbol type="id" name="input_method_nav_center_group" /> @@ -5400,6 +5404,7 @@ <java-symbol type="style" name="Theme.DeviceDefault.DialogWhenLarge" /> <java-symbol type="style" name="Theme.DeviceDefault.DocumentsUI" /> <java-symbol type="style" name="Theme.DeviceDefault.InputMethod" /> + <java-symbol type="style" name="Theme.DeviceDefault.InputMethodSwitcherDialog" /> <java-symbol type="style" name="Theme.DeviceDefault.Light.DarkActionBar" /> <java-symbol type="style" name="Theme.DeviceDefault.Light.Dialog.FixedSize" /> <java-symbol type="style" name="Theme.DeviceDefault.Light.Dialog.MinWidth" /> diff --git a/core/res/res/values/themes_device_defaults.xml b/core/res/res/values/themes_device_defaults.xml index 382ff0441fd2..f5c67387cb92 100644 --- a/core/res/res/values/themes_device_defaults.xml +++ b/core/res/res/values/themes_device_defaults.xml @@ -6179,4 +6179,10 @@ easier. <item name="colorListDivider">@color/list_divider_opacity_device_default_light</item> <item name="opacityListDivider">@color/list_divider_opacity_device_default_light</item> </style> + + <!-- Device default theme for the Input Method Switcher dialog. --> + <style name="Theme.DeviceDefault.InputMethodSwitcherDialog" parent="Theme.DeviceDefault.Dialog.Alert.DayNight"> + <item name="windowMinWidthMajor">@null</item> + <item name="windowMinWidthMinor">@null</item> + </style> </resources> diff --git a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/CursorAnchorInfoTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/CursorAnchorInfoTest.java index 9d7d71d8d539..616c72ec6078 100644 --- a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/CursorAnchorInfoTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/CursorAnchorInfoTest.java @@ -20,8 +20,8 @@ import static org.junit.Assert.assertEquals; import android.graphics.Matrix; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/DeleteRangeGestureTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/DeleteRangeGestureTest.java index d7b911dda672..d0a4141eb9ed 100644 --- a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/DeleteRangeGestureTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/DeleteRangeGestureTest.java @@ -22,8 +22,8 @@ import static org.junit.Assert.assertNotNull; import android.graphics.RectF; import android.platform.test.annotations.Presubmit; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import com.android.compatibility.common.util.ApiTest; diff --git a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/EditorInfoTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/EditorInfoTest.java index 4839dd27b283..013117e89c3e 100644 --- a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/EditorInfoTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/EditorInfoTest.java @@ -45,8 +45,8 @@ import android.util.StringBuilderPrinter; import android.view.MotionEvent; import android.view.autofill.AutofillId; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodInfoTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodInfoTest.java index ce85a76f478d..61bf1378fc36 100644 --- a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodInfoTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodInfoTest.java @@ -28,9 +28,9 @@ import android.os.Bundle; import android.os.Parcel; import android.platform.test.flag.junit.SetFlagsRule; -import androidx.test.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; import com.android.frameworks.inputmethodcoretests.R; @@ -135,7 +135,7 @@ public class InputMethodInfoTest { private InputMethodInfo buildInputMethodForTest(final @XmlRes int metaDataRes) throws Exception { - final Context context = InstrumentationRegistry.getContext(); + final Context context = InstrumentationRegistry.getInstrumentation().getContext(); final ServiceInfo serviceInfo = new ServiceInfo(); serviceInfo.applicationInfo = context.getApplicationInfo(); serviceInfo.packageName = context.getPackageName(); diff --git a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodManagerTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodManagerTest.java index d70572444128..812b3f5cd153 100644 --- a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodManagerTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodManagerTest.java @@ -24,9 +24,9 @@ import static org.junit.Assert.assertNotNull; import android.content.Context; import android.hardware.display.DisplayManager; -import androidx.test.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodSubtypeArrayTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodSubtypeArrayTest.java index e7b1110f898a..73ff30408a7b 100644 --- a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodSubtypeArrayTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodSubtypeArrayTest.java @@ -26,8 +26,8 @@ import android.os.Parcel; import android.platform.test.annotations.Presubmit; import android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodSubtypeTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodSubtypeTest.java index 5095cad1b607..4c76992cbcac 100644 --- a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodSubtypeTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodSubtypeTest.java @@ -27,8 +27,8 @@ import android.annotation.Nullable; import android.os.Parcel; import android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InsertGestureTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InsertGestureTest.java index 47a724d36038..608dd4d9fca1 100644 --- a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InsertGestureTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InsertGestureTest.java @@ -22,8 +22,8 @@ import static org.junit.Assert.assertNotNull; import android.graphics.PointF; import android.platform.test.annotations.Presubmit; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import com.android.compatibility.common.util.ApiTest; diff --git a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InsertModeGestureTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InsertModeGestureTest.java index a94f8772fcd0..bb6a944ba17f 100644 --- a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InsertModeGestureTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InsertModeGestureTest.java @@ -24,8 +24,8 @@ import android.os.CancellationSignal; import android.os.CancellationSignalBeamer; import android.platform.test.annotations.Presubmit; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import com.android.compatibility.common.util.ApiTest; diff --git a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/SelectGestureTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/SelectGestureTest.java index b2eb07c0a9e7..4cbd7ab1ef65 100644 --- a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/SelectGestureTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/SelectGestureTest.java @@ -22,8 +22,8 @@ import static org.junit.Assert.assertNotNull; import android.graphics.RectF; import android.platform.test.annotations.Presubmit; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import com.android.compatibility.common.util.ApiTest; diff --git a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/SelectRangeGestureTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/SelectRangeGestureTest.java index df63a4aaaefe..c1e2197b8d85 100644 --- a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/SelectRangeGestureTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/SelectRangeGestureTest.java @@ -22,8 +22,8 @@ import static org.junit.Assert.assertNotNull; import android.graphics.RectF; import android.platform.test.annotations.Presubmit; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import com.android.compatibility.common.util.ApiTest; diff --git a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/SparseRectFArrayTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/SparseRectFArrayTest.java index f264cc630dc5..724d729c950f 100644 --- a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/SparseRectFArrayTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/SparseRectFArrayTest.java @@ -25,8 +25,8 @@ import android.graphics.RectF; import android.os.Parcel; import android.view.inputmethod.SparseRectFArray.SparseRectFArrayBuilder; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/InputConnectionWrapperTest.java b/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/InputConnectionWrapperTest.java index a6262944e8b0..9eb5dd1a2422 100644 --- a/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/InputConnectionWrapperTest.java +++ b/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/InputConnectionWrapperTest.java @@ -22,8 +22,8 @@ import android.platform.test.annotations.Presubmit; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputConnectionWrapper; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/InputMethodDebugTest.java b/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/InputMethodDebugTest.java index 32bfdcb7b217..6ac36398ad55 100644 --- a/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/InputMethodDebugTest.java +++ b/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/InputMethodDebugTest.java @@ -20,8 +20,8 @@ import static org.junit.Assert.assertEquals; import android.view.WindowManager.LayoutParams; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/SubtypeLocaleUtilsTest.java b/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/SubtypeLocaleUtilsTest.java index ba6390808151..7b0a5dacbea0 100644 --- a/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/SubtypeLocaleUtilsTest.java +++ b/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/SubtypeLocaleUtilsTest.java @@ -18,8 +18,8 @@ package com.android.internal.inputmethod; import static org.junit.Assert.assertEquals; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/core/tests/coretests/src/android/animation/OWNERS b/core/tests/coretests/src/android/animation/OWNERS new file mode 100644 index 000000000000..1eefb3a3dc65 --- /dev/null +++ b/core/tests/coretests/src/android/animation/OWNERS @@ -0,0 +1 @@ +include /core/java/android/animation/OWNERS diff --git a/core/tests/coretests/src/android/inputmethodservice/ImsConfigurationTrackerTest.java b/core/tests/coretests/src/android/inputmethodservice/ImsConfigurationTrackerTest.java index 6998c32978c1..f5e9cc6953fb 100644 --- a/core/tests/coretests/src/android/inputmethodservice/ImsConfigurationTrackerTest.java +++ b/core/tests/coretests/src/android/inputmethodservice/ImsConfigurationTrackerTest.java @@ -26,8 +26,8 @@ import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.platform.test.annotations.RequiresFlagsEnabled; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import org.junit.Before; import org.junit.Test; diff --git a/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java b/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java index 47b288993d11..248db65d7435 100644 --- a/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java +++ b/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java @@ -37,8 +37,12 @@ import android.graphics.Insets; import android.graphics.Point; import android.graphics.Rect; import android.platform.test.annotations.Presubmit; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.view.WindowManager.BadTokenException; import android.view.WindowManager.LayoutParams; +import android.view.inputmethod.Flags; import android.view.inputmethod.ImeTracker; import android.widget.TextView; @@ -46,6 +50,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.platform.app.InstrumentationRegistry; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; @@ -61,6 +66,9 @@ import org.mockito.Spy; @RunWith(AndroidJUnit4.class) public class ImeInsetsSourceConsumerTest { + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + Context mContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); InsetsSourceConsumer mImeConsumer; @Spy InsetsController mController; @@ -112,6 +120,7 @@ public class ImeInsetsSourceConsumerTest { } @Test + @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) public void testImeRequestedVisibleAwaitingControl() { // Set null control and then request show. mController.onControlsChanged(new InsetsSourceControl[] { null }); @@ -141,6 +150,7 @@ public class ImeInsetsSourceConsumerTest { } @Test + @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) public void testImeRequestedVisibleAwaitingLeash() { // Set null control, then request show. mController.onControlsChanged(new InsetsSourceControl[] { null }); @@ -185,6 +195,7 @@ public class ImeInsetsSourceConsumerTest { } @Test + @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) public void testImeGetAndClearSkipAnimationOnce_expectSkip() { // Expect IME animation will skipped when the IME is visible at first place. verifyImeGetAndClearSkipAnimationOnce(true /* hasWindowFocus */, true /* hasViewFocus */, @@ -192,6 +203,7 @@ public class ImeInsetsSourceConsumerTest { } @Test + @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) public void testImeGetAndClearSkipAnimationOnce_expectNoSkip() { // Expect IME animation will not skipped if previously no view focused when gained the // window focus and requesting the IME visible next time. diff --git a/core/tests/coretests/src/android/view/ViewFrameRateTest.java b/core/tests/coretests/src/android/view/ViewFrameRateTest.java index b68ff78107ca..62291d405a60 100644 --- a/core/tests/coretests/src/android/view/ViewFrameRateTest.java +++ b/core/tests/coretests/src/android/view/ViewFrameRateTest.java @@ -237,8 +237,8 @@ public class ViewFrameRateTest { return; } waitForFrameRateCategoryToSettle(); - assertEquals(FRAME_RATE_CATEGORY_LOW, - mViewRoot.getLastPreferredFrameRateCategory()); + assertTrue(mViewRoot.getLastPreferredFrameRateCategory() + < FRAME_RATE_CATEGORY_HIGH_HINT); int width = mMovingView.getWidth(); int height = mMovingView.getHeight(); diff --git a/core/tests/coretests/src/android/view/accessibility/AccessibilityNodeInfoTest.java b/core/tests/coretests/src/android/view/accessibility/AccessibilityNodeInfoTest.java index 3d4918b1bd42..2d82d231e279 100644 --- a/core/tests/coretests/src/android/view/accessibility/AccessibilityNodeInfoTest.java +++ b/core/tests/coretests/src/android/view/accessibility/AccessibilityNodeInfoTest.java @@ -46,7 +46,7 @@ public class AccessibilityNodeInfoTest { // The number of fields tested in the corresponding CTS AccessibilityNodeInfoTest: // See fullyPopulateAccessibilityNodeInfo, assertEqualsAccessibilityNodeInfo, // and assertAccessibilityNodeInfoCleared in that class. - private static final int NUM_MARSHALLED_PROPERTIES = 43; + private static final int NUM_MARSHALLED_PROPERTIES = 44; /** * The number of properties that are purposely not marshalled diff --git a/core/tests/coretests/src/com/android/internal/policy/FoldLockSettingsObserverTest.java b/core/tests/coretests/src/com/android/internal/policy/FoldLockSettingsObserverTest.java new file mode 100644 index 000000000000..537dd69d017d --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/policy/FoldLockSettingsObserverTest.java @@ -0,0 +1,110 @@ +/* + * 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.policy; + +import static com.android.internal.policy.FoldLockSettingsObserver.SETTING_VALUE_DEFAULT; +import static com.android.internal.policy.FoldLockSettingsObserver.SETTING_VALUE_SLEEP_ON_FOLD; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import android.content.ContentResolver; +import android.content.Context; +import android.os.Handler; +import android.os.UserHandle; +import android.provider.Settings; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Tests for {@link FoldLockSettingsObserver}. + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public final class FoldLockSettingsObserverTest { + @Mock + private Context mContext; + @Mock + private Handler mHandler; + @Mock + private ContentResolver mContentResolver; + + private FoldLockSettingsObserver mFoldLockSettingsObserver; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + mFoldLockSettingsObserver = + spy(new FoldLockSettingsObserver(mHandler, mContext)); + + doReturn(mContentResolver).when(mContext).getContentResolver(); + doReturn(SETTING_VALUE_DEFAULT).when(mFoldLockSettingsObserver).request(); + + mFoldLockSettingsObserver.register(); + } + + @Test + public void shouldRegister() { + doReturn(mContentResolver).when(mContext).getContentResolver(); + + mFoldLockSettingsObserver.register(); + + verify(mContentResolver).registerContentObserver( + Settings.System.getUriFor(Settings.System.FOLD_LOCK_BEHAVIOR), + false /*notifyForDescendants */, + mFoldLockSettingsObserver, + UserHandle.USER_ALL + ); + } + + @Test + public void shouldUnregister() { + mFoldLockSettingsObserver.unregister(); + + verify(mContentResolver).unregisterContentObserver(mFoldLockSettingsObserver); + } + + @Test + public void shouldCacheNewValue() { + // Reset the mock's behavior and call count to zero. + reset(mFoldLockSettingsObserver); + doReturn(SETTING_VALUE_SLEEP_ON_FOLD).when(mFoldLockSettingsObserver).request(); + + // Setting is DEFAULT at first. + assertEquals(SETTING_VALUE_DEFAULT, mFoldLockSettingsObserver.mFoldLockSetting); + + // Cache new setting. + mFoldLockSettingsObserver.requestAndCacheFoldLockSetting(); + + // Check that setter was called once and change went through properly. + verify(mFoldLockSettingsObserver).setCurrentFoldSetting(anyString()); + assertTrue(mFoldLockSettingsObserver.isSleepOnFold()); + } +} diff --git a/errorprone/java/com/google/errorprone/bugpatterns/android/RequiresPermissionChecker.java b/errorprone/java/com/google/errorprone/bugpatterns/android/RequiresPermissionChecker.java index 7c7cb18afc4d..9887c272e7f8 100644 --- a/errorprone/java/com/google/errorprone/bugpatterns/android/RequiresPermissionChecker.java +++ b/errorprone/java/com/google/errorprone/bugpatterns/android/RequiresPermissionChecker.java @@ -55,9 +55,9 @@ import com.sun.source.util.TreeScanner; import com.sun.tools.javac.code.Symbol; import com.sun.tools.javac.code.Symbol.ClassSymbol; import com.sun.tools.javac.code.Symbol.MethodSymbol; -import com.sun.tools.javac.code.Symbol.VarSymbol; import com.sun.tools.javac.code.Type; import com.sun.tools.javac.code.Type.ClassType; +import com.sun.tools.javac.tree.JCTree.JCNewClass; import java.util.ArrayList; import java.util.Arrays; @@ -67,7 +67,6 @@ import java.util.List; import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Predicate; import java.util.regex.Pattern; import javax.lang.model.element.Name; @@ -125,6 +124,12 @@ public final class RequiresPermissionChecker extends BugChecker instanceMethod() .onDescendantOf("android.content.Context") .withNameMatching(Pattern.compile("^send(Ordered|Sticky)?Broadcast.*$"))); + private static final Matcher<ExpressionTree> SEND_BROADCAST_AS_USER = + methodInvocation( + instanceMethod() + .onDescendantOf("android.content.Context") + .withNameMatching( + Pattern.compile("^send(Ordered|Sticky)?Broadcast.*AsUser.*$"))); private static final Matcher<ExpressionTree> SEND_PENDING_INTENT = methodInvocation( instanceMethod() .onDescendantOf("android.app.PendingIntent") @@ -306,18 +311,6 @@ public final class RequiresPermissionChecker extends BugChecker } } - private static ExpressionTree findArgumentByParameterName(MethodInvocationTree tree, - Predicate<String> paramName) { - final MethodSymbol sym = ASTHelpers.getSymbol(tree); - final List<VarSymbol> params = sym.getParameters(); - for (int i = 0; i < params.size(); i++) { - if (paramName.test(params.get(i).name.toString())) { - return tree.getArguments().get(i); - } - } - return null; - } - private static Name resolveName(ExpressionTree tree) { if (tree instanceof IdentifierTree) { return ((IdentifierTree) tree).getName(); @@ -345,76 +338,85 @@ public final class RequiresPermissionChecker extends BugChecker private static ParsedRequiresPermission parseBroadcastSourceRequiresPermission( MethodInvocationTree methodTree, VisitorState state) { - final ExpressionTree arg = findArgumentByParameterName(methodTree, - (name) -> name.toLowerCase().contains("intent")); - if (arg instanceof IdentifierTree) { - final Name argName = ((IdentifierTree) arg).getName(); - final MethodTree method = state.findEnclosing(MethodTree.class); - final AtomicReference<ParsedRequiresPermission> res = new AtomicReference<>(); - method.accept(new TreeScanner<Void, Void>() { - private ParsedRequiresPermission last; - - @Override - public Void visitMethodInvocation(MethodInvocationTree tree, Void param) { - if (Objects.equal(methodTree, tree)) { - res.set(last); - } else { - final Name name = resolveName(tree.getMethodSelect()); - if (Objects.equal(argName, name) - && INTENT_SET_ACTION.matches(tree, state)) { - last = parseIntentAction(tree); + if (methodTree.getArguments().size() < 1) { + return null; + } + final ExpressionTree arg = methodTree.getArguments().get(0); + if (!(arg instanceof IdentifierTree)) { + return null; + } + final Name argName = ((IdentifierTree) arg).getName(); + final MethodTree method = state.findEnclosing(MethodTree.class); + final AtomicReference<ParsedRequiresPermission> res = new AtomicReference<>(); + method.accept(new TreeScanner<Void, Void>() { + private ParsedRequiresPermission mLast; + + @Override + public Void visitMethodInvocation(MethodInvocationTree tree, Void param) { + if (Objects.equal(methodTree, tree)) { + res.set(mLast); + } else { + final Name name = resolveName(tree.getMethodSelect()); + if (Objects.equal(argName, name) && INTENT_SET_ACTION.matches(tree, state)) { + mLast = parseIntentAction(tree); + } else if (name == null && tree.getMethodSelect() instanceof MemberSelectTree) { + ExpressionTree innerTree = + ((MemberSelectTree) tree.getMethodSelect()).getExpression(); + if (innerTree instanceof JCNewClass) { + mLast = parseIntentAction((NewClassTree) innerTree); } } - return super.visitMethodInvocation(tree, param); } + return super.visitMethodInvocation(tree, param); + } - @Override - public Void visitAssignment(AssignmentTree tree, Void param) { - final Name name = resolveName(tree.getVariable()); - final Tree init = tree.getExpression(); - if (Objects.equal(argName, name) - && init instanceof NewClassTree) { - last = parseIntentAction((NewClassTree) init); - } - return super.visitAssignment(tree, param); + @Override + public Void visitAssignment(AssignmentTree tree, Void param) { + final Name name = resolveName(tree.getVariable()); + final Tree init = tree.getExpression(); + if (Objects.equal(argName, name) && init instanceof NewClassTree) { + mLast = parseIntentAction((NewClassTree) init); } + return super.visitAssignment(tree, param); + } - @Override - public Void visitVariable(VariableTree tree, Void param) { - final Name name = tree.getName(); - final ExpressionTree init = tree.getInitializer(); - if (Objects.equal(argName, name) - && init instanceof NewClassTree) { - last = parseIntentAction((NewClassTree) init); - } - return super.visitVariable(tree, param); + @Override + public Void visitVariable(VariableTree tree, Void param) { + final Name name = tree.getName(); + final ExpressionTree init = tree.getInitializer(); + if (Objects.equal(argName, name) && init instanceof NewClassTree) { + mLast = parseIntentAction((NewClassTree) init); } - }, null); - return res.get(); - } - return null; + return super.visitVariable(tree, param); + } + }, null); + return res.get(); } private static ParsedRequiresPermission parseBroadcastTargetRequiresPermission( MethodInvocationTree tree, VisitorState state) { - final ExpressionTree arg = findArgumentByParameterName(tree, - (name) -> name.toLowerCase().contains("permission")); final ParsedRequiresPermission res = new ParsedRequiresPermission(); - if (arg != null) { - arg.accept(new TreeScanner<Void, Void>() { - @Override - public Void visitIdentifier(IdentifierTree tree, Void param) { - res.addConstValue(tree); - return super.visitIdentifier(tree, param); - } - - @Override - public Void visitMemberSelect(MemberSelectTree tree, Void param) { - res.addConstValue(tree); - return super.visitMemberSelect(tree, param); - } - }, null); + int permission_position = 1; + if (SEND_BROADCAST_AS_USER.matches(tree, state)) { + permission_position = 2; } + if (tree.getArguments().size() < permission_position + 1) { + return res; + } + final ExpressionTree arg = tree.getArguments().get(permission_position); + arg.accept(new TreeScanner<Void, Void>() { + @Override + public Void visitIdentifier(IdentifierTree tree, Void param) { + res.addConstValue(tree); + return super.visitIdentifier(tree, param); + } + + @Override + public Void visitMemberSelect(MemberSelectTree tree, Void param) { + res.addConstValue(tree); + return super.visitMemberSelect(tree, param); + } + }, null); return res; } diff --git a/errorprone/tests/java/com/google/errorprone/bugpatterns/android/RequiresPermissionCheckerTest.java b/errorprone/tests/java/com/google/errorprone/bugpatterns/android/RequiresPermissionCheckerTest.java index e53372d97f3d..05fde7c4fe57 100644 --- a/errorprone/tests/java/com/google/errorprone/bugpatterns/android/RequiresPermissionCheckerTest.java +++ b/errorprone/tests/java/com/google/errorprone/bugpatterns/android/RequiresPermissionCheckerTest.java @@ -412,6 +412,19 @@ public class RequiresPermissionCheckerTest { " context.sendBroadcast(intent);", " }", " }", + " public void exampleWithChainedMethod(Context context) {", + " Intent intent = new Intent(FooManager.ACTION_RED)", + " .putExtra(\"foo\", 42);", + " context.sendBroadcast(intent, FooManager.PERMISSION_RED);", + " context.sendBroadcastWithMultiplePermissions(intent,", + " new String[] { FooManager.PERMISSION_RED });", + " }", + " public void exampleWithAsUser(Context context) {", + " Intent intent = new Intent(FooManager.ACTION_RED);", + " context.sendBroadcastAsUser(intent, 42, FooManager.PERMISSION_RED);", + " context.sendBroadcastAsUserMultiplePermissions(intent, 42,", + " new String[] { FooManager.PERMISSION_RED });", + " }", "}") .doTest(); } diff --git a/errorprone/tests/res/android/content/Context.java b/errorprone/tests/res/android/content/Context.java index efc4fb122435..9d622ffaf120 100644 --- a/errorprone/tests/res/android/content/Context.java +++ b/errorprone/tests/res/android/content/Context.java @@ -36,4 +36,15 @@ public class Context { public void sendBroadcastWithMultiplePermissions(Intent intent, String[] receiverPermissions) { throw new UnsupportedOperationException(); } + + /* Fake user type for test purposes */ + public void sendBroadcastAsUser(Intent intent, int user, String receiverPermission) { + throw new UnsupportedOperationException(); + } + + /* Fake user type for test purposes */ + public void sendBroadcastAsUserMultiplePermissions( + Intent intent, int user, String[] receiverPermissions) { + throw new UnsupportedOperationException(); + } } diff --git a/errorprone/tests/res/android/content/Intent.java b/errorprone/tests/res/android/content/Intent.java index 288396e60577..7ccea784754a 100644 --- a/errorprone/tests/res/android/content/Intent.java +++ b/errorprone/tests/res/android/content/Intent.java @@ -24,4 +24,8 @@ public class Intent { public Intent setAction(String action) { throw new UnsupportedOperationException(); } + + public Intent putExtra(String extra, int value) { + throw new UnsupportedOperationException(); + } } diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp index e6cb3a08119a..5135e9ee14bc 100644 --- a/libs/WindowManager/Shell/Android.bp +++ b/libs/WindowManager/Shell/Android.bp @@ -239,6 +239,9 @@ android_library { "wmshell.protolog.json.gz", "wmshell.protolog.pb", ], + flags_packages: [ + "com_android_wm_shell_flags", + ], kotlincflags: ["-Xjvm-default=all"], manifest: "AndroidManifest.xml", plugins: ["dagger2-compiler"], diff --git a/libs/WindowManager/Shell/AndroidManifest.xml b/libs/WindowManager/Shell/AndroidManifest.xml index 52ae93f5ebf1..bbbc23e8b922 100644 --- a/libs/WindowManager/Shell/AndroidManifest.xml +++ b/libs/WindowManager/Shell/AndroidManifest.xml @@ -34,6 +34,7 @@ <activity android:name=".bubbles.shortcut.CreateBubbleShortcutActivity" + android:featureFlag="com.android.wm.shell.enable_retrievable_bubbles" android:exported="true" android:excludeFromRecents="true" android:theme="@android:style/Theme.NoDisplay" @@ -47,6 +48,7 @@ <activity android:name=".bubbles.shortcut.ShowBubblesActivity" + android:featureFlag="com.android.wm.shell.enable_retrievable_bubbles" android:exported="true" android:excludeFromRecents="true" android:theme="@android:style/Theme.NoDisplay" > diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/PhysicsAnimatorTestUtils.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/PhysicsAnimatorTestUtils.kt index 235b9bf7b9fd..fc3dc1465dff 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/PhysicsAnimatorTestUtils.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/PhysicsAnimatorTestUtils.kt @@ -168,6 +168,16 @@ object PhysicsAnimatorTestUtils { } } + /** Whether any animation is currently running. */ + @JvmStatic + fun isAnyAnimationRunning(): Boolean { + for (target in allAnimatedObjects) { + val animator = PhysicsAnimator.getInstance(target) + if (animator.isRunning()) return true + } + return false + } + /** * Blocks the calling thread until the first animation frame in which predicate returns true. If * the given object isn't animating, returns without blocking. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java index f7a5c271a729..d4d9d003bc0d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java @@ -16,7 +16,7 @@ package com.android.wm.shell.bubbles; -import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT; @@ -225,8 +225,7 @@ public class BubbleExpandedView extends LinearLayout { options.setTaskAlwaysOnTop(true); options.setLaunchedFromBubble(true); options.setPendingIntentBackgroundActivityStartMode( - MODE_BACKGROUND_ACTIVITY_START_ALLOWED); - options.setPendingIntentBackgroundActivityLaunchAllowedByPermission(true); + MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS); Intent fillInIntent = new Intent(); // Apply flags to make behaviour match documentLaunchMode=always. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java index c79d9c4942bf..5e2141aa639e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java @@ -15,7 +15,7 @@ */ package com.android.wm.shell.bubbles; -import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT; @@ -103,8 +103,7 @@ public class BubbleTaskViewHelper { options.setTaskAlwaysOnTop(true); options.setLaunchedFromBubble(true); options.setPendingIntentBackgroundActivityStartMode( - MODE_BACKGROUND_ACTIVITY_START_ALLOWED); - options.setPendingIntentBackgroundActivityLaunchAllowedByPermission(true); + MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS); Intent fillInIntent = new Intent(); // Apply flags to make behaviour match documentLaunchMode=always. 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 e713af6a5311..80f6a637ba1c 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 @@ -211,6 +211,7 @@ public abstract class WMShellModule { @ShellMainThread ShellExecutor mainExecutor, @ShellMainThread Handler mainHandler, @ShellMainThread Choreographer mainChoreographer, + @ShellBackgroundThread ShellExecutor bgExecutor, ShellInit shellInit, IWindowManager windowManager, ShellCommandHandler shellCommandHandler, @@ -229,6 +230,7 @@ public abstract class WMShellModule { mainExecutor, mainHandler, mainChoreographer, + bgExecutor, shellInit, shellCommandHandler, windowManager, @@ -246,6 +248,7 @@ public abstract class WMShellModule { context, mainHandler, mainExecutor, + bgExecutor, mainChoreographer, windowManager, shellInit, @@ -366,13 +369,14 @@ public abstract class WMShellModule { Optional<WindowDecorViewModel> windowDecorViewModel, Optional<DesktopTasksController> desktopTasksController, MultiInstanceHelper multiInstanceHelper, - @ShellMainThread ShellExecutor mainExecutor) { + @ShellMainThread ShellExecutor mainExecutor, + @ShellMainThread Handler mainHandler) { return new SplitScreenController(context, shellInit, shellCommandHandler, shellController, shellTaskOrganizer, syncQueue, rootTaskDisplayAreaOrganizer, displayController, displayImeController, displayInsetsController, dragAndDropController, transitions, transactionPool, iconProvider, recentTasks, launchAdjacentController, windowDecorViewModel, desktopTasksController, null /* stageCoordinator */, - multiInstanceHelper, mainExecutor); + multiInstanceHelper, mainExecutor, mainHandler); } // 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 1bf125938e6f..da212e704b24 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 @@ -66,7 +66,7 @@ fun calculateInitialBounds( idealSize } } else { - maximumSizeMaintainingAspectRatio(taskInfo, idealSize, appAspectRatio) + maximizeSizeGivenAspectRatio(taskInfo, idealSize, appAspectRatio) } } ORIENTATION_PORTRAIT -> { @@ -85,13 +85,13 @@ fun calculateInitialBounds( } else { if (isFixedOrientationLandscape(topActivityInfo.screenOrientation)) { // Apply custom app width and calculate maximum size - maximumSizeMaintainingAspectRatio( + maximizeSizeGivenAspectRatio( taskInfo, Size(customPortraitWidthForLandscapeApp, idealSize.height), appAspectRatio ) } else { - maximumSizeMaintainingAspectRatio(taskInfo, idealSize, appAspectRatio) + maximizeSizeGivenAspectRatio(taskInfo, idealSize, appAspectRatio) } } } @@ -107,7 +107,7 @@ fun calculateInitialBounds( * Calculates the largest size that can fit in a given area while maintaining a specific aspect * ratio. */ -fun maximumSizeMaintainingAspectRatio( +fun maximizeSizeGivenAspectRatio( taskInfo: RunningTaskInfo, targetArea: Size, aspectRatio: Float 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 9a1a8a20ae0e..de901b5dabd8 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 @@ -69,6 +69,7 @@ import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOT import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT import com.android.wm.shell.compatui.isTopActivityExemptFromDesktopWindowing import com.android.wm.shell.desktopmode.DesktopModeTaskRepository.VisibleTasksListener +import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler.DragToDesktopStateListener import com.android.wm.shell.draganddrop.DragAndDropController import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE @@ -671,7 +672,7 @@ class DesktopTasksController( } else { // if non-resizable then calculate max bounds according to aspect ratio val activityAspectRatio = calculateAspectRatio(taskInfo) - val newSize = maximumSizeMaintainingAspectRatio(taskInfo, + val newSize = maximizeSizeGivenAspectRatio(taskInfo, Size(stableBounds.width(), stableBounds.height()), activityAspectRatio) val newBounds = centerInArea( newSize, stableBounds, stableBounds.left, stableBounds.top) @@ -818,9 +819,8 @@ class DesktopTasksController( val intent = Intent(context, DesktopWallpaperActivity::class.java) val options = ActivityOptions.makeBasic().apply { - isPendingIntentBackgroundActivityLaunchAllowedByPermission = true pendingIntentBackgroundActivityStartMode = - ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS } val pendingIntent = PendingIntent.getActivity( @@ -983,6 +983,7 @@ class DesktopTasksController( ProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: skip keyguard is locked") return null } + val wct = WindowContainerTransaction() if (!isDesktopModeShowing(task.displayId)) { ProtoLog.d( WM_SHELL_DESKTOP_MODE, @@ -990,12 +991,17 @@ class DesktopTasksController( " taskId=%d", task.taskId ) - return WindowContainerTransaction().also { wct -> - bringDesktopAppsToFrontBeforeShowingNewTask(task.displayId, wct, task.taskId) - wct.reorder(task.token, true) + // We are outside of desktop mode and already existing desktop task is being launched. + // We should make this task go to fullscreen instead of freeform. Note that this means + // any re-launch of a freeform window outside of desktop will be in fullscreen. + if (desktopModeTaskRepository.isActiveTask(task.taskId)) { + addMoveToFullscreenChanges(wct, task) + return wct } + bringDesktopAppsToFrontBeforeShowingNewTask(task.displayId, wct, task.taskId) + wct.reorder(task.token, true) + return wct } - val wct = WindowContainerTransaction() if (useDesktopOverrideDensity()) { wct.setDensityDpi(task.token, DESKTOP_DENSITY_OVERRIDE) } @@ -1074,7 +1080,6 @@ class DesktopTasksController( wct: WindowContainerTransaction, taskInfo: RunningTaskInfo ) { - val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(taskInfo.displayId)!! val tdaWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode val targetWindowingMode = @@ -1084,9 +1089,6 @@ class DesktopTasksController( } else { WINDOWING_MODE_FREEFORM } - if (Flags.enableWindowingDynamicInitialBounds()) { - wct.setBounds(taskInfo.token, calculateInitialBounds(displayLayout, taskInfo)) - } wct.setWindowingMode(taskInfo.token, targetWindowingMode) wct.reorder(taskInfo.token, true /* onTop */) if (useDesktopOverrideDensity()) { @@ -1333,33 +1335,36 @@ class DesktopTasksController( * * @param taskInfo the task being dragged. * @param y height of drag, to be checked against status bar height. + * @return the [IndicatorType] used for the resulting transition */ fun onDragPositioningEndThroughStatusBar( inputCoordinates: PointF, taskInfo: RunningTaskInfo, - ) { - val indicator = getVisualIndicator() ?: return + ): IndicatorType { + val indicator = getVisualIndicator() ?: return IndicatorType.NO_INDICATOR val indicatorType = indicator.updateIndicatorType(inputCoordinates, taskInfo.windowingMode) when (indicatorType) { - DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR -> { - val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return + IndicatorType.TO_DESKTOP_INDICATOR -> { + val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) + ?: return IndicatorType.NO_INDICATOR if (Flags.enableWindowingDynamicInitialBounds()) { finalizeDragToDesktop(taskInfo, calculateInitialBounds(displayLayout, taskInfo)) } else { finalizeDragToDesktop(taskInfo, getDefaultDesktopTaskBounds(displayLayout)) } } - DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR, - DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR -> { + IndicatorType.NO_INDICATOR, + IndicatorType.TO_FULLSCREEN_INDICATOR -> { cancelDragToDesktop(taskInfo) } - DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR -> { + IndicatorType.TO_SPLIT_LEFT_INDICATOR -> { requestSplit(taskInfo, leftOrTop = true) } - DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR -> { + IndicatorType.TO_SPLIT_RIGHT_INDICATOR -> { requestSplit(taskInfo, leftOrTop = false) } } + return indicatorType } /** Update the exclusion region for a specified task */ @@ -1420,7 +1425,6 @@ class DesktopTasksController( setPendingIntentBackgroundActivityStartMode( ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED ) - isPendingIntentBackgroundActivityLaunchAllowedByPermission = true } val wct = WindowContainerTransaction() wct.sendPendingIntent(launchIntent, null, opts.toBundle()) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java index 95fe8b6f1f4e..7e0362475f21 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java @@ -16,7 +16,7 @@ package com.android.wm.shell.draganddrop; -import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS; import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; @@ -280,8 +280,7 @@ public class DragAndDropPolicy { baseActivityOpts.setDisallowEnterPictureInPictureWhileLaunching(true); // Put BAL flags to avoid activity start aborted. baseActivityOpts.setPendingIntentBackgroundActivityStartMode( - MODE_BACKGROUND_ACTIVITY_START_ALLOWED); - baseActivityOpts.setPendingIntentBackgroundActivityLaunchAllowedByPermission(true); + MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS); final Bundle opts = baseActivityOpts.toBundle(); if (session.appData.hasExtra(EXTRA_ACTIVITY_OPTIONS)) { opts.putAll(session.appData.getBundleExtra(EXTRA_ACTIVITY_OPTIONS)); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java index 9539a456502f..d001b2c09f85 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java @@ -466,7 +466,7 @@ public class RecentTasksController implements TaskStackListenerCallback, @Nullable WindowContainerToken ignoreTaskToken) { List<ActivityManager.RunningTaskInfo> tasks = mActivityTaskManager.getTasks(2, false /* filterOnlyVisibleRecents */); - for (int i = tasks.size() - 1; i >= 0; i--) { + for (int i = 0; i < tasks.size(); i++) { final ActivityManager.RunningTaskInfo task = tasks.get(i); if (task.token.equals(ignoreTaskToken)) { continue; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java index 8df287d12cbc..06c57bd7092d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java @@ -113,6 +113,9 @@ public interface SplitScreen { /** Called when device waking up finished. */ void onFinishedWakingUp(); + /** Called when device starts going to sleep (screen off). */ + void onStartedGoingToSleep(); + /** Called when requested to go to fullscreen from the current active split app. */ void goToFullscreenFromSplit(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java index e659151fee7f..b8575565ef8a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java @@ -50,6 +50,7 @@ import android.content.pm.LauncherApps; import android.content.pm.ShortcutInfo; import android.graphics.Rect; import android.os.Bundle; +import android.os.Handler; import android.os.RemoteException; import android.os.UserHandle; import android.util.ArrayMap; @@ -180,6 +181,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, private final LauncherApps mLauncherApps; private final RootTaskDisplayAreaOrganizer mRootTDAOrganizer; private final ShellExecutor mMainExecutor; + private final Handler mMainHandler; private final SplitScreenImpl mImpl = new SplitScreenImpl(); private final DisplayController mDisplayController; private final DisplayImeController mDisplayImeController; @@ -227,7 +229,8 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, Optional<DesktopTasksController> desktopTasksController, @Nullable StageCoordinator stageCoordinator, MultiInstanceHelper multiInstanceHelper, - ShellExecutor mainExecutor) { + ShellExecutor mainExecutor, + Handler mainHandler) { mShellCommandHandler = shellCommandHandler; mShellController = shellController; mTaskOrganizer = shellTaskOrganizer; @@ -236,6 +239,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, mLauncherApps = context.getSystemService(LauncherApps.class); mRootTDAOrganizer = rootTDAOrganizer; mMainExecutor = mainExecutor; + mMainHandler = mainHandler; mDisplayController = displayController; mDisplayImeController = displayImeController; mDisplayInsetsController = displayInsetsController; @@ -292,7 +296,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, return new StageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue, mTaskOrganizer, mDisplayController, mDisplayImeController, mDisplayInsetsController, mTransitions, mTransactionPool, mIconProvider, - mMainExecutor, mRecentTasksOptional, mLaunchAdjacentController, + mMainExecutor, mMainHandler, mRecentTasksOptional, mLaunchAdjacentController, mWindowDecorViewModel); } @@ -448,13 +452,17 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, @Override public void onKeyguardVisibilityChanged(boolean visible, boolean occluded, boolean animatingDismiss) { - mStageCoordinator.onKeyguardVisibilityChanged(visible); + mStageCoordinator.onKeyguardStateChanged(visible, occluded); } public void onFinishedWakingUp() { mStageCoordinator.onFinishedWakingUp(); } + public void onStartedGoingToSleep() { + mStageCoordinator.onStartedGoingToSleep(); + } + public void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) { mStageCoordinator.exitSplitScreenOnHide(exitSplitScreenOnHide); } @@ -1201,6 +1209,11 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, } @Override + public void onStartedGoingToSleep() { + mMainExecutor.execute(SplitScreenController.this::onStartedGoingToSleep); + } + + @Override public void goToFullscreenFromSplit() { mMainExecutor.execute(SplitScreenController.this::goToFullscreenFromSplit); } 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 9bcd9b0a11c8..a4f32c45c0a9 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 @@ -16,7 +16,7 @@ package com.android.wm.shell.splitscreen; -import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; @@ -92,6 +92,7 @@ import android.graphics.Rect; import android.hardware.devicestate.DeviceStateManager; import android.os.Bundle; import android.os.Debug; +import android.os.Handler; import android.os.IBinder; import android.os.RemoteException; import android.os.ServiceManager; @@ -119,6 +120,7 @@ import android.window.WindowContainerTransaction; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.InstanceId; +import com.android.internal.policy.FoldLockSettingsObserver; import com.android.internal.protolog.ProtoLog; import com.android.internal.util.ArrayUtils; import com.android.launcher3.icons.IconProvider; @@ -191,7 +193,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, private SplitLayout mSplitLayout; private ValueAnimator mDividerFadeInAnimator; private boolean mDividerVisible; - private boolean mKeyguardShowing; + private boolean mKeyguardActive; private boolean mShowDecorImmediately; private final SyncTransactionQueue mSyncQueue; private final ShellTaskOrganizer mTaskOrganizer; @@ -205,6 +207,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, private SplitScreenTransitions mSplitTransitions; private final SplitscreenEventLogger mLogger; private final ShellExecutor mMainExecutor; + private final Handler mMainHandler; // Cache live tile tasks while entering recents, evict them from stages in finish transaction // if user is opening another task(s). private final ArrayList<Integer> mPausingTasks = new ArrayList<>(); @@ -233,7 +236,10 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, private boolean mIsExiting; private boolean mIsRootTranslucent; @VisibleForTesting - int mTopStageAfterFoldDismiss; + @StageType int mLastActiveStage; + private boolean mBreakOnNextWake; + /** Used to get the Settings value for "Continue using apps on fold". */ + private FoldLockSettingsObserver mFoldLockSettingsObserver; private DefaultMixedHandler mMixedHandler; private final Toast mSplitUnsupportedToast; @@ -313,9 +319,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, ShellTaskOrganizer taskOrganizer, DisplayController displayController, DisplayImeController displayImeController, DisplayInsetsController displayInsetsController, Transitions transitions, - TransactionPool transactionPool, - IconProvider iconProvider, ShellExecutor mainExecutor, - Optional<RecentTasksController> recentTasks, + TransactionPool transactionPool, IconProvider iconProvider, ShellExecutor mainExecutor, + Handler mainHandler, Optional<RecentTasksController> recentTasks, LaunchAdjacentController launchAdjacentController, Optional<WindowDecorViewModel> windowDecorViewModel) { mContext = context; @@ -324,6 +329,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mTaskOrganizer = taskOrganizer; mLogger = new SplitscreenEventLogger(); mMainExecutor = mainExecutor; + mMainHandler = mainHandler; mRecentTasks = recentTasks; mLaunchAdjacentController = launchAdjacentController; mWindowDecorViewModel = windowDecorViewModel; @@ -366,6 +372,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // With shell transition, we should update recents tile each callback so set this to true by // default. mShouldUpdateRecents = ENABLE_SHELL_TRANSITIONS; + mFoldLockSettingsObserver = + new FoldLockSettingsObserver(mainHandler, context); + mFoldLockSettingsObserver.register(); } @VisibleForTesting @@ -373,9 +382,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, ShellTaskOrganizer taskOrganizer, MainStage mainStage, SideStage sideStage, DisplayController displayController, DisplayImeController displayImeController, DisplayInsetsController displayInsetsController, SplitLayout splitLayout, - Transitions transitions, TransactionPool transactionPool, - ShellExecutor mainExecutor, - Optional<RecentTasksController> recentTasks, + Transitions transitions, TransactionPool transactionPool, ShellExecutor mainExecutor, + Handler mainHandler, Optional<RecentTasksController> recentTasks, LaunchAdjacentController launchAdjacentController, Optional<WindowDecorViewModel> windowDecorViewModel) { mContext = context; @@ -393,6 +401,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, this::onTransitionAnimationComplete, this); mLogger = new SplitscreenEventLogger(); mMainExecutor = mainExecutor; + mMainHandler = mainHandler; mRecentTasks = recentTasks; mLaunchAdjacentController = launchAdjacentController; mWindowDecorViewModel = windowDecorViewModel; @@ -400,6 +409,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, transitions.addHandler(this); mSplitUnsupportedToast = Toast.makeText(mContext, R.string.dock_non_resizeble_failed_to_dock_text, Toast.LENGTH_SHORT); + mFoldLockSettingsObserver = + new FoldLockSettingsObserver(context.getMainThreadHandler(), context); + mFoldLockSettingsObserver.register(); } public void setMixedHandler(DefaultMixedHandler mixedHandler) { @@ -1504,51 +1516,80 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } } - void onKeyguardVisibilityChanged(boolean showing) { - mKeyguardShowing = showing; + /** + * Runs when keyguard state changes. The booleans here are a bit complicated, so for reference: + * @param active {@code true} if we are in a state where the keyguard *should* be shown + * -- still true when keyguard is "there" but is behind an app, or + * screen is off. + * @param occludingTaskRunning {@code true} when there is a running task that has + * FLAG_SHOW_WHEN_LOCKED -- also true when the task is + * just running on its own and keyguard is not active + * at all. + */ + void onKeyguardStateChanged(boolean active, boolean occludingTaskRunning) { + mKeyguardActive = active; if (!mMainStage.isActive()) { return; } - ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onKeyguardVisibilityChanged: showing=%b", showing); - setDividerVisibility(!mKeyguardShowing, null); + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, + "onKeyguardVisibilityChanged: active=%b occludingTaskRunning=%b", + active, occludingTaskRunning); + setDividerVisibility(!mKeyguardActive, null); + + if (active && occludingTaskRunning) { + dismissSplitKeepingLastActiveStage(EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP); + } } void onFinishedWakingUp() { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onFinishedWakingUp"); - if (!mMainStage.isActive()) { + if (mBreakOnNextWake) { + dismissSplitKeepingLastActiveStage(EXIT_REASON_DEVICE_FOLDED); + } + } + + void onStartedGoingToSleep() { + recordLastActiveStage(); + } + + /** + * Records the user's last focused stage -- main stage or side stage. Used to determine which + * stage of a split pair should be kept, in cases where system focus has moved elsewhere. + */ + void recordLastActiveStage() { + if (!isSplitActive() || !isSplitScreenVisible()) { + mLastActiveStage = STAGE_TYPE_UNDEFINED; + } else if (mMainStage.isFocused()) { + mLastActiveStage = STAGE_TYPE_MAIN; + } else if (mSideStage.isFocused()) { + mLastActiveStage = STAGE_TYPE_SIDE; + } + } + + /** + * Dismisses split, keeping the app that the user focused last in split screen. If the user was + * not in split screen, {@link #mLastActiveStage} should be set to STAGE_TYPE_UNDEFINED, and we + * will do a no-op. + */ + void dismissSplitKeepingLastActiveStage(@ExitReason int reason) { + if (!mMainStage.isActive() || mLastActiveStage == STAGE_TYPE_UNDEFINED) { + // no-op return; } - // Check if there's only one stage visible while keyguard occluded. - final boolean mainStageVisible = mMainStage.mRootTaskInfo.isVisible; - final boolean oneStageVisible = - mMainStage.mRootTaskInfo.isVisible != mSideStage.mRootTaskInfo.isVisible; - if (oneStageVisible && !ENABLE_SHELL_TRANSITIONS) { - // Dismiss split because there's show-when-locked activity showing on top of keyguard. - // Also make sure the task contains show-when-locked activity remains on top after split - // dismissed. - final StageTaskListener toTop = mainStageVisible ? mMainStage : mSideStage; - exitSplitScreen(toTop, EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP); - } - - // Dismiss split if the flag record any side of stages. - if (mTopStageAfterFoldDismiss != STAGE_TYPE_UNDEFINED) { - if (ENABLE_SHELL_TRANSITIONS) { - // Need manually clear here due to this transition might be aborted due to keyguard - // on top and lead to no visible change. - clearSplitPairedInRecents(EXIT_REASON_DEVICE_FOLDED); - final WindowContainerTransaction wct = new WindowContainerTransaction(); - prepareExitSplitScreen(mTopStageAfterFoldDismiss, wct); - mSplitTransitions.startDismissTransition(wct, this, - mTopStageAfterFoldDismiss, EXIT_REASON_DEVICE_FOLDED); - setSplitsVisible(false); - } else { - exitSplitScreen( - mTopStageAfterFoldDismiss == STAGE_TYPE_MAIN ? mMainStage : mSideStage, - EXIT_REASON_DEVICE_FOLDED); - } - mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED; + if (ENABLE_SHELL_TRANSITIONS) { + // Need manually clear here due to this transition might be aborted due to keyguard + // on top and lead to no visible change. + clearSplitPairedInRecents(reason); + final WindowContainerTransaction wct = new WindowContainerTransaction(); + prepareExitSplitScreen(mLastActiveStage, wct); + mSplitTransitions.startDismissTransition(wct, this, mLastActiveStage, reason); + setSplitsVisible(false); + } else { + exitSplitScreen(mLastActiveStage == STAGE_TYPE_MAIN ? mMainStage : mSideStage, reason); } + + mBreakOnNextWake = false; } void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) { @@ -1909,8 +1950,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } // Put BAL flags to avoid activity start aborted. Otherwise, flows like shortcut to split // will be canceled. - options.setPendingIntentBackgroundActivityStartMode(MODE_BACKGROUND_ACTIVITY_START_ALLOWED); - options.setPendingIntentBackgroundActivityLaunchAllowedByPermission(true); + options.setPendingIntentBackgroundActivityStartMode( + MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS); // TODO (b/336477473): Disallow enter PiP when launching a task in split by default; // this might have to be changed as more split-to-pip cujs are defined. @@ -2223,11 +2264,11 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "setDividerVisibility: visible=%b keyguardShowing=%b dividerAnimating=%b caller=%s", - visible, mKeyguardShowing, mIsDividerRemoteAnimating, Debug.getCaller()); + visible, mKeyguardActive, mIsDividerRemoteAnimating, Debug.getCaller()); // Defer showing divider bar after keyguard dismissed, so it won't interfere with keyguard // dismissing animation. - if (visible && mKeyguardShowing) { + if (visible && mKeyguardActive) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, " Defer showing divider bar due to keyguard showing."); return; @@ -2597,21 +2638,24 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, @VisibleForTesting void onFoldedStateChanged(boolean folded) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onFoldedStateChanged: folded=%b", folded); - mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED; - if (!folded) return; - - if (!isSplitActive() || !isSplitScreenVisible()) return; - // To avoid split dismiss when user fold the device and unfold to use later, we only - // record the flag here and try to dismiss on wakeUp callback to ensure split dismiss - // when user interact on phone folded. - if (mMainStage.isFocused()) { - mTopStageAfterFoldDismiss = STAGE_TYPE_MAIN; - } else if (mSideStage.isFocused()) { - mTopStageAfterFoldDismiss = STAGE_TYPE_SIDE; + if (folded) { + recordLastActiveStage(); + // If user folds and has the setting "Continue using apps on fold = NEVER", we assume + // they don't want to continue using split on the outer screen (i.e. we break split if + // they wake the device in its folded state). + mBreakOnNextWake = willSleepOnFold(); + } else { + mBreakOnNextWake = false; } } + /** Returns true if the phone will sleep when it folds. */ + @VisibleForTesting + boolean willSleepOnFold() { + return mFoldLockSettingsObserver != null && mFoldLockSettingsObserver.isSleepOnFold(); + } + private Rect getSideStageBounds() { return mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSplitLayout.getBounds1() : mSplitLayout.getBounds2(); @@ -3739,8 +3783,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSplitTransitions.startDismissTransition(wct, StageCoordinator.this, stageType, EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW); Log.w(TAG, splitFailureMessage("onNoLongerSupportMultiWindow", - "app package " + taskInfo.baseActivity.getPackageName() - + " does not support splitscreen, or is a controlled activity type")); + "app package " + taskInfo.baseIntent.getComponent() + + " does not support splitscreen, or is a controlled activity" + + " type")); if (splitScreenVisible) { handleUnsupportedSplitStart(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitScreenController.java index e330f3ab65ab..b65e97899f3e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitScreenController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitScreenController.java @@ -33,7 +33,6 @@ import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.SystemWindows; import com.android.wm.shell.common.TransactionPool; -import com.android.wm.shell.draganddrop.DragAndDropController; import com.android.wm.shell.recents.RecentTasksController; import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.splitscreen.StageCoordinator; @@ -88,7 +87,8 @@ public class TvSplitScreenController extends SplitScreenController { syncQueue, rootTDAOrganizer, displayController, displayImeController, displayInsetsController, null, transitions, transactionPool, iconProvider, recentTasks, launchAdjacentController, Optional.empty(), - Optional.empty(), null /* stageCoordinator */, multiInstanceHelper, mainExecutor); + Optional.empty(), null /* stageCoordinator */, multiInstanceHelper, mainExecutor, + mainHandler); mTaskOrganizer = shellTaskOrganizer; mSyncQueue = syncQueue; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvStageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvStageCoordinator.java index 79476919221e..81ca48fa6b3d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvStageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvStageCoordinator.java @@ -56,7 +56,7 @@ public class TvStageCoordinator extends StageCoordinator SystemWindows systemWindows) { super(context, displayId, syncQueue, taskOrganizer, displayController, displayImeController, displayInsetsController, transitions, transactionPool, iconProvider, - mainExecutor, recentTasks, launchAdjacentController, Optional.empty()); + mainExecutor, mainHandler, recentTasks, launchAdjacentController, Optional.empty()); mTvSplitMenuController = new TvSplitMenuController(context, this, systemWindows, mainHandler); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java index dd4595a70211..287e779d8e24 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java @@ -48,6 +48,7 @@ public class ShellInit { public ShellInit(ShellExecutor mainExecutor) { mMainExecutor = mainExecutor; + ProtoLog.registerGroups(ShellProtoLogGroup.values()); } /** @@ -76,7 +77,6 @@ public class ShellInit { */ @VisibleForTesting public void init() { - ProtoLog.registerGroups(ShellProtoLogGroup.values()); ProtoLog.v(WM_SHELL_INIT, "Initializing Shell Components: %d", mInitCallbacks.size()); SurfaceControl.setDebugUsageAfterRelease(true); // Init in order of registration diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java index b9cb6d3d5007..5c230c0a505c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java @@ -56,6 +56,7 @@ import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.freeform.FreeformTaskTransitionStarter; +import com.android.wm.shell.shared.annotations.ShellBackgroundThread; import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; @@ -72,6 +73,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { private final IWindowManager mWindowManager; private final Context mContext; private final Handler mMainHandler; + private final @ShellBackgroundThread ShellExecutor mBgExecutor; private final ShellExecutor mMainExecutor; private final Choreographer mMainChoreographer; private final DisplayController mDisplayController; @@ -108,6 +110,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { public CaptionWindowDecorViewModel( Context context, Handler mainHandler, + @ShellBackgroundThread ShellExecutor bgExecutor, ShellExecutor shellExecutor, Choreographer mainChoreographer, IWindowManager windowManager, @@ -120,6 +123,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { mContext = context; mMainExecutor = shellExecutor; mMainHandler = mainHandler; + mBgExecutor = bgExecutor; mWindowManager = windowManager; mMainChoreographer = mainChoreographer; mTaskOrganizer = taskOrganizer; @@ -289,6 +293,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { taskInfo, taskSurface, mMainHandler, + mBgExecutor, mMainChoreographer, mSyncQueue); mWindowDecorByTaskId.put(taskInfo.taskId, windowDecoration); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java index 7e1b973a98f4..cf42a49f7103 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java @@ -21,6 +21,7 @@ import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getLarge import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getResizeEdgeHandleSize; import android.annotation.NonNull; +import android.annotation.SuppressLint; import android.app.ActivityManager; import android.app.ActivityManager.RunningTaskInfo; import android.app.WindowConfiguration; @@ -48,7 +49,9 @@ import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.shared.annotations.ShellBackgroundThread; import com.android.wm.shell.windowdecor.extension.TaskInfoKt; /** @@ -58,6 +61,7 @@ import com.android.wm.shell.windowdecor.extension.TaskInfoKt; */ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearLayout> { private final Handler mHandler; + private final @ShellBackgroundThread ShellExecutor mBgExecutor; private final Choreographer mChoreographer; private final SyncTransactionQueue mSyncQueue; @@ -78,10 +82,12 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL RunningTaskInfo taskInfo, SurfaceControl taskSurface, Handler handler, + @ShellBackgroundThread ShellExecutor bgExecutor, Choreographer choreographer, SyncTransactionQueue syncQueue) { super(context, displayController, taskOrganizer, taskInfo, taskSurface); mHandler = handler; + mBgExecutor = bgExecutor; mChoreographer = choreographer; mSyncQueue = syncQueue; } @@ -218,6 +224,7 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL relayoutParams.mOccludingCaptionElements.add(controlsElement); } + @SuppressLint("MissingPermission") void relayout(RunningTaskInfo taskInfo, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, boolean applyStartTransactionOnDraw, boolean setTaskCropAndPosition) { @@ -235,7 +242,7 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult); // After this line, mTaskInfo is up-to-date and should be used instead of taskInfo - mTaskOrganizer.applyTransaction(wct); + mBgExecutor.execute(() -> mTaskOrganizer.applyTransaction(wct)); if (mResult.mRootView == null) { // This means something blocks the window decor from showing, e.g. the task is hidden. 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 53976251e31d..8312aef110b9 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 @@ -22,6 +22,8 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; +import static android.content.Intent.ACTION_MAIN; +import static android.content.Intent.CATEGORY_APP_BROWSER; import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; import static android.view.InputDevice.SOURCE_TOUCHSCREEN; import static android.view.MotionEvent.ACTION_CANCEL; @@ -36,6 +38,8 @@ import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSIT import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; import static com.android.wm.shell.compatui.AppCompatUtils.isTopActivityExemptFromDesktopWindowing; import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR; +import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR; +import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE; import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED; @@ -43,11 +47,8 @@ import android.annotation.NonNull; import android.app.ActivityManager; import android.app.ActivityManager.RunningTaskInfo; import android.app.ActivityTaskManager; -import android.content.ComponentName; import android.content.Context; import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; import android.graphics.Point; import android.graphics.PointF; import android.graphics.Rect; @@ -98,6 +99,7 @@ import com.android.wm.shell.desktopmode.DesktopTasksController; import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition; import com.android.wm.shell.desktopmode.DesktopWallpaperActivity; import com.android.wm.shell.freeform.FreeformTaskTransitionStarter; +import com.android.wm.shell.shared.annotations.ShellBackgroundThread; import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; import com.android.wm.shell.splitscreen.SplitScreen; import com.android.wm.shell.splitscreen.SplitScreen.StageType; @@ -132,6 +134,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { private final ShellController mShellController; private final Context mContext; private final Handler mMainHandler; + private final @ShellBackgroundThread ShellExecutor mBgExecutor; private final Choreographer mMainChoreographer; private final DisplayController mDisplayController; private final SyncTransactionQueue mSyncQueue; @@ -183,6 +186,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { ShellExecutor shellExecutor, Handler mainHandler, Choreographer mainChoreographer, + @ShellBackgroundThread ShellExecutor bgExecutor, ShellInit shellInit, ShellCommandHandler shellCommandHandler, IWindowManager windowManager, @@ -201,6 +205,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { shellExecutor, mainHandler, mainChoreographer, + bgExecutor, shellInit, shellCommandHandler, windowManager, @@ -225,6 +230,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { ShellExecutor shellExecutor, Handler mainHandler, Choreographer mainChoreographer, + @ShellBackgroundThread ShellExecutor bgExecutor, ShellInit shellInit, ShellCommandHandler shellCommandHandler, IWindowManager windowManager, @@ -245,6 +251,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mMainExecutor = shellExecutor; mMainHandler = mainHandler; mMainChoreographer = mainChoreographer; + mBgExecutor = bgExecutor; mActivityTaskManager = mContext.getSystemService(ActivityTaskManager.class); mTaskOrganizer = taskOrganizer; mShellController = shellController; @@ -321,7 +328,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { if (decoration == null) return; final RunningTaskInfo oldTaskInfo = decoration.mTaskInfo; - if (taskInfo.displayId != oldTaskInfo.displayId) { + if (taskInfo.displayId != oldTaskInfo.displayId + && !Flags.enableAdditionalWindowsAboveStatusBar()) { removeTaskFromEventReceiver(oldTaskInfo.displayId); incrementEventReceiverTasks(taskInfo.displayId); } @@ -385,7 +393,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { decoration.close(); final int displayId = taskInfo.displayId; - if (mEventReceiversByDisplay.contains(displayId)) { + if (mEventReceiversByDisplay.contains(displayId) + && !Flags.enableAdditionalWindowsAboveStatusBar()) { removeTaskFromEventReceiver(displayId); } // Remove the decoration from the cache last because WindowDecoration#close could still @@ -424,19 +433,12 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { } private void openInBrowser(Uri uri) { - final Intent intent = new Intent(Intent.ACTION_VIEW, uri) - .setComponent(getDefaultBrowser()) + final Intent intent = Intent.makeMainSelectorActivity(ACTION_MAIN, CATEGORY_APP_BROWSER) + .setData(uri) .addFlags(FLAG_ACTIVITY_NEW_TASK); mContext.startActivity(intent); } - private ComponentName getDefaultBrowser() { - final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://")); - final ResolveInfo info = mContext.getPackageManager() - .resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY); - return info.getComponentInfo().getComponentName(); - } - private class DesktopModeTouchEventListener extends GestureDetector.SimpleOnGestureListener implements View.OnClickListener, View.OnTouchListener, View.OnLongClickListener, View.OnGenericMotionListener, DragDetector.MotionEventHandler { @@ -517,6 +519,9 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { } } else if (id == R.id.split_screen_button) { decoration.closeHandleMenu(); + // When the app enters split-select, the handle will no longer be visible, meaning + // we shouldn't receive input for it any longer. + decoration.disposeStatusBarInputLayer(); mDesktopTasksController.requestSplit(decoration.mTaskInfo); } else if (id == R.id.open_in_browser_button) { // TODO(b/346441962): let the decoration handle the click gesture and only call back @@ -653,13 +658,38 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId); final RunningTaskInfo taskInfo = decoration.mTaskInfo; if (DesktopModeStatus.canEnterDesktopMode(mContext) - && taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) { - return false; + && !taskInfo.isFreeform()) { + return handleNonFreeformMotionEvent(decoration, v, e); + } else { + return handleFreeformMotionEvent(decoration, taskInfo, v, e); } + } + + private boolean handleNonFreeformMotionEvent(DesktopModeWindowDecoration decoration, + View v, MotionEvent e) { + final int id = v.getId(); + if (id == R.id.caption_handle) { + if (e.getActionMasked() == MotionEvent.ACTION_DOWN) { + // Caption handle is located within the status bar region, meaning the + // DisplayPolicy will attempt to transfer this input to status bar if it's + // a swipe down. Pilfer here to keep the gesture in handle alone. + mInputManager.pilferPointers(v.getViewRootImpl().getInputToken()); + } + handleCaptionThroughStatusBar(e, decoration); + final boolean wasDragging = mIsDragging; + updateDragStatus(e.getActionMasked()); + // Only prevent onClick from receiving this event if it's a drag. + return wasDragging; + } + return false; + } + + private boolean handleFreeformMotionEvent(DesktopModeWindowDecoration decoration, + RunningTaskInfo taskInfo, View v, MotionEvent e) { + final int id = v.getId(); if (mGestureDetector.onTouchEvent(e)) { return true; } - final int id = v.getId(); final boolean touchingButton = (id == R.id.close_window || id == R.id.maximize_window || id == R.id.open_menu_button); switch (e.getActionMasked()) { @@ -668,7 +698,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mDragPositioningCallback.onDragPositioningStart( 0 /* ctrlType */, e.getRawX(0), e.getRawY(0)); - mIsDragging = false; + updateDragStatus(e.getActionMasked()); mHasLongClicked = false; // Do not consume input event if a button is touched, otherwise it would // prevent the button's ripple effect from showing. @@ -688,7 +718,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { decoration.mTaskSurface, e.getRawX(dragPointerIdx), newTaskBounds); - mIsDragging = true; + updateDragStatus(e.getActionMasked()); return true; } case MotionEvent.ACTION_UP: @@ -718,7 +748,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { // onClick call that results. return false; } else { - mIsDragging = false; + updateDragStatus(e.getActionMasked()); return true; } } @@ -726,6 +756,21 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { return true; } + private void updateDragStatus(int eventAction) { + switch (eventAction) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: { + mIsDragging = false; + break; + } + case MotionEvent.ACTION_MOVE: { + mIsDragging = true; + break; + } + } + } + /** * Perform a task size toggle on release of the double-tap, assuming no drag event * was handled during the double-tap. @@ -850,6 +895,10 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { * * @param relevantDecor the window decoration of the focused task's caption. This method only * handles motion events outside this caption's bounds. + * TODO(b/349135068): Outside-touch detection no longer works with the + * enableAdditionalWindowsAboveStatusBar flag enabled. This + * will be fixed once we can add FLAG_WATCH_OUTSIDE_TOUCH to relevant menus, + * at which point, all EventReceivers and external touch logic should be removed. */ private void handleEventOutsideCaption(MotionEvent ev, DesktopModeWindowDecoration relevantDecor) { @@ -902,9 +951,10 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { dragFromStatusBarAllowed = windowingMode == WINDOWING_MODE_FULLSCREEN || windowingMode == WINDOWING_MODE_MULTI_WINDOW; } - - if (dragFromStatusBarAllowed - && relevantDecor.checkTouchEventInFocusedCaptionHandle(ev)) { + final boolean shouldStartTransitionDrag = + relevantDecor.checkTouchEventInFocusedCaptionHandle(ev) + || Flags.enableAdditionalWindowsAboveStatusBar(); + if (dragFromStatusBarAllowed && shouldStartTransitionDrag) { mTransitionDragActive = true; } break; @@ -918,8 +968,15 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { // Though this isn't a hover event, we need to update handle's hover state // as it likely will change. relevantDecor.updateHoverAndPressStatus(ev); - mDesktopTasksController.onDragPositioningEndThroughStatusBar( + DesktopModeVisualIndicator.IndicatorType resultType = + mDesktopTasksController.onDragPositioningEndThroughStatusBar( new PointF(ev.getRawX(), ev.getRawY()), relevantDecor.mTaskInfo); + // If we are entering split select, handle will no longer be visible and + // should not be receiving any input. + if (resultType == TO_SPLIT_LEFT_INDICATOR + || resultType != TO_SPLIT_RIGHT_INDICATOR) { + relevantDecor.disposeStatusBarInputLayer(); + } mMoveToDesktopAnimator = null; return; } else { @@ -931,7 +988,6 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { relevantDecor.checkTouchEvent(ev); break; } - case ACTION_MOVE: { if (relevantDecor == null) { return; @@ -1091,10 +1147,12 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mDesktopModeWindowDecorFactory.create( mContext, mDisplayController, + mSplitScreenController, mTaskOrganizer, taskInfo, taskSurface, mMainHandler, + mBgExecutor, mMainChoreographer, mSyncQueue, mRootTaskDisplayAreaOrganizer); @@ -1132,7 +1190,9 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { windowDecoration.setDragDetector(touchEventListener.mDragDetector); windowDecoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */, false /* shouldSetTaskPositionAndCrop */); - incrementEventReceiverTasks(taskInfo.displayId); + if (!Flags.enableAdditionalWindowsAboveStatusBar()) { + incrementEventReceiverTasks(taskInfo.displayId); + } } private RunningTaskInfo getOtherSplitTask(int taskId) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java index 5d662b20ebb9..529def7ca3d7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java @@ -24,11 +24,13 @@ import static android.view.MotionEvent.ACTION_DOWN; import static android.view.MotionEvent.ACTION_UP; import static com.android.launcher3.icons.BaseIconFactory.MODE_DEFAULT; +import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getFineResizeCornerSize; import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getLargeResizeCornerSize; import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getResizeEdgeHandleSize; import android.annotation.NonNull; +import android.annotation.SuppressLint; import android.app.ActivityManager; import android.app.WindowConfiguration.WindowingMode; import android.content.ComponentName; @@ -68,7 +70,9 @@ import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.shared.annotations.ShellBackgroundThread; import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.windowdecor.common.OnTaskActionClickListener; @@ -95,8 +99,10 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin static final long CLOSE_MAXIMIZE_MENU_DELAY_MS = 150L; private final Handler mHandler; + private final @ShellBackgroundThread ShellExecutor mBgExecutor; private final Choreographer mChoreographer; private final SyncTransactionQueue mSyncQueue; + private final SplitScreenController mSplitScreenController; private WindowDecorationViewHolder mWindowDecorViewHolder; private View.OnClickListener mOnCaptionButtonClickListener; @@ -147,27 +153,32 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin DesktopModeWindowDecoration( Context context, DisplayController displayController, + SplitScreenController splitScreenController, ShellTaskOrganizer taskOrganizer, ActivityManager.RunningTaskInfo taskInfo, SurfaceControl taskSurface, Handler handler, + @ShellBackgroundThread ShellExecutor bgExecutor, Choreographer choreographer, SyncTransactionQueue syncQueue, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) { - this (context, displayController, taskOrganizer, taskInfo, taskSurface, - handler, choreographer, syncQueue, rootTaskDisplayAreaOrganizer, - SurfaceControl.Builder::new, SurfaceControl.Transaction::new, - WindowContainerTransaction::new, SurfaceControl::new, - new SurfaceControlViewHostFactory() {}, DefaultMaximizeMenuFactory.INSTANCE); + this (context, displayController, splitScreenController, taskOrganizer, taskInfo, + taskSurface, handler, bgExecutor, choreographer, syncQueue, + rootTaskDisplayAreaOrganizer, SurfaceControl.Builder::new, + SurfaceControl.Transaction::new, WindowContainerTransaction::new, + SurfaceControl::new, new SurfaceControlViewHostFactory() {}, + DefaultMaximizeMenuFactory.INSTANCE); } DesktopModeWindowDecoration( Context context, DisplayController displayController, + SplitScreenController splitScreenController, ShellTaskOrganizer taskOrganizer, ActivityManager.RunningTaskInfo taskInfo, SurfaceControl taskSurface, Handler handler, + @ShellBackgroundThread ShellExecutor bgExecutor, Choreographer choreographer, SyncTransactionQueue syncQueue, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, @@ -181,7 +192,9 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin surfaceControlBuilderSupplier, surfaceControlTransactionSupplier, windowContainerTransactionSupplier, surfaceControlSupplier, surfaceControlViewHostFactory); + mSplitScreenController = splitScreenController; mHandler = handler; + mBgExecutor = bgExecutor; mChoreographer = choreographer; mSyncQueue = syncQueue; mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer; @@ -327,6 +340,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mHandler.post(mCurrentViewHostRunnable); } + @SuppressLint("MissingPermission") private void updateRelayoutParamsAndSurfaces(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop) { @@ -337,7 +351,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin } if (isHandleMenuActive()) { - mHandleMenu.relayout(startT); + mHandleMenu.relayout(startT, mResult.mCaptionX); } updateRelayoutParams(mRelayoutParams, mContext, taskInfo, applyStartTransactionOnDraw, @@ -353,33 +367,47 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin // After this line, mTaskInfo is up-to-date and should be used instead of taskInfo Trace.beginSection("DesktopModeWindowDecoration#relayout-applyWCT"); - mTaskOrganizer.applyTransaction(wct); + mBgExecutor.execute(() -> mTaskOrganizer.applyTransaction(wct)); Trace.endSection(); if (mResult.mRootView == null) { // This means something blocks the window decor from showing, e.g. the task is hidden. // Nothing is set up in this case including the decoration surface. + disposeStatusBarInputLayer(); Trace.endSection(); // DesktopModeWindowDecoration#updateRelayoutParamsAndSurfaces return; } if (oldRootView != mResult.mRootView) { + disposeStatusBarInputLayer(); mWindowDecorViewHolder = createViewHolder(); } Trace.beginSection("DesktopModeWindowDecoration#relayout-binding"); - mWindowDecorViewHolder.bindData(mTaskInfo); + + final Point position = new Point(); + if (isAppHandle(mWindowDecorViewHolder)) { + position.set(determineHandlePosition()); + } + mWindowDecorViewHolder.bindData(mTaskInfo, + position, + mResult.mCaptionWidth, + mResult.mCaptionHeight, + isCaptionVisible()); Trace.endSection(); if (!mTaskInfo.isFocused) { closeHandleMenu(); closeMaximizeMenu(); } - updateDragResizeListener(oldDecorationSurface); updateMaximizeMenu(startT); Trace.endSection(); // DesktopModeWindowDecoration#updateRelayoutParamsAndSurfaces } + private boolean isCaptionVisible() { + return mTaskInfo.isVisible && mIsCaptionVisible; + } + private void setCapturedLink(Uri capturedLink, long timeStamp) { if (capturedLink == null || (mCapturedLink != null && mCapturedLink.mTimeStamp == timeStamp)) { @@ -461,12 +489,42 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin } } + private Point determineHandlePosition() { + final Point position = new Point(mResult.mCaptionX, 0); + if (mSplitScreenController.getSplitPosition(mTaskInfo.taskId) + == SPLIT_POSITION_BOTTOM_OR_RIGHT + && mDisplayController.getDisplayLayout(mTaskInfo.displayId).isLandscape() + ) { + // If this is the right split task, add left stage's width. + final Rect leftStageBounds = new Rect(); + mSplitScreenController.getStageBounds(leftStageBounds, new Rect()); + position.x += leftStageBounds.width(); + } + return position; + } + + /** + * Dispose of the view used to forward inputs in status bar region. Intended to be + * used any time handle is no longer visible. + */ + void disposeStatusBarInputLayer() { + if (!isAppHandle(mWindowDecorViewHolder) + || !Flags.enableAdditionalWindowsAboveStatusBar()) { + return; + } + ((AppHandleViewHolder) mWindowDecorViewHolder).disposeStatusBarInputLayer(); + } + private WindowDecorationViewHolder createViewHolder() { if (mRelayoutParams.mLayoutResId == R.layout.desktop_mode_app_handle) { return new AppHandleViewHolder( mResult.mRootView, mOnCaptionTouchListener, - mOnCaptionButtonClickListener + mOnCaptionButtonClickListener, + (v, event) -> { + updateHoverAndPressStatus(event); + return true; + } ); } else if (mRelayoutParams.mLayoutResId == R.layout.desktop_mode_app_header) { @@ -489,6 +547,10 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin throw new IllegalArgumentException("Unexpected layout resource id"); } + private boolean isAppHandle(WindowDecorationViewHolder viewHolder) { + return viewHolder instanceof AppHandleViewHolder; + } + @VisibleForTesting static void updateRelayoutParams( RelayoutParams relayoutParams, @@ -863,7 +925,9 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin splitScreenController, DesktopModeStatus.canEnterDesktopMode(mContext), browserLinkAvailable(), - mResult.mCaptionHeight + mResult.mCaptionWidth, + mResult.mCaptionHeight, + mResult.mCaptionX ); mWindowDecorViewHolder.onHandleMenuOpened(); mHandleMenu.show(); @@ -955,10 +1019,17 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin * @return {@code true} if event is inside caption handle view, {@code false} if not */ boolean checkTouchEventInFocusedCaptionHandle(MotionEvent ev) { - if (isHandleMenuActive() || !(mWindowDecorViewHolder - instanceof AppHandleViewHolder)) { + if (isHandleMenuActive() || !isAppHandle(mWindowDecorViewHolder) + || Flags.enableAdditionalWindowsAboveStatusBar()) { return false; } + // The status bar input layer can only receive input in handle coordinates to begin with, + // so checking coordinates is unnecessary as input is always within handle bounds. + if (isAppHandle(mWindowDecorViewHolder) + && Flags.enableAdditionalWindowsAboveStatusBar() + && isCaptionVisible()) { + return true; + } return checkTouchEventInCaption(ev); } @@ -992,7 +1063,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin * @param ev the MotionEvent to compare */ void checkTouchEvent(MotionEvent ev) { - if (mResult.mRootView == null) return; + if (mResult.mRootView == null || Flags.enableAdditionalWindowsAboveStatusBar()) return; final View caption = mResult.mRootView.findViewById(R.id.desktop_mode_caption); final View handle = caption.findViewById(R.id.caption_handle); final boolean inHandle = !isHandleMenuActive() @@ -1014,7 +1085,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin * @param ev the MotionEvent to compare against. */ void updateHoverAndPressStatus(MotionEvent ev) { - if (mResult.mRootView == null) return; + if (mResult.mRootView == null || Flags.enableAdditionalWindowsAboveStatusBar()) return; final View handle = mResult.mRootView.findViewById(R.id.caption_handle); final boolean inHandle = !isHandleMenuActive() && checkTouchEventInFocusedCaptionHandle(ev); @@ -1024,15 +1095,11 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin // We want handle to remain pressed if the pointer moves outside of it during a drag. handle.setPressed((inHandle && action == ACTION_DOWN) || (handle.isPressed() && action != ACTION_UP && action != ACTION_CANCEL)); - if (isHandleMenuActive() && !isHandleMenuAboveStatusBar()) { + if (isHandleMenuActive()) { mHandleMenu.checkMotionEvent(ev); } } - private boolean isHandleMenuAboveStatusBar() { - return Flags.enableAdditionalWindowsAboveStatusBar() && !mTaskInfo.isFreeform(); - } - private boolean pointInView(View v, float x, float y) { return v != null && v.getLeft() <= x && v.getRight() >= x && v.getTop() <= y && v.getBottom() >= y; @@ -1044,6 +1111,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin closeHandleMenu(); mExclusionRegionListener.onExclusionRegionDismissed(mTaskInfo.taskId); disposeResizeVeil(); + disposeStatusBarInputLayer(); clearCurrentViewHostRunnable(); super.close(); } @@ -1143,20 +1211,24 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin DesktopModeWindowDecoration create( Context context, DisplayController displayController, + SplitScreenController splitScreenController, ShellTaskOrganizer taskOrganizer, ActivityManager.RunningTaskInfo taskInfo, SurfaceControl taskSurface, Handler handler, + @ShellBackgroundThread ShellExecutor bgExecutor, Choreographer choreographer, SyncTransactionQueue syncQueue, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) { return new DesktopModeWindowDecoration( context, displayController, + splitScreenController, taskOrganizer, taskInfo, taskSurface, handler, + bgExecutor, choreographer, syncQueue, rootTaskDisplayAreaOrganizer); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt index bce233fb0b52..e174e83d11c9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt @@ -15,19 +15,15 @@ */ package com.android.wm.shell.windowdecor +import android.annotation.ColorInt import android.annotation.DimenRes import android.app.ActivityManager -import android.app.WindowConfiguration -import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM -import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN -import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW -import android.app.WindowConfiguration.WINDOWING_MODE_PINNED import android.content.Context import android.content.res.ColorStateList -import android.content.res.Configuration import android.content.res.Resources import android.graphics.Bitmap -import android.graphics.Color +import android.graphics.BlendMode +import android.graphics.BlendModeColorFilter import android.graphics.Point import android.graphics.PointF import android.graphics.Rect @@ -40,6 +36,8 @@ import android.widget.ImageView import android.widget.TextView import android.window.SurfaceSyncGroup import androidx.annotation.VisibleForTesting +import androidx.compose.ui.graphics.toArgb +import androidx.core.view.isGone import com.android.window.flags.Flags import com.android.wm.shell.R import com.android.wm.shell.common.DisplayController @@ -47,7 +45,10 @@ import com.android.wm.shell.common.split.SplitScreenConstants import com.android.wm.shell.splitscreen.SplitScreenController import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewContainer +import com.android.wm.shell.windowdecor.common.DecorThemeUtil import com.android.wm.shell.windowdecor.extension.isFullscreen +import com.android.wm.shell.windowdecor.extension.isMultiWindow +import com.android.wm.shell.windowdecor.extension.isPinned /** * Handle menu opened when the appropriate button is clicked on. @@ -68,10 +69,13 @@ class HandleMenu( private val splitScreenController: SplitScreenController, private val shouldShowWindowingPill: Boolean, private val shouldShowBrowserPill: Boolean, - private val captionHeight: Int + private val captionWidth: Int, + private val captionHeight: Int, + captionX: Int ) { private val context: Context = parentDecor.mDecorWindowContext private val taskInfo: ActivityManager.RunningTaskInfo = parentDecor.mTaskInfo + private val decorThemeUtil = DecorThemeUtil(context) private val isViewAboveStatusBar: Boolean get() = (Flags.enableAdditionalWindowsAboveStatusBar() && !taskInfo.isFreeform) @@ -102,33 +106,8 @@ class HandleMenu( // those as well. private val globalMenuPosition: Point = Point() - /** - * An a array of windowing icon color based on current UI theme. First element of the - * array is for inactive icons and the second is for active icons. - */ - private val windowingIconColor: Array<ColorStateList> - get() { - val mode = (context.resources.configuration.uiMode - and Configuration.UI_MODE_NIGHT_MASK) - val isNightMode = (mode == Configuration.UI_MODE_NIGHT_YES) - val typedArray = context.obtainStyledAttributes( - intArrayOf( - com.android.internal.R.attr.materialColorOnSurface, - com.android.internal.R.attr.materialColorPrimary - ) - ) - val inActiveColor = - typedArray.getColor(0, if (isNightMode) Color.WHITE else Color.BLACK) - val activeColor = typedArray.getColor(1, if (isNightMode) Color.WHITE else Color.BLACK) - typedArray.recycle() - return arrayOf( - ColorStateList.valueOf(inActiveColor), - ColorStateList.valueOf(activeColor) - ) - } - init { - updateHandleMenuPillPositions() + updateHandleMenuPillPositions(captionX) } fun show() { @@ -175,9 +154,8 @@ class HandleMenu( * Animates the appearance of the handle menu and its three pills. */ private fun animateHandleMenu() { - when (taskInfo.windowingMode) { - WindowConfiguration.WINDOWING_MODE_FULLSCREEN, - WINDOWING_MODE_MULTI_WINDOW -> { + when { + taskInfo.isFullscreen || taskInfo.isMultiWindow -> { handleMenuAnimator?.animateCaptionHandleExpandToOpen() } else -> { @@ -193,95 +171,104 @@ class HandleMenu( private fun setupHandleMenu() { val handleMenu = handleMenuViewContainer?.view ?: return handleMenu.setOnTouchListener(onTouchListener) - setupAppInfoPill(handleMenu) + + val style = calculateMenuStyle() + setupAppInfoPill(handleMenu, style) if (shouldShowWindowingPill) { - setupWindowingPill(handleMenu) + setupWindowingPill(handleMenu, style) } - setupMoreActionsPill(handleMenu) - setupOpenInBrowserPill(handleMenu) + setupMoreActionsPill(handleMenu, style) + setupOpenInBrowserPill(handleMenu, style) } /** * Set up interactive elements of handle menu's app info pill. */ - private fun setupAppInfoPill(handleMenu: View) { - val collapseBtn = handleMenu.findViewById<HandleMenuImageButton>(R.id.collapse_menu_button) - val appIcon = handleMenu.findViewById<ImageView>(R.id.application_icon) - val appName = handleMenu.findViewById<TextView>(R.id.application_name) - collapseBtn.setOnClickListener(onClickListener) - collapseBtn.taskInfo = taskInfo - appIcon.setImageBitmap(appIconBitmap) - appName.text = this.appName + private fun setupAppInfoPill(handleMenu: View, style: MenuStyle) { + val pill = handleMenu.requireViewById<View>(R.id.app_info_pill).apply { + background.colorFilter = BlendModeColorFilter(style.backgroundColor, BlendMode.MULTIPLY) + } + + pill.requireViewById<HandleMenuImageButton>(R.id.collapse_menu_button) + .let { collapseBtn -> + collapseBtn.imageTintList = ColorStateList.valueOf(style.textColor) + collapseBtn.setOnClickListener(onClickListener) + collapseBtn.taskInfo = taskInfo + } + pill.requireViewById<ImageView>(R.id.application_icon).let { appIcon -> + appIcon.setImageBitmap(appIconBitmap) + } + pill.requireViewById<TextView>(R.id.application_name).let { appNameView -> + appNameView.text = appName + appNameView.setTextColor(style.textColor) + } } /** * Set up interactive elements and color of handle menu's windowing pill. */ - private fun setupWindowingPill(handleMenu: View) { - val fullscreenBtn = handleMenu.findViewById<ImageButton>(R.id.fullscreen_button) - val splitscreenBtn = handleMenu.findViewById<ImageButton>(R.id.split_screen_button) - val floatingBtn = handleMenu.findViewById<ImageButton>(R.id.floating_button) + private fun setupWindowingPill(handleMenu: View, style: MenuStyle) { + val pill = handleMenu.requireViewById<View>(R.id.windowing_pill).apply { + background.colorFilter = BlendModeColorFilter(style.backgroundColor, BlendMode.MULTIPLY) + } + + val fullscreenBtn = pill.requireViewById<ImageButton>(R.id.fullscreen_button) + val splitscreenBtn = pill.requireViewById<ImageButton>(R.id.split_screen_button) + val floatingBtn = pill.requireViewById<ImageButton>(R.id.floating_button) // TODO: Remove once implemented. floatingBtn.visibility = View.GONE + val desktopBtn = handleMenu.requireViewById<ImageButton>(R.id.desktop_button) - val desktopBtn = handleMenu.findViewById<ImageButton>(R.id.desktop_button) fullscreenBtn.setOnClickListener(onClickListener) splitscreenBtn.setOnClickListener(onClickListener) floatingBtn.setOnClickListener(onClickListener) desktopBtn.setOnClickListener(onClickListener) - // The button corresponding to the windowing mode that the task is currently in uses a - // different color than the others. - val iconColors = windowingIconColor - val inActiveColorStateList = iconColors[0] - val activeColorStateList = iconColors[1] - fullscreenBtn.imageTintList = if (taskInfo.isFullscreen) { - activeColorStateList - } else { - inActiveColorStateList - } - splitscreenBtn.imageTintList = if (taskInfo.windowingMode == WINDOWING_MODE_MULTI_WINDOW) { - activeColorStateList - } else { - inActiveColorStateList - } - floatingBtn.imageTintList = if (taskInfo.windowingMode == WINDOWING_MODE_PINNED) { - activeColorStateList - } else { - inActiveColorStateList - } - desktopBtn.imageTintList = if (taskInfo.isFreeform) { - activeColorStateList - } else { - inActiveColorStateList - } + + fullscreenBtn.isSelected = taskInfo.isFullscreen + fullscreenBtn.imageTintList = style.windowingButtonColor + splitscreenBtn.isSelected = taskInfo.isMultiWindow + splitscreenBtn.imageTintList = style.windowingButtonColor + floatingBtn.isSelected = taskInfo.isPinned + floatingBtn.imageTintList = style.windowingButtonColor + desktopBtn.isSelected = taskInfo.isFreeform + desktopBtn.imageTintList = style.windowingButtonColor } /** * Set up interactive elements & height of handle menu's more actions pill */ - private fun setupMoreActionsPill(handleMenu: View) { - if (!SHOULD_SHOW_MORE_ACTIONS_PILL) { - handleMenu.findViewById<View>(R.id.more_actions_pill).visibility = View.GONE + private fun setupMoreActionsPill(handleMenu: View, style: MenuStyle) { + val pill = handleMenu.requireViewById<View>(R.id.more_actions_pill).apply { + isGone = !SHOULD_SHOW_MORE_ACTIONS_PILL + background.colorFilter = BlendModeColorFilter(style.backgroundColor, BlendMode.MULTIPLY) + } + pill.requireViewById<Button>(R.id.screenshot_button).let { screenshotBtn -> + screenshotBtn.setTextColor(style.textColor) + screenshotBtn.compoundDrawableTintList = ColorStateList.valueOf(style.textColor) } } - private fun setupOpenInBrowserPill(handleMenu: View) { - if (!shouldShowBrowserPill) { - handleMenu.findViewById<View>(R.id.open_in_browser_pill).visibility = View.GONE - return + private fun setupOpenInBrowserPill(handleMenu: View, style: MenuStyle) { + val pill = handleMenu.requireViewById<View>(R.id.open_in_browser_pill).apply { + isGone = !shouldShowBrowserPill + background.colorFilter = BlendModeColorFilter(style.backgroundColor, BlendMode.MULTIPLY) + } + + pill.requireViewById<Button>(R.id.open_in_browser_button).let { browserButton -> + browserButton.setOnClickListener(onClickListener) + browserButton.setTextColor(style.textColor) + browserButton.compoundDrawableTintList = ColorStateList.valueOf(style.textColor) } - val browserButton = handleMenu.findViewById<Button>(R.id.open_in_browser_button) - browserButton.setOnClickListener(onClickListener) } /** * Updates handle menu's position variables to reflect its next position. */ - private fun updateHandleMenuPillPositions() { + private fun updateHandleMenuPillPositions(captionX: Int) { val menuX: Int val menuY: Int val taskBounds = taskInfo.getConfiguration().windowConfiguration.bounds - updateGlobalMenuPosition(taskBounds) + updateGlobalMenuPosition(taskBounds, captionX) if (layoutResId == R.layout.desktop_mode_app_header) { // Align the handle menu to the left side of the caption. menuX = marginMenuStart @@ -302,21 +289,22 @@ class HandleMenu( handleMenuPosition.set(menuX.toFloat(), menuY.toFloat()) } - private fun updateGlobalMenuPosition(taskBounds: Rect) { - when (taskInfo.windowingMode) { - WINDOWING_MODE_FREEFORM -> { + private fun updateGlobalMenuPosition(taskBounds: Rect, captionX: Int) { + val nonFreeformX = captionX + (captionWidth / 2) - (menuWidth / 2) + when { + taskInfo.isFreeform -> { globalMenuPosition.set( /* x = */ taskBounds.left + marginMenuStart, /* y = */ taskBounds.top + marginMenuTop ) } - WINDOWING_MODE_FULLSCREEN -> { + taskInfo.isFullscreen -> { globalMenuPosition.set( - /* x = */ taskBounds.width() / 2 - (menuWidth / 2), + /* x = */ nonFreeformX, /* y = */ marginMenuTop ) } - WINDOWING_MODE_MULTI_WINDOW -> { + taskInfo.isMultiWindow -> { val splitPosition = splitScreenController.getSplitPosition(taskInfo.taskId) val leftOrTopStageBounds = Rect() val rightOrBottomStageBounds = Rect() @@ -326,16 +314,13 @@ class HandleMenu( when (splitPosition) { SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT -> { globalMenuPosition.set( - /* x = */ leftOrTopStageBounds.width() - + (rightOrBottomStageBounds.width() / 2) - - (menuWidth / 2), + /* x = */ leftOrTopStageBounds.width() + nonFreeformX, /* y = */ marginMenuTop ) } SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT -> { globalMenuPosition.set( - /* x = */ (leftOrTopStageBounds.width() / 2) - - (menuWidth / 2), + /* x = */ nonFreeformX, /* y = */ marginMenuTop ) } @@ -347,9 +332,12 @@ class HandleMenu( /** * Update pill layout, in case task changes have caused positioning to change. */ - fun relayout(t: SurfaceControl.Transaction) { + fun relayout( + t: SurfaceControl.Transaction, + captionX: Int + ) { handleMenuViewContainer?.let { container -> - updateHandleMenuPillPositions() + updateHandleMenuPillPositions(captionX) container.setPosition(t, handleMenuPosition.x, handleMenuPosition.y) } } @@ -469,14 +457,41 @@ class HandleMenu( handleMenuViewContainer?.releaseView() handleMenuViewContainer = null } - if (taskInfo.windowingMode == WINDOWING_MODE_FULLSCREEN || - taskInfo.windowingMode == WINDOWING_MODE_MULTI_WINDOW) { + if (taskInfo.isFullscreen || taskInfo.isMultiWindow) { handleMenuAnimator?.animateCollapseIntoHandleClose(after) } else { handleMenuAnimator?.animateClose(after) } } + private fun calculateMenuStyle(): MenuStyle { + val colorScheme = decorThemeUtil.getColorScheme(taskInfo) + return MenuStyle( + backgroundColor = colorScheme.surfaceBright.toArgb(), + textColor = colorScheme.onSurface.toArgb(), + windowingButtonColor = ColorStateList( + arrayOf( + intArrayOf(android.R.attr.state_pressed), + intArrayOf(android.R.attr.state_focused), + intArrayOf(android.R.attr.state_selected), + intArrayOf(), + ), + intArrayOf( + colorScheme.onSurface.toArgb(), + colorScheme.onSurface.toArgb(), + colorScheme.primary.toArgb(), + colorScheme.onSurface.toArgb(), + ) + ), + ) + } + + private data class MenuStyle( + @ColorInt val backgroundColor: Int, + @ColorInt val textColor: Int, + val windowingButtonColor: ColorStateList, + ) + companion object { private const val TAG = "HandleMenu" private const val SHOULD_SHOW_MORE_ACTIONS_PILL = false 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 d212f2131ed4..a691f59a2155 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 @@ -139,7 +139,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> private SurfaceControlViewHost mViewHost; private Configuration mWindowDecorConfig; TaskDragResizer mTaskDragResizer; - private boolean mIsCaptionVisible; + boolean mIsCaptionVisible; /** The most recent set of insets applied to this window decoration. */ private WindowDecorationInsets mWindowDecorationInsets; @@ -508,6 +508,8 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> mTaskDragResizer = taskDragResizer; } + // TODO(b/346441962): Move these three methods closer to implementing or View-level classes to + // keep implementation details more encapsulated. private void setCaptionVisibility(View rootView, boolean visible) { if (rootView == null) { return; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt index 4897f76a20cf..6a354f10049b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt @@ -30,17 +30,22 @@ import android.view.WindowManager */ class AdditionalSystemViewContainer( private val context: Context, - layoutId: Int, taskId: Int, x: Int, y: Int, width: Int, - height: Int + height: Int, + layoutId: Int? = null ) : AdditionalViewContainer() { override val view: View + val windowManager: WindowManager? = context.getSystemService(WindowManager::class.java) init { - view = LayoutInflater.from(context).inflate(layoutId, null) + if (layoutId != null) { + view = LayoutInflater.from(context).inflate(layoutId, null) + } else { + view = View(context) + } val lp = WindowManager.LayoutParams( width, height, x, y, WindowManager.LayoutParams.TYPE_STATUS_BAR_ADDITIONAL, @@ -51,12 +56,11 @@ class AdditionalSystemViewContainer( gravity = Gravity.LEFT or Gravity.TOP setTrustedOverlay() } - val wm: WindowManager? = context.getSystemService(WindowManager::class.java) - wm?.addView(view, lp) + windowManager?.addView(view, lp) } override fun releaseView() { - context.getSystemService(WindowManager::class.java)?.removeViewImmediate(view) + windowManager?.removeViewImmediate(view) } override fun setPosition(t: SurfaceControl.Transaction, x: Float, y: Float) { @@ -64,6 +68,6 @@ class AdditionalSystemViewContainer( this.x = x.toInt() this.y = y.toInt() } - view.layoutParams = lp + windowManager?.updateViewLayout(view, lp) } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/extension/TaskInfo.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/extension/TaskInfo.kt index 7ade9876d28a..6f8e00143848 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/extension/TaskInfo.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/extension/TaskInfo.kt @@ -18,6 +18,8 @@ package com.android.wm.shell.windowdecor.extension import android.app.TaskInfo import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN +import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW +import android.app.WindowConfiguration.WINDOWING_MODE_PINNED import android.view.WindowInsetsController.APPEARANCE_LIGHT_CAPTION_BARS import android.view.WindowInsetsController.APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND @@ -33,5 +35,14 @@ val TaskInfo.isLightCaptionBarAppearance: Boolean return (appearance and APPEARANCE_LIGHT_CAPTION_BARS) != 0 } +/** Whether the task is in fullscreen windowing mode. */ val TaskInfo.isFullscreen: Boolean get() = windowingMode == WINDOWING_MODE_FULLSCREEN + +/** Whether the task is in pinned windowing mode. */ +val TaskInfo.isPinned: Boolean + get() = windowingMode == WINDOWING_MODE_PINNED + +/** Whether the task is in multi-window windowing mode. */ +val TaskInfo.isMultiWindow: Boolean + get() = windowingMode == WINDOWING_MODE_MULTI_WINDOW diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt index 8d822c252288..76dfe37c7cc0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt @@ -20,37 +20,68 @@ import android.app.ActivityManager.RunningTaskInfo import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.content.res.ColorStateList import android.graphics.Color +import android.graphics.Point +import android.view.SurfaceControl import android.view.View +import android.view.View.OnClickListener +import android.view.View.OnHoverListener import android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS +import android.view.WindowManager import android.widget.ImageButton +import com.android.window.flags.Flags import com.android.wm.shell.R import com.android.wm.shell.animation.Interpolators +import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer /** * A desktop mode window decoration used when the window is in full "focus" (i.e. fullscreen/split). * It hosts a simple handle bar from which to initiate a drag motion to enter desktop mode. */ internal class AppHandleViewHolder( - rootView: View, - onCaptionTouchListener: View.OnTouchListener, - onCaptionButtonClickListener: View.OnClickListener + rootView: View, + private val onCaptionTouchListener: View.OnTouchListener, + private val onCaptionButtonClickListener: OnClickListener, + private val onCaptionHoverListener: OnHoverListener, ) : WindowDecorationViewHolder(rootView) { companion object { private const val CAPTION_HANDLE_ANIMATION_DURATION: Long = 100 } - + private lateinit var taskInfo: RunningTaskInfo + private val windowManager = context.getSystemService(WindowManager::class.java) private val captionView: View = rootView.requireViewById(R.id.desktop_mode_caption) private val captionHandle: ImageButton = rootView.requireViewById(R.id.caption_handle) + // An invisible View that takes up the same coordinates as captionHandle but is layered + // above the status bar. The purpose of this View is to receive input intended for + // captionHandle. + private var statusBarInputLayer: AdditionalSystemViewContainer? = null + init { captionView.setOnTouchListener(onCaptionTouchListener) captionHandle.setOnTouchListener(onCaptionTouchListener) captionHandle.setOnClickListener(onCaptionButtonClickListener) + captionHandle.setOnHoverListener(onCaptionHoverListener) } - override fun bindData(taskInfo: RunningTaskInfo) { + override fun bindData( + taskInfo: RunningTaskInfo, + position: Point, + width: Int, + height: Int, + isCaptionVisible: Boolean + ) { captionHandle.imageTintList = ColorStateList.valueOf(getCaptionHandleBarColor(taskInfo)) + this.taskInfo = taskInfo + if (!isCaptionVisible && hasStatusBarInputLayer()) { + disposeStatusBarInputLayer() + return + } + if (hasStatusBarInputLayer()) { + updateStatusBarInputLayer(position) + } else { + createStatusBarInputLayer(position, width, height) + } } override fun onHandleMenuOpened() { @@ -61,6 +92,45 @@ internal class AppHandleViewHolder( animateCaptionHandleAlpha(startValue = 0f, endValue = 1f) } + private fun createStatusBarInputLayer(handlePosition: Point, + handleWidth: Int, + handleHeight: Int) { + if (!Flags.enableAdditionalWindowsAboveStatusBar()) return + statusBarInputLayer = AdditionalSystemViewContainer(context, taskInfo.taskId, + handlePosition.x, handlePosition.y, handleWidth, handleHeight) + val view = statusBarInputLayer?.view + val lp = view?.layoutParams as WindowManager.LayoutParams + lp.title = "Handle Input Layer of task " + taskInfo.taskId + lp.setTrustedOverlay() + // Make this window a spy window to enable it to pilfer pointers from the system-wide + // gesture listener that receives events before window. This is to prevent notification + // shade gesture when we swipe down to enter desktop. + lp.inputFeatures = WindowManager.LayoutParams.INPUT_FEATURE_SPY + view.id = R.id.caption_handle + view.setOnClickListener(onCaptionButtonClickListener) + view.setOnTouchListener(onCaptionTouchListener) + view.setOnHoverListener(onCaptionHoverListener) + windowManager.updateViewLayout(view, lp) + } + + private fun updateStatusBarInputLayer(globalPosition: Point) { + statusBarInputLayer?.setPosition(SurfaceControl.Transaction(), globalPosition.x.toFloat(), + globalPosition.y.toFloat()) ?: return + } + + private fun hasStatusBarInputLayer(): Boolean { + return statusBarInputLayer != null + } + + /** + * Remove the input layer from [WindowManager]. Should be used when caption handle + * is not visible. + */ + fun disposeStatusBarInputLayer() { + statusBarInputLayer?.releaseView() + statusBarInputLayer = null + } + private fun getCaptionHandleBarColor(taskInfo: RunningTaskInfo): Int { return if (shouldUseLightCaptionColors(taskInfo)) { context.getColor(R.color.desktop_mode_caption_handle_bar_light) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt index 46127b177bc3..b704d9c001c8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt @@ -21,6 +21,7 @@ import android.content.res.ColorStateList import android.content.res.Configuration import android.graphics.Bitmap import android.graphics.Color +import android.graphics.Point import android.graphics.drawable.LayerDrawable import android.graphics.drawable.RippleDrawable import android.graphics.drawable.ShapeDrawable @@ -136,7 +137,13 @@ internal class AppHeaderViewHolder( onMaximizeHoverAnimationFinishedListener } - override fun bindData(taskInfo: RunningTaskInfo) { + override fun bindData( + taskInfo: RunningTaskInfo, + position: Point, + width: Int, + height: Int, + isCaptionVisible: Boolean + ) { if (Flags.enableThemedAppHeaders()) { bindDataWithThemedHeaders(taskInfo) } else { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/WindowDecorationViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/WindowDecorationViewHolder.kt index 5ae8d252a908..2341b099699f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/WindowDecorationViewHolder.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/WindowDecorationViewHolder.kt @@ -17,6 +17,7 @@ package com.android.wm.shell.windowdecor.viewholder import android.app.ActivityManager.RunningTaskInfo import android.content.Context +import android.graphics.Point import android.view.View /** @@ -30,7 +31,13 @@ internal abstract class WindowDecorationViewHolder(rootView: View) { * A signal to the view holder that new data is available and that the views should be updated to * reflect it. */ - abstract fun bindData(taskInfo: RunningTaskInfo) + abstract fun bindData( + taskInfo: RunningTaskInfo, + position: Point, + width: Int, + height: Int, + isCaptionVisible: Boolean + ) /** Callback when the handle menu is opened. */ abstract fun onHandleMenuOpened() diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt index db962e717a3b..2406bdeebdf2 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt @@ -48,7 +48,10 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { @Before fun setup() { - tapl.workspace.switchToOverview().dismissAllTasks() + val overview = tapl.workspace.switchToOverview() + if (overview.hasTasks()) { + overview.dismissAllTasks() + } tapl.setEnableRotation(true) tapl.setExpectedRotation(rotation.value) 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 da886863cc1f..8421365e594d 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 @@ -580,138 +580,6 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun moveToDesktop_landscapeDevice_resizable_undefinedOrientation_defaultLandscapeBounds() { - val task = setUpFullscreenTask() - setUpLandscapeDisplay() - - controller.moveToDesktop(task, transitionSource = UNKNOWN) - val wct = getLatestEnterDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun moveToDesktop_landscapeDevice_resizable_landscapeOrientation_defaultLandscapeBounds() { - val task = setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_LANDSCAPE) - setUpLandscapeDisplay() - - controller.moveToDesktop(task, transitionSource = UNKNOWN) - val wct = getLatestEnterDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun moveToDesktop_landscapeDevice_resizable_portraitOrientation_resizablePortraitBounds() { - val task = - setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_PORTRAIT, shouldLetterbox = true) - setUpLandscapeDisplay() - - controller.moveToDesktop(task, transitionSource = UNKNOWN) - val wct = getLatestEnterDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_PORTRAIT_BOUNDS) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun moveToDesktop_landscapeDevice_unResizable_landscapeOrientation_defaultLandscapeBounds() { - val task = - setUpFullscreenTask(isResizable = false, screenOrientation = SCREEN_ORIENTATION_LANDSCAPE) - setUpLandscapeDisplay() - - controller.moveToDesktop(task, transitionSource = UNKNOWN) - val wct = getLatestEnterDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun moveToDesktop_landscapeDevice_unResizable_portraitOrientation_unResizablePortraitBounds() { - val task = - setUpFullscreenTask( - isResizable = false, - screenOrientation = SCREEN_ORIENTATION_PORTRAIT, - shouldLetterbox = true) - setUpLandscapeDisplay() - - controller.moveToDesktop(task, transitionSource = UNKNOWN) - val wct = getLatestEnterDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_PORTRAIT_BOUNDS) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun moveToDesktop_portraitDevice_resizable_undefinedOrientation_defaultPortraitBounds() { - val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT) - setUpPortraitDisplay() - - controller.moveToDesktop(task, transitionSource = UNKNOWN) - val wct = getLatestEnterDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun moveToDesktop_portraitDevice_resizable_portraitOrientation_defaultPortraitBounds() { - val task = - setUpFullscreenTask( - deviceOrientation = ORIENTATION_PORTRAIT, - screenOrientation = SCREEN_ORIENTATION_PORTRAIT) - setUpPortraitDisplay() - - controller.moveToDesktop(task, transitionSource = UNKNOWN) - val wct = getLatestEnterDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun moveToDesktop_portraitDevice_resizable_landscapeOrientation_resizableLandscapeBounds() { - val task = - setUpFullscreenTask( - deviceOrientation = ORIENTATION_PORTRAIT, - screenOrientation = SCREEN_ORIENTATION_LANDSCAPE, - shouldLetterbox = true) - setUpPortraitDisplay() - - controller.moveToDesktop(task, transitionSource = UNKNOWN) - val wct = getLatestEnterDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_LANDSCAPE_BOUNDS) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun moveToDesktop_portraitDevice_unResizable_portraitOrientation_defaultPortraitBounds() { - val task = - setUpFullscreenTask( - isResizable = false, - deviceOrientation = ORIENTATION_PORTRAIT, - screenOrientation = SCREEN_ORIENTATION_PORTRAIT) - setUpPortraitDisplay() - - controller.moveToDesktop(task, transitionSource = UNKNOWN) - val wct = getLatestEnterDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun moveToDesktop_portraitDevice_unResizable_landscapeOrientation_unResizableLandscapeBounds() { - val task = - setUpFullscreenTask( - isResizable = false, - deviceOrientation = ORIENTATION_PORTRAIT, - screenOrientation = SCREEN_ORIENTATION_LANDSCAPE, - shouldLetterbox = true) - setUpPortraitDisplay() - - controller.moveToDesktop(task, transitionSource = UNKNOWN) - val wct = getLatestEnterDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_LANDSCAPE_BOUNDS) - } - - @Test fun moveToDesktop_tdaFullscreen_windowingModeSetToFreeform() { val task = setUpFullscreenTask() val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! @@ -1326,6 +1194,22 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + fun handleRequest_freeformTask_relaunchActiveTask_taskBecomesUndefined() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val freeformTask = setUpFreeformTask() + markTaskHidden(freeformTask) + + val wct = + controller.handleRequest(Binder(), createTransition(freeformTask)) + + // Should become undefined as the TDA is set to fullscreen. It will inherit from the TDA. + assertNotNull(wct, "should handle request") + assertThat(wct.changes[freeformTask.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_UNDEFINED) + } + + @Test @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun handleRequest_freeformTask_desktopWallpaperDisabled_freeformNotVisible_reorderedToTop() { assumeTrue(ENABLE_SHELL_TRANSITIONS) @@ -1574,7 +1458,7 @@ class DesktopTasksControllerTest : ShellTestCase() { Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION, ) - fun handleRequest_backTransition_singleActiveTaskNoToken_wallpaperDisabled_backNavDisabled_doesNotHandle() { + fun handleRequest_backTransition_singleTaskNoToken_noWallpaper_noBackNav_doesNotHandle() { val task = setUpFreeformTask() val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK)) @@ -1587,7 +1471,7 @@ class DesktopTasksControllerTest : ShellTestCase() { Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION ) - fun handleRequest_backTransition_singleActiveTaskNoToken_wallpaperEnabled_backNavEnabled_removesTask() { + fun handleRequest_backTransition_singleTaskNoToken_withWallpaper_withBackNav_removesTask() { val task = setUpFreeformTask() val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK)) @@ -1598,7 +1482,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) - fun handleRequest_backTransition_singleActiveTaskNoToken_backNavigationDisabled_doesNotHandle() { + fun handleRequest_backTransition_singleTaskNoToken_noBackNav_doesNotHandle() { val task = setUpFreeformTask() val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK)) @@ -1611,7 +1495,7 @@ class DesktopTasksControllerTest : ShellTestCase() { Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION ) - fun handleRequest_backTransition_singleActiveTaskWithToken_wallpaperDisabled_backNavDisabled_doesNotHandle() { + fun handleRequest_backTransition_singleTaskWithToken_noWallpaper_noBackNav_doesNotHandle() { val task = setUpFreeformTask() desktopModeTaskRepository.wallpaperActivityToken = MockToken().token() @@ -1625,7 +1509,7 @@ class DesktopTasksControllerTest : ShellTestCase() { Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION ) - fun handleRequest_backTransition_singleActiveTaskWithToken_wallpaperEnabled_backNavEnabled_removesWallpaperAndTask() { + fun handleRequest_backTransition_singleTask_withWallpaper_withBackNav_removesWallpaperAndTask() { val task = setUpFreeformTask() val wallpaperToken = MockToken().token() @@ -1640,7 +1524,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) - fun handleRequest_backTransition_singleActiveTaskWithToken_backNavigationDisabled_removesWallpaper() { + fun handleRequest_backTransition_singleTaskWithToken_noBackNav_removesWallpaper() { val task = setUpFreeformTask() val wallpaperToken = MockToken().token() @@ -1656,7 +1540,7 @@ class DesktopTasksControllerTest : ShellTestCase() { Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION ) - fun handleRequest_backTransition_multipleTasks_wallpaperDisabled_backNavDisabled_doesNotHandle() { + fun handleRequest_backTransition_multipleTasks_noWallpaper_noBackNav_doesNotHandle() { val task1 = setUpFreeformTask() setUpFreeformTask() @@ -1671,7 +1555,7 @@ class DesktopTasksControllerTest : ShellTestCase() { Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION ) - fun handleRequest_backTransition_multipleTasks_wallpaperEnabled_backNavEnabled_removesTask() { + fun handleRequest_backTransition_multipleTasks_withWallpaper_withBackNav_removesTask() { val task1 = setUpFreeformTask() setUpFreeformTask() @@ -1684,7 +1568,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) - fun handleRequest_backTransition_multipleTasks_backNavigationDisabled_doesNotHandle() { + fun handleRequest_backTransition_multipleTasks_noBackNav_doesNotHandle() { val task1 = setUpFreeformTask() setUpFreeformTask() @@ -1699,7 +1583,7 @@ class DesktopTasksControllerTest : ShellTestCase() { Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION ) - fun handleRequest_backTransition_multipleTasksSingleNonClosing_wallpaperEnabled_backNavEnabled_removesWallpaperAndTask() { + fun handleRequest_backTransition_multipleTasksSingleNonClosing_removesWallpaperAndTask() { val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) val wallpaperToken = MockToken().token() @@ -1716,7 +1600,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) - fun handleRequest_backTransition_multipleTasksSingleNonClosing_backNavigationDisabled_removesWallpaper() { + fun handleRequest_backTransition_multipleTasksSingleNonClosing_noBackNav_removesWallpaper() { val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) val wallpaperToken = MockToken().token() @@ -1734,7 +1618,7 @@ class DesktopTasksControllerTest : ShellTestCase() { Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION ) - fun handleRequest_backTransition_multipleTasksSingleNonMinimized_wallpaperEnabled_backNavEnabled_removesWallpaperAndTask() { + fun handleRequest_backTransition_multipleTasksSingleNonMinimized_removesWallpaperAndTask() { val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) val wallpaperToken = MockToken().token() @@ -1751,7 +1635,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) - fun handleRequest_backTransition_multipleTasksSingleNonMinimized_backNavigationDisabled_removesWallpaper() { + fun handleRequest_backTransition_multipleTasksSingleNonMinimized_noBackNav_removesWallpaper() { val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) val wallpaperToken = MockToken().token() @@ -1769,7 +1653,7 @@ class DesktopTasksControllerTest : ShellTestCase() { Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION ) - fun handleRequest_backTransition_nonMinimizadTask_wallpaperEnabled_backNavEnabled_removesWallpaper() { + fun handleRequest_backTransition_nonMinimizadTask_withWallpaper_withBackNav_removesWallpaper() { val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) val wallpaperToken = MockToken().token() @@ -1789,7 +1673,7 @@ class DesktopTasksControllerTest : ShellTestCase() { Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION ) - fun handleRequest_closeTransition_singleActiveTaskNoToken_wallpaperDisabled_backNavDisabled_doesNotHandle() { + fun handleRequest_closeTransition_singleTaskNoToken_noWallpaper_noBackNav_doesNotHandle() { val task = setUpFreeformTask() val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE)) @@ -1802,7 +1686,7 @@ class DesktopTasksControllerTest : ShellTestCase() { Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION ) - fun handleRequest_closeTransition_singleActiveTaskNoToken_wallpaperEnabled_backNavEnabled_removesTask() { + fun handleRequest_closeTransition_singleTaskNoToken_withWallpaper_withBackNav_removesTask() { val task = setUpFreeformTask() val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE)) @@ -1813,7 +1697,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) - fun handleRequest_closeTransition_singleActiveTaskNoToken_backNavigationDisabled_doesNotHandle() { + fun handleRequest_closeTransition_singleTaskNoToken_noBackNav_doesNotHandle() { val task = setUpFreeformTask() val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE)) @@ -1826,7 +1710,7 @@ class DesktopTasksControllerTest : ShellTestCase() { Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION ) - fun handleRequest_closeTransition_singleActiveTaskWithToken_wallpaperDisabled_backNavDisabled_doesNotHandle() { + fun handleRequest_closeTransition_singleTaskWithToken_noWallpaper_noBackNav_doesNotHandle() { val task = setUpFreeformTask() desktopModeTaskRepository.wallpaperActivityToken = MockToken().token() @@ -1840,7 +1724,7 @@ class DesktopTasksControllerTest : ShellTestCase() { Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION ) - fun handleRequest_closeTransition_singleActiveTaskWithToken_wallpaperEnabled_backNavEnabled_removesWallpaperAndTask() { + fun handleRequest_closeTransition_singleTaskWithToken_removesWallpaperAndTask() { val task = setUpFreeformTask() val wallpaperToken = MockToken().token() @@ -1855,7 +1739,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) - fun handleRequest_closeTransition_singleActiveTaskWithToken_backNavigationDisabled_removesWallpaper() { + fun handleRequest_closeTransition_singleTaskWithToken_withWallpaper_noBackNav_removesWallpaper() { val task = setUpFreeformTask() val wallpaperToken = MockToken().token() @@ -1871,7 +1755,7 @@ class DesktopTasksControllerTest : ShellTestCase() { Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION ) - fun handleRequest_closeTransition_multipleTasks_wallpaperDisabled_backNavDisabled_doesNotHandle() { + fun handleRequest_closeTransition_multipleTasks_noWallpaper_noBackNav_doesNotHandle() { val task1 = setUpFreeformTask() setUpFreeformTask() @@ -1886,7 +1770,7 @@ class DesktopTasksControllerTest : ShellTestCase() { Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION ) - fun handleRequest_closeTransition_multipleTasks_wallpaperEnabled_backNavEnabled_removesTask() { + fun handleRequest_closeTransition_multipleTasks_withWallpaper_withBackNav_removesTask() { val task1 = setUpFreeformTask() setUpFreeformTask() @@ -1900,7 +1784,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) - fun handleRequest_closeTransition_multipleTasksFlagEnabled_backNavigationDisabled_doesNotHandle() { + fun handleRequest_closeTransition_multipleTasksFlagEnabled_noBackNav_doesNotHandle() { val task1 = setUpFreeformTask() setUpFreeformTask() @@ -1915,7 +1799,7 @@ class DesktopTasksControllerTest : ShellTestCase() { Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION ) - fun handleRequest_closeTransition_multipleTasksSingleNonClosing_wallpaperEnabled_backNavEnabled_removesWallpaperAndTask() { + fun handleRequest_closeTransition_multipleTasksSingleNonClosing_removesWallpaperAndTask() { val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) val wallpaperToken = MockToken().token() @@ -1932,7 +1816,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) - fun handleRequest_closeTransition_multipleTasksSingleNonClosing_backNavigationDisabled_removesWallpaper() { + fun handleRequest_closeTransition_multipleTasksSingleNonClosing_noBackNav_removesWallpaper() { val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) val wallpaperToken = MockToken().token() @@ -1950,7 +1834,7 @@ class DesktopTasksControllerTest : ShellTestCase() { Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION ) - fun handleRequest_closeTransition_multipleTasksOneNonMinimized_wallpaperEnabled_backNavEnabled_removesWallpaperAndTask() { + fun handleRequest_closeTransition_multipleTasksOneNonMinimized_removesWallpaperAndTask() { val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) val wallpaperToken = MockToken().token() @@ -1967,7 +1851,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) - fun handleRequest_closeTransition_multipleTasksSingleNonMinimized_backNavigationDisabled_removesWallpaper() { + fun handleRequest_closeTransition_multipleTasksSingleNonMinimized_noBackNav_removesWallpaper() { val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) val wallpaperToken = MockToken().token() @@ -1985,7 +1869,7 @@ class DesktopTasksControllerTest : ShellTestCase() { Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION ) - fun handleRequest_closeTransition_minimizadTask_wallpaperEnabled_backNavEnabled_removesWallpaper() { + fun handleRequest_closeTransition_minimizadTask_withWallpaper_withBackNav_removesWallpaper() { val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) val wallpaperToken = MockToken().token() diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java index 5b95b1588814..1c5d5e963156 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java @@ -50,7 +50,7 @@ import android.content.ComponentName; import android.content.Intent; import android.content.pm.ActivityInfo; import android.os.Bundle; -import android.os.IBinder; +import android.os.Handler; import android.window.IWindowContainerToken; import android.window.WindowContainerToken; @@ -104,6 +104,7 @@ public class SplitScreenControllerTests extends ShellTestCase { @Mock SyncTransactionQueue mSyncQueue; @Mock RootTaskDisplayAreaOrganizer mRootTDAOrganizer; @Mock ShellExecutor mMainExecutor; + @Mock Handler mMainHandler; @Mock DisplayController mDisplayController; @Mock DisplayImeController mDisplayImeController; @Mock DisplayInsetsController mDisplayInsetsController; @@ -134,7 +135,7 @@ public class SplitScreenControllerTests extends ShellTestCase { mDisplayInsetsController, mDragAndDropController, mTransitions, mTransactionPool, mIconProvider, Optional.of(mRecentTasks), mLaunchAdjacentController, Optional.of(mWindowDecorViewModel), Optional.of(mDesktopTasksController), - mStageCoordinator, mMultiInstanceHelper, mMainExecutor)); + mStageCoordinator, mMultiInstanceHelper, mMainExecutor, mMainHandler)); } @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java index a3009a55198f..29d3fb4cc04e 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java @@ -22,6 +22,7 @@ import static org.mockito.Mockito.mock; import android.app.ActivityManager; import android.content.Context; import android.graphics.Rect; +import android.os.Handler; import android.view.SurfaceControl; import android.view.SurfaceSession; @@ -77,13 +78,13 @@ public class SplitTestUtils { DisplayController displayController, DisplayImeController imeController, DisplayInsetsController insetsController, SplitLayout splitLayout, Transitions transitions, TransactionPool transactionPool, - ShellExecutor mainExecutor, + ShellExecutor mainExecutor, Handler mainHandler, Optional<RecentTasksController> recentTasks, LaunchAdjacentController launchAdjacentController, Optional<WindowDecorViewModel> windowDecorViewModel) { super(context, displayId, syncQueue, taskOrganizer, mainStage, sideStage, displayController, imeController, insetsController, splitLayout, - transitions, transactionPool, mainExecutor, recentTasks, + transitions, transactionPool, mainExecutor, mainHandler, recentTasks, launchAdjacentController, windowDecorViewModel); // Prepare root task for testing. diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java index 34b2eebb15a1..37ef7881bde7 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java @@ -49,6 +49,7 @@ import static org.mockito.Mockito.verify; import android.annotation.NonNull; import android.app.ActivityManager; +import android.os.Handler; import android.os.IBinder; import android.view.SurfaceControl; import android.view.SurfaceSession; @@ -107,6 +108,7 @@ public class SplitTransitionTests extends ShellTestCase { @Mock private IconProvider mIconProvider; @Mock private WindowDecorViewModel mWindowDecorViewModel; @Mock private ShellExecutor mMainExecutor; + @Mock private Handler mMainHandler; @Mock private LaunchAdjacentController mLaunchAdjacentController; @Mock private DefaultMixedHandler mMixedHandler; @Mock private SplitScreen.SplitInvocationListener mInvocationListener; @@ -140,7 +142,7 @@ public class SplitTransitionTests extends ShellTestCase { mStageCoordinator = new SplitTestUtils.TestStageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue, mTaskOrganizer, mMainStage, mSideStage, mDisplayController, mDisplayImeController, mDisplayInsetsController, mSplitLayout, mTransitions, - mTransactionPool, mMainExecutor, Optional.empty(), + mTransactionPool, mMainExecutor, mMainHandler, Optional.empty(), mLaunchAdjacentController, Optional.empty()); mStageCoordinator.setMixedHandler(mMixedHandler); mSplitScreenTransitions = mStageCoordinator.getSplitTransitions(); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java index d18fec2f24ad..eaef704b7d78 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java @@ -138,7 +138,8 @@ public class StageCoordinatorTests extends ShellTestCase { mStageCoordinator = spy(new StageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue, mTaskOrganizer, mMainStage, mSideStage, mDisplayController, mDisplayImeController, mDisplayInsetsController, mSplitLayout, mTransitions, mTransactionPool, - mMainExecutor, Optional.empty(), mLaunchAdjacentController, Optional.empty())); + mMainExecutor, mMainHandler, Optional.empty(), mLaunchAdjacentController, + Optional.empty())); mDividerLeash = new SurfaceControl.Builder(mSurfaceSession).setName("fakeDivider").build(); when(mSplitLayout.getBounds1()).thenReturn(mBounds1); @@ -347,8 +348,7 @@ public class StageCoordinatorTests extends ShellTestCase { assertThat(options.getLaunchRootTask()).isEqualTo(mMainStage.mRootTaskInfo.token); assertThat(options.getPendingIntentBackgroundActivityStartMode()) - .isEqualTo(ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED); - assertThat(options.isPendingIntentBackgroundActivityLaunchAllowedByPermission()).isTrue(); + .isEqualTo(ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS); } @Test @@ -359,10 +359,11 @@ public class StageCoordinatorTests extends ShellTestCase { mMainStage.mRootTaskInfo = new TestRunningTaskInfoBuilder().setVisible(true).build(); when(mStageCoordinator.isSplitActive()).thenReturn(true); when(mStageCoordinator.isSplitScreenVisible()).thenReturn(true); + when(mStageCoordinator.willSleepOnFold()).thenReturn(true); mStageCoordinator.onFoldedStateChanged(true); - assertEquals(mStageCoordinator.mTopStageAfterFoldDismiss, STAGE_TYPE_MAIN); + assertEquals(mStageCoordinator.mLastActiveStage, STAGE_TYPE_MAIN); mStageCoordinator.onFinishedWakingUp(); 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 b1803e97b107..aeae0bebb697 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 @@ -30,6 +30,7 @@ import android.hardware.display.DisplayManager import android.hardware.display.VirtualDisplay import android.hardware.input.InputManager 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 @@ -62,6 +63,7 @@ import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase import com.android.wm.shell.TestRunningTaskInfoBuilder +import com.android.wm.shell.TestShellExecutor import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayInsetsController import com.android.wm.shell.common.DisplayLayout @@ -71,6 +73,7 @@ import com.android.wm.shell.desktopmode.DesktopTasksController 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 @@ -78,8 +81,6 @@ import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.Transitions import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel.DesktopModeOnInsetsChangedListener import com.android.wm.shell.windowdecor.common.OnTaskActionClickListener -import java.util.Optional -import java.util.function.Supplier import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Rule @@ -99,6 +100,8 @@ import org.mockito.kotlin.eq import org.mockito.kotlin.spy import org.mockito.kotlin.whenever import org.mockito.quality.Strictness +import java.util.Optional +import java.util.function.Supplier /** * Tests of [DesktopModeWindowDecorViewModel] @@ -122,6 +125,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { @Mock private lateinit var mockMainChoreographer: Choreographer @Mock private lateinit var mockTaskOrganizer: ShellTaskOrganizer @Mock private lateinit var mockDisplayController: DisplayController + @Mock private lateinit var mockSplitScreenController: SplitScreenController @Mock private lateinit var mockDisplayLayout: DisplayLayout @Mock private lateinit var displayInsetsController: DisplayInsetsController @Mock private lateinit var mockSyncQueue: SyncTransactionQueue @@ -136,6 +140,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { @Mock private lateinit var mockShellCommandHandler: ShellCommandHandler @Mock private lateinit var mockWindowManager: IWindowManager @Mock private lateinit var mockInteractionJankMonitor: InteractionJankMonitor + private val bgExecutor = TestShellExecutor() private val transactionFactory = Supplier<SurfaceControl.Transaction> { SurfaceControl.Transaction() @@ -155,6 +160,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { mockShellExecutor, mockMainHandler, mockMainChoreographer, + bgExecutor, shellInit, mockShellCommandHandler, mockWindowManager, @@ -171,7 +177,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { mockRootTaskDisplayAreaOrganizer, windowDecorByTaskIdSpy, mockInteractionJankMonitor ) - + desktopModeWindowDecorViewModel.setSplitScreenController(mockSplitScreenController) whenever(mockDisplayController.getDisplayLayout(any())).thenReturn(mockDisplayLayout) whenever(mockDisplayLayout.stableInsets()).thenReturn(STABLE_INSETS) whenever(mockInputMonitorFactory.create(any(), any())).thenReturn(mockInputMonitor) @@ -204,10 +210,12 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { verify(mockDesktopModeWindowDecorFactory).create( mContext, mockDisplayController, + mockSplitScreenController, mockTaskOrganizer, task, taskSurface, mockMainHandler, + bgExecutor, mockMainChoreographer, mockSyncQueue, mockRootTaskDisplayAreaOrganizer @@ -228,10 +236,12 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { verify(mockDesktopModeWindowDecorFactory, never()).create( mContext, mockDisplayController, + mockSplitScreenController, mockTaskOrganizer, task, taskSurface, mockMainHandler, + bgExecutor, mockMainChoreographer, mockSyncQueue, mockRootTaskDisplayAreaOrganizer @@ -243,10 +253,12 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { verify(mockDesktopModeWindowDecorFactory, times(1)).create( mContext, mockDisplayController, + mockSplitScreenController, mockTaskOrganizer, task, taskSurface, mockMainHandler, + bgExecutor, mockMainChoreographer, mockSyncQueue, mockRootTaskDisplayAreaOrganizer @@ -254,6 +266,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { } @Test + @DisableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR) fun testCreateAndDisposeEventReceiver() { val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM) setUpMockDecorationForTask(task) @@ -266,6 +279,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { } @Test + @DisableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR) fun testEventReceiversOnMultipleDisplays() { val secondaryDisplay = createVirtualDisplay() ?: return val secondaryDisplayId = secondaryDisplay.display.displayId @@ -344,7 +358,8 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { onTaskChanging(task) verify(mockDesktopModeWindowDecorFactory, never()) - .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any()) + .create(any(), any(), any(), any(), eq(task), any(), any(), any(), any(), any(), + any()) } @Test @@ -365,7 +380,8 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { onTaskOpening(task) verify(mockDesktopModeWindowDecorFactory) - .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any()) + .create(any(), any(), any(), any(), eq(task), any(), any(), any(), any(), + any(), any()) } finally { mockitoSession.finishMocking() } @@ -382,7 +398,8 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { onTaskOpening(task) verify(mockDesktopModeWindowDecorFactory, never()) - .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any()) + .create(any(), any(), any(), any(), eq(task), any(), any(), any(), any(), any(), + any()) } @Test @@ -399,7 +416,8 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { onTaskOpening(task) verify(mockDesktopModeWindowDecorFactory, never()) - .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any()) + .create(any(), any(), any(), any(), eq(task), any(), any(), any(), any(), + any(), any()) } @Test @@ -496,7 +514,8 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { onTaskOpening(task) verify(mockDesktopModeWindowDecorFactory, never()) - .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any()) + .create(any(), any(), any(), any(), eq(task), any(), any(), any(), any(), any(), + any()) } finally { mockitoSession.finishMocking() } @@ -520,7 +539,8 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { onTaskOpening(task) verify(mockDesktopModeWindowDecorFactory) - .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any()) + .create(any(), any(), any(), any(), eq(task), any(), any(), any(), any(), any(), + any()) } finally { mockitoSession.finishMocking() } @@ -543,7 +563,8 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { onTaskOpening(task) verify(mockDesktopModeWindowDecorFactory) - .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any()) + .create(any(), any(), any(), any(), eq(task), any(), any(), any(), any(), any(), + any()) } finally { mockitoSession.finishMocking() } @@ -682,7 +703,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { val decoration = mock(DesktopModeWindowDecoration::class.java) whenever( mockDesktopModeWindowDecorFactory.create( - any(), any(), any(), eq(task), any(), any(), any(), any(), any()) + any(), any(), any(), any(), eq(task), any(), any(), any(), any(), any(), any()) ).thenReturn(decoration) decoration.mTaskInfo = task whenever(decoration.isFocused).thenReturn(task.isFocused) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java index d8606093ac5c..412fef30d4fb 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java @@ -82,9 +82,12 @@ import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestRunningTaskInfoBuilder; +import com.android.wm.shell.TestShellExecutor; import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; +import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.windowdecor.WindowDecoration.RelayoutParams; import com.android.wm.shell.windowdecor.common.OnTaskActionClickListener; @@ -127,6 +130,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { @Mock private DisplayController mMockDisplayController; @Mock + private SplitScreenController mMockSplitScreenController; + @Mock private ShellTaskOrganizer mMockShellTaskOrganizer; @Mock private Choreographer mMockChoreographer; @@ -165,6 +170,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { private SurfaceControl.Transaction mMockTransaction; private StaticMockitoSession mMockitoSession; private TestableContext mTestableContext; + private ShellExecutor mBgExecutor = new TestShellExecutor(); /** Set up run before test class. */ @BeforeClass @@ -392,6 +398,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { } @Test + @DisableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR) public void relayout_fullscreenTask_appliesTransactionImmediately() { final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo)); @@ -418,6 +425,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { } @Test + @DisableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR) public void relayout_fullscreenTask_doesNotCreateViewHostImmediately() { final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo)); @@ -429,6 +437,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { } @Test + @DisableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR) public void relayout_fullscreenTask_postsViewHostCreation() { final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo)); @@ -457,6 +466,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { } @Test + @DisableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR) public void relayout_removesExistingHandlerCallback() { final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo)); @@ -471,6 +481,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { } @Test + @DisableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR) public void close_removesExistingHandlerCallback() { final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo)); @@ -656,8 +667,9 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { ActivityManager.RunningTaskInfo taskInfo, MaximizeMenuFactory maximizeMenuFactory) { final DesktopModeWindowDecoration windowDecor = new DesktopModeWindowDecoration(mContext, - mMockDisplayController, mMockShellTaskOrganizer, taskInfo, mMockSurfaceControl, - mMockHandler, mMockChoreographer, mMockSyncQueue, mMockRootTaskDisplayAreaOrganizer, + mMockDisplayController, mMockSplitScreenController, mMockShellTaskOrganizer, + taskInfo, mMockSurfaceControl, mMockHandler, mBgExecutor, mMockChoreographer, + mMockSyncQueue, mMockRootTaskDisplayAreaOrganizer, SurfaceControl.Builder::new, mMockTransactionSupplier, WindowContainerTransaction::new, SurfaceControl::new, mMockSurfaceControlViewHostFactory, maximizeMenuFactory); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt index adda9a688172..e548f8f1eb2a 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt @@ -130,7 +130,7 @@ class HandleMenuTest : ShellTestCase() { @EnableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR) fun testFullscreenMenuUsesSystemViewContainer() { createTaskInfo(WINDOWING_MODE_FULLSCREEN, SPLIT_POSITION_UNDEFINED) - val handleMenu = createAndShowHandleMenu() + val handleMenu = createAndShowHandleMenu(SPLIT_POSITION_UNDEFINED) assertTrue(handleMenu.handleMenuViewContainer is AdditionalSystemViewContainer) // Verify menu is created at coordinates that, when added to WindowManager, // show at the top-center of display. @@ -142,7 +142,7 @@ class HandleMenuTest : ShellTestCase() { @EnableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR) fun testFreeformMenu_usesViewHostViewContainer() { createTaskInfo(WINDOWING_MODE_FREEFORM, SPLIT_POSITION_UNDEFINED) - handleMenu = createAndShowHandleMenu() + handleMenu = createAndShowHandleMenu(SPLIT_POSITION_UNDEFINED) assertTrue(handleMenu.handleMenuViewContainer is AdditionalViewHostViewContainer) // Verify menu is created near top-left of task. val expected = Point(MENU_START_MARGIN, MENU_TOP_MARGIN) @@ -153,7 +153,7 @@ class HandleMenuTest : ShellTestCase() { @EnableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR) fun testSplitLeftMenu_usesSystemViewContainer() { createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, SPLIT_POSITION_TOP_OR_LEFT) - handleMenu = createAndShowHandleMenu() + handleMenu = createAndShowHandleMenu(SPLIT_POSITION_TOP_OR_LEFT) assertTrue(handleMenu.handleMenuViewContainer is AdditionalSystemViewContainer) // Verify menu is created at coordinates that, when added to WindowManager, // show at the top-center of split left task. @@ -168,7 +168,7 @@ class HandleMenuTest : ShellTestCase() { @EnableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR) fun testSplitRightMenu_usesSystemViewContainer() { createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, SPLIT_POSITION_BOTTOM_OR_RIGHT) - handleMenu = createAndShowHandleMenu() + handleMenu = createAndShowHandleMenu(SPLIT_POSITION_BOTTOM_OR_RIGHT) assertTrue(handleMenu.handleMenuViewContainer is AdditionalSystemViewContainer) // Verify menu is created at coordinates that, when added to WindowManager, // show at the top-center of split right task. @@ -208,16 +208,30 @@ class HandleMenuTest : ShellTestCase() { } } - private fun createAndShowHandleMenu(): HandleMenu { + private fun createAndShowHandleMenu(splitPosition: Int): HandleMenu { val layoutId = if (mockDesktopWindowDecoration.mTaskInfo.isFreeform) { R.layout.desktop_mode_app_header } else { R.layout.desktop_mode_app_handle } + val captionX = when (mockDesktopWindowDecoration.mTaskInfo.windowingMode) { + WINDOWING_MODE_FULLSCREEN -> (DISPLAY_BOUNDS.width() / 2) - (HANDLE_WIDTH / 2) + WINDOWING_MODE_FREEFORM -> 0 + WINDOWING_MODE_MULTI_WINDOW -> { + if (splitPosition == SPLIT_POSITION_TOP_OR_LEFT) { + (SPLIT_LEFT_BOUNDS.width() / 2) - (HANDLE_WIDTH / 2) + } else { + (SPLIT_RIGHT_BOUNDS.width() / 2) - (HANDLE_WIDTH / 2) + } + } + else -> error("Invalid windowing mode") + } val handleMenu = HandleMenu(mockDesktopWindowDecoration, layoutId, onClickListener, onTouchListener, appIcon, appName, displayController, - splitScreenController, true /* shouldShowWindowingPill */, - true /* shouldShowBrowserPill */, 50 /* captionHeight */) + splitScreenController, shouldShowWindowingPill = true, + shouldShowBrowserPill = true, captionWidth = HANDLE_WIDTH, captionHeight = 50, + captionX = captionX + ) handleMenu.show() return handleMenu } @@ -233,5 +247,6 @@ class HandleMenuTest : ShellTestCase() { private const val MENU_START_MARGIN = 20 private const val MENU_PILL_ELEVATION = 2 private const val MENU_PILL_SPACING_MARGIN = 4 + private const val HANDLE_WIDTH = 80 } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainerTest.kt index d3e996b12e1f..3b490550ab61 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainerTest.kt @@ -68,12 +68,12 @@ class AdditionalSystemViewContainerTest : ShellTestCase() { fun testReleaseView_ViewRemoved() { viewContainer = AdditionalSystemViewContainer( mockContext, - R.layout.desktop_mode_window_decor_handle_menu, TASK_ID, X, Y, WIDTH, - HEIGHT + HEIGHT, + R.layout.desktop_mode_window_decor_handle_menu ) verify(mockWindowManager).addView(eq(mockView), any()) viewContainer.releaseView() diff --git a/location/java/android/location/flags/location.aconfig b/location/java/android/location/flags/location.aconfig index acfe473c260c..0edaaefbd75d 100644 --- a/location/java/android/location/flags/location.aconfig +++ b/location/java/android/location/flags/location.aconfig @@ -2,6 +2,20 @@ package: "android.location.flags" container: "system" flag { + name: "keep_gnss_stationary_throttling" + namespace: "location" + description: "Keeps stationary throttling for the GNSS provider even if the disable_stationary_throttling flag is true." + bug: "354000147" +} + +flag { + name: "disable_stationary_throttling" + namespace: "location" + description: "Disables stationary throttling for all providers" + bug: "354000147" +} + +flag { name: "new_geocoder" namespace: "location" description: "Flag for new Geocoder APIs" diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java index 2d0e7abbe890..a255f730b0f3 100644 --- a/media/java/android/media/AudioSystem.java +++ b/media/java/android/media/AudioSystem.java @@ -2651,4 +2651,11 @@ public class AudioSystem * @hide */ public static native boolean isBluetoothVariableLatencyEnabled(); + + /** + * Register a native listener for system property sysprop + * @param callback the listener which fires when the property changes + * @hide + */ + public static native void listenForSystemPropertyChange(String sysprop, Runnable callback); } diff --git a/media/java/android/media/projection/MediaProjection.java b/media/java/android/media/projection/MediaProjection.java index 999f40e53952..1c5049e891e9 100644 --- a/media/java/android/media/projection/MediaProjection.java +++ b/media/java/android/media/projection/MediaProjection.java @@ -23,6 +23,7 @@ import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledSince; import android.content.Context; import android.hardware.display.DisplayManager; +import android.hardware.display.DisplayManager.VirtualDisplayFlag; import android.hardware.display.VirtualDisplay; import android.hardware.display.VirtualDisplayConfig; import android.os.Build; @@ -140,6 +141,7 @@ public final class MediaProjection { /** * @hide */ + @Nullable public VirtualDisplay createVirtualDisplay(@NonNull String name, int width, int height, int dpi, boolean isSecure, @Nullable Surface surface, @Nullable VirtualDisplay.Callback callback, @Nullable Handler handler) { @@ -192,6 +194,11 @@ public final class MediaProjection { * <li>If attempting to create a new virtual display * associated with this MediaProjection instance after it has * been stopped by invoking {@link #stop()}. + * <li>If attempting to create a new virtual display + * associated with this MediaProjection instance after a + * {@link MediaProjection.Callback#onStop()} callback has been + * received due to the user or the system stopping the + * MediaProjection session. * <li>If the target SDK is {@link * android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE U} and up, * and if this instance has already taken a recording through @@ -208,12 +215,17 @@ public final class MediaProjection { * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE U}. * Instead, recording doesn't begin until the user re-grants * consent in the dialog. + * @return The created {@link VirtualDisplay}, or {@code null} if no {@link VirtualDisplay} + * could be created. * @see VirtualDisplay * @see VirtualDisplay.Callback */ + @SuppressWarnings("RequiresPermission") + @Nullable public VirtualDisplay createVirtualDisplay(@NonNull String name, - int width, int height, int dpi, int flags, @Nullable Surface surface, - @Nullable VirtualDisplay.Callback callback, @Nullable Handler handler) { + int width, int height, int dpi, @VirtualDisplayFlag int flags, + @Nullable Surface surface, @Nullable VirtualDisplay.Callback callback, + @Nullable Handler handler) { if (shouldMediaProjectionRequireCallback()) { if (mCallbacks.isEmpty()) { final IllegalStateException e = new IllegalStateException( diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt index 88770d487a83..186b69b4107b 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt @@ -912,8 +912,10 @@ class InstallRepository(private val context: Context) { "message: $message" ) } + + val shouldReturnResult = intent.getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false) + if (statusCode == PackageInstaller.STATUS_SUCCESS) { - val shouldReturnResult = intent.getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false) val resultIntent = if (shouldReturnResult) { Intent().putExtra(Intent.EXTRA_INSTALL_RESULT, PackageManager.INSTALL_SUCCEEDED) } else { @@ -922,12 +924,34 @@ class InstallRepository(private val context: Context) { } _installResult.setValue(InstallSuccess(appSnippet, shouldReturnResult, resultIntent)) } else { - if (statusCode != PackageInstaller.STATUS_FAILURE_ABORTED) { + // TODO (b/346655018): Use INSTALL_FAILED_ABORTED legacyCode in the condition + // statusCode can be STATUS_FAILURE_ABORTED if: + // 1. GPP blocks an install. + // 2. User denies ownership update explicitly. + // InstallFailed dialog must not be shown only when the user denies ownership update. We + // must show this dialog for all other install failures. + + val userDenied = + statusCode == PackageInstaller.STATUS_FAILURE_ABORTED && + legacyStatus != PackageManager.INSTALL_FAILED_VERIFICATION_TIMEOUT && + legacyStatus != PackageManager.INSTALL_FAILED_VERIFICATION_FAILURE + + if (shouldReturnResult) { + val resultIntent = Intent().putExtra(Intent.EXTRA_INSTALL_RESULT, legacyStatus) _installResult.setValue( - InstallFailed(appSnippet, statusCode, legacyStatus, message) + InstallFailed( + legacyCode = legacyStatus, + statusCode = statusCode, + shouldReturnResult = true, + resultIntent = resultIntent + ) ) - } else { + } else if (userDenied) { _installResult.setValue(InstallAborted(ABORT_REASON_INTERNAL_ERROR)) + } else { + _installResult.setValue( + InstallFailed(appSnippet, legacyStatus, statusCode, message) + ) } } } diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallStages.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallStages.kt index 5dd4d2905f47..8de8fbb3e688 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallStages.kt +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallStages.kt @@ -19,6 +19,7 @@ package com.android.packageinstaller.v2.model import android.app.Activity import android.content.Intent import android.content.pm.PackageManager +import android.content.pm.PackageInstaller import android.graphics.drawable.Drawable sealed class InstallStage(val stageCode: Int) { @@ -77,11 +78,10 @@ data class InstallSuccess( val shouldReturnResult: Boolean = false, /** * - * * If the caller is requesting a result back, this will hold the Intent with - * [Intent.EXTRA_INSTALL_RESULT] set to [PackageManager.INSTALL_SUCCEEDED] which is sent - * back to the caller. + * * If the caller is requesting a result back, this will hold an Intent with + * [Intent.EXTRA_INSTALL_RESULT] set to [PackageManager.INSTALL_SUCCEEDED]. * - * * If the caller doesn't want the result back, this will hold the Intent that launches + * * If the caller doesn't want the result back, this will hold an Intent that launches * the newly installed / updated app if a launchable activity exists. */ val resultIntent: Intent? = null, @@ -95,17 +95,23 @@ data class InstallSuccess( } data class InstallFailed( - private val appSnippet: PackageUtil.AppSnippet, + private val appSnippet: PackageUtil.AppSnippet? = null, val legacyCode: Int, val statusCode: Int, - val message: String?, + val message: String? = null, + val shouldReturnResult: Boolean = false, + /** + * If the caller is requesting a result back, this will hold an Intent with + * [Intent.EXTRA_INSTALL_RESULT] set to the [PackageInstaller.EXTRA_LEGACY_STATUS]. + */ + val resultIntent: Intent? = null ) : InstallStage(STAGE_FAILED) { val appIcon: Drawable? - get() = appSnippet.icon + get() = appSnippet?.icon val appLabel: String? - get() = appSnippet.label as String? + get() = appSnippet?.label as String? } data class InstallAborted( diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt index 31b9ccbdb838..e2ab31662380 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt @@ -171,15 +171,20 @@ class InstallLaunch : FragmentActivity(), InstallActionListener { val successIntent = success.resultIntent setResult(Activity.RESULT_OK, successIntent, true) } else { - val successFragment = InstallSuccessFragment(success) - showDialogInner(successFragment) + val successDialog = InstallSuccessFragment(success) + showDialogInner(successDialog) } } InstallStage.STAGE_FAILED -> { val failed = installStage as InstallFailed - val failedDialog = InstallFailedFragment(failed) - showDialogInner(failedDialog) + if (failed.shouldReturnResult) { + val failureIntent = failed.resultIntent + setResult(Activity.RESULT_FIRST_USER, failureIntent, true) + } else { + val failureDialog = InstallFailedFragment(failed) + showDialogInner(failureDialog) + } } else -> { diff --git a/packages/SettingsLib/Spa/spa/Android.bp b/packages/SettingsLib/Spa/spa/Android.bp index 6df0e9956f27..ac44a1be4cff 100644 --- a/packages/SettingsLib/Spa/spa/Android.bp +++ b/packages/SettingsLib/Spa/spa/Android.bp @@ -25,9 +25,6 @@ android_library { use_resource_processor: true, static_libs: [ "SettingsLibColor", - "androidx.slice_slice-builders", - "androidx.slice_slice-core", - "androidx.slice_slice-view", "androidx.compose.animation_animation", "androidx.compose.material3_material3", "androidx.compose.material_material-icons-extended", diff --git a/packages/SettingsLib/Spa/spa/build.gradle.kts b/packages/SettingsLib/Spa/spa/build.gradle.kts index 9b8ecf773661..ee59ec45ab4e 100644 --- a/packages/SettingsLib/Spa/spa/build.gradle.kts +++ b/packages/SettingsLib/Spa/spa/build.gradle.kts @@ -54,9 +54,6 @@ android { dependencies { api(project(":SettingsLibColor")) api("androidx.appcompat:appcompat:1.7.0-rc01") - api("androidx.slice:slice-builders:1.1.0-alpha02") - api("androidx.slice:slice-core:1.1.0-alpha02") - api("androidx.slice:slice-view:1.1.0-alpha02") api("androidx.compose.material3:material3:1.3.0-beta02") api("androidx.compose.material:material-icons-extended:$jetpackComposeVersion") api("androidx.compose.runtime:runtime-livedata:$jetpackComposeVersion") diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/SpaIntent.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/SpaIntent.kt index d8c35a36d061..9e8ca0cd71a4 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/SpaIntent.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/SpaIntent.kt @@ -25,7 +25,6 @@ import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory const val SESSION_UNKNOWN = "unknown" const val SESSION_BROWSE = "browse" const val SESSION_SEARCH = "search" -const val SESSION_SLICE = "slice" const val SESSION_EXTERNAL = "external" const val KEY_DESTINATION = "spaActivityDestination" diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/DataServiceUtils.java b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/DataServiceUtils.java index 4028b73a2c71..714f9519f378 100644 --- a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/DataServiceUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/DataServiceUtils.java @@ -18,9 +18,7 @@ package com.android.settingslib.mobile.dataservice; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; -import android.telephony.UiccCardInfo; import android.telephony.UiccPortInfo; -import android.telephony.UiccSlotInfo; import android.telephony.UiccSlotMapping; public class DataServiceUtils { @@ -71,53 +69,9 @@ public class DataServiceUtils { public static final String COLUMN_ID = "sudId"; /** - * The name of the physical slot index column, see - * {@link UiccSlotMapping#getPhysicalSlotIndex()}. - */ - public static final String COLUMN_PHYSICAL_SLOT_INDEX = "physicalSlotIndex"; - - /** - * The name of the logical slot index column, see - * {@link UiccSlotMapping#getLogicalSlotIndex()}. - */ - public static final String COLUMN_LOGICAL_SLOT_INDEX = "logicalSlotIndex"; - - /** - * The name of the card ID column, see {@link UiccCardInfo#getCardId()}. - */ - public static final String COLUMN_CARD_ID = "cardId"; - - /** - * The name of the eUICC state column, see {@link UiccCardInfo#isEuicc()}. - */ - public static final String COLUMN_IS_EUICC = "isEuicc"; - - /** - * The name of the multiple enabled profiles supported state column, see - * {@link UiccCardInfo#isMultipleEnabledProfilesSupported()}. - */ - public static final String COLUMN_IS_MULTIPLE_ENABLED_PROFILES_SUPPORTED = - "isMultipleEnabledProfilesSupported"; - - /** - * The name of the card state column, see {@link UiccSlotInfo#getCardStateInfo()}. - */ - public static final String COLUMN_CARD_STATE = "cardState"; - - /** - * The name of the removable state column, see {@link UiccSlotInfo#isRemovable()}. - */ - public static final String COLUMN_IS_REMOVABLE = "isRemovable"; - - /** * The name of the active state column, see {@link UiccPortInfo#isActive()}. */ public static final String COLUMN_IS_ACTIVE = "isActive"; - - /** - * The name of the port index column, see {@link UiccPortInfo#getPortIndex()}. - */ - public static final String COLUMN_PORT_INDEX = "portIndex"; } /** diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/MobileNetworkDatabase.java b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/MobileNetworkDatabase.java index c92204fa1f39..5f7fa278082b 100644 --- a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/MobileNetworkDatabase.java +++ b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/MobileNetworkDatabase.java @@ -19,14 +19,13 @@ package com.android.settingslib.mobile.dataservice; import android.content.Context; import android.util.Log; -import java.util.List; -import java.util.Objects; - import androidx.lifecycle.LiveData; import androidx.room.Database; import androidx.room.Room; import androidx.room.RoomDatabase; -import androidx.sqlite.db.SupportSQLiteDatabase; + +import java.util.List; +import java.util.Objects; @Database(entities = {SubscriptionInfoEntity.class, UiccInfoEntity.class, MobileNetworkInfoEntity.class}, exportSchema = false, version = 1) @@ -132,13 +131,6 @@ public abstract class MobileNetworkDatabase extends RoomDatabase { } /** - * Query the UICC info by the subscription ID from the UiccInfoEntity table. - */ - public LiveData<UiccInfoEntity> queryUiccInfoById(String id) { - return mUiccInfoDao().queryUiccInfoById(id); - } - - /** * Delete the subscriptionInfo info by the subscription ID from the SubscriptionInfoEntity * table. */ diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/UiccInfoDao.java b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/UiccInfoDao.java index 7e60421d0ab4..90e5189fdf1d 100644 --- a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/UiccInfoDao.java +++ b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/UiccInfoDao.java @@ -16,14 +16,14 @@ package com.android.settingslib.mobile.dataservice; -import java.util.List; - import androidx.lifecycle.LiveData; import androidx.room.Dao; import androidx.room.Insert; import androidx.room.OnConflictStrategy; import androidx.room.Query; +import java.util.List; + @Dao public interface UiccInfoDao { @@ -34,14 +34,6 @@ public interface UiccInfoDao { + DataServiceUtils.UiccInfoData.COLUMN_ID) LiveData<List<UiccInfoEntity>> queryAllUiccInfos(); - @Query("SELECT * FROM " + DataServiceUtils.UiccInfoData.TABLE_NAME + " WHERE " - + DataServiceUtils.UiccInfoData.COLUMN_ID + " = :subId") - LiveData<UiccInfoEntity> queryUiccInfoById(String subId); - - @Query("SELECT * FROM " + DataServiceUtils.UiccInfoData.TABLE_NAME + " WHERE " - + DataServiceUtils.UiccInfoData.COLUMN_IS_EUICC + " = :isEuicc") - LiveData<List<UiccInfoEntity>> queryUiccInfosByEuicc(boolean isEuicc); - @Query("SELECT COUNT(*) FROM " + DataServiceUtils.UiccInfoData.TABLE_NAME) int count(); diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/UiccInfoEntity.java b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/UiccInfoEntity.java index 2ccf295007dc..0f80edf52d80 100644 --- a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/UiccInfoEntity.java +++ b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/UiccInfoEntity.java @@ -26,20 +26,9 @@ import androidx.room.PrimaryKey; @Entity(tableName = DataServiceUtils.UiccInfoData.TABLE_NAME) public class UiccInfoEntity { - public UiccInfoEntity(@NonNull String subId, @NonNull String physicalSlotIndex, - int logicalSlotIndex, int cardId, boolean isEuicc, - boolean isMultipleEnabledProfilesSupported, int cardState, boolean isRemovable, - boolean isActive, int portIndex) { + public UiccInfoEntity(@NonNull String subId, boolean isActive) { this.subId = subId; - this.physicalSlotIndex = physicalSlotIndex; - this.logicalSlotIndex = logicalSlotIndex; - this.cardId = cardId; - this.isEuicc = isEuicc; - this.isMultipleEnabledProfilesSupported = isMultipleEnabledProfilesSupported; - this.cardState = cardState; - this.isRemovable = isRemovable; this.isActive = isActive; - this.portIndex = portIndex; } @PrimaryKey @@ -47,48 +36,14 @@ public class UiccInfoEntity { @NonNull public String subId; - @ColumnInfo(name = DataServiceUtils.UiccInfoData.COLUMN_PHYSICAL_SLOT_INDEX) - @NonNull - public String physicalSlotIndex; - - @ColumnInfo(name = DataServiceUtils.UiccInfoData.COLUMN_LOGICAL_SLOT_INDEX) - public int logicalSlotIndex; - - @ColumnInfo(name = DataServiceUtils.UiccInfoData.COLUMN_CARD_ID) - public int cardId; - - @ColumnInfo(name = DataServiceUtils.UiccInfoData.COLUMN_IS_EUICC) - public boolean isEuicc; - - @ColumnInfo(name = DataServiceUtils.UiccInfoData.COLUMN_IS_MULTIPLE_ENABLED_PROFILES_SUPPORTED) - public boolean isMultipleEnabledProfilesSupported; - - @ColumnInfo(name = DataServiceUtils.UiccInfoData.COLUMN_CARD_STATE) - public int cardState; - - @ColumnInfo(name = DataServiceUtils.UiccInfoData.COLUMN_IS_REMOVABLE) - public boolean isRemovable; - @ColumnInfo(name = DataServiceUtils.UiccInfoData.COLUMN_IS_ACTIVE) public boolean isActive; - @ColumnInfo(name = DataServiceUtils.UiccInfoData.COLUMN_PORT_INDEX) - public int portIndex; - - @Override public int hashCode() { int result = 17; result = 31 * result + subId.hashCode(); - result = 31 * result + physicalSlotIndex.hashCode(); - result = 31 * result + logicalSlotIndex; - result = 31 * result + cardId; - result = 31 * result + Boolean.hashCode(isEuicc); - result = 31 * result + Boolean.hashCode(isMultipleEnabledProfilesSupported); - result = 31 * result + cardState; - result = 31 * result + Boolean.hashCode(isRemovable); result = 31 * result + Boolean.hashCode(isActive); - result = 31 * result + portIndex; return result; } @@ -102,40 +57,15 @@ public class UiccInfoEntity { } UiccInfoEntity info = (UiccInfoEntity) obj; - return TextUtils.equals(subId, info.subId) - && TextUtils.equals(physicalSlotIndex, info.physicalSlotIndex) - && logicalSlotIndex == info.logicalSlotIndex - && cardId == info.cardId - && isEuicc == info.isEuicc - && isMultipleEnabledProfilesSupported == info.isMultipleEnabledProfilesSupported - && cardState == info.cardState - && isRemovable == info.isRemovable - && isActive == info.isActive - && portIndex == info.portIndex; + return TextUtils.equals(subId, info.subId) && isActive == info.isActive; } public String toString() { StringBuilder builder = new StringBuilder(); builder.append(" {UiccInfoEntity(subId = ") .append(subId) - .append(", logicalSlotIndex = ") - .append(physicalSlotIndex) - .append(", logicalSlotIndex = ") - .append(logicalSlotIndex) - .append(", cardId = ") - .append(cardId) - .append(", isEuicc = ") - .append(isEuicc) - .append(", isMultipleEnabledProfilesSupported = ") - .append(isMultipleEnabledProfilesSupported) - .append(", cardState = ") - .append(cardState) - .append(", isRemovable = ") - .append(isRemovable) .append(", isActive = ") .append(isActive) - .append(", portIndex = ") - .append(portIndex) .append(")}"); return builder.toString(); } diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt index 7886e85cbad8..49b974fa3f00 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt +++ b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt @@ -20,6 +20,7 @@ import android.app.NotificationManager import android.provider.Settings import com.android.settingslib.notification.modes.TestModeBuilder import com.android.settingslib.notification.modes.ZenMode +import java.time.Duration import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -35,8 +36,7 @@ class FakeZenModeRepository : ZenModeRepository { override val globalZenMode: StateFlow<Int> get() = mutableZenMode.asStateFlow() - private val mutableModesFlow: MutableStateFlow<List<ZenMode>> = - MutableStateFlow(listOf(TestModeBuilder.EXAMPLE)) + private val mutableModesFlow: MutableStateFlow<List<ZenMode>> = MutableStateFlow(listOf()) override val modes: Flow<List<ZenMode>> get() = mutableModesFlow.asStateFlow() @@ -52,6 +52,10 @@ class FakeZenModeRepository : ZenModeRepository { mutableZenMode.value = zenMode } + fun addModes(zenModes: List<ZenMode>) { + mutableModesFlow.value += zenModes + } + fun addMode(id: String, active: Boolean = false) { mutableModesFlow.value += newMode(id, active) } @@ -60,6 +64,20 @@ class FakeZenModeRepository : ZenModeRepository { mutableModesFlow.value = mutableModesFlow.value.filter { it.id != id } } + override fun activateMode(zenMode: ZenMode, duration: Duration?) { + activateMode(zenMode.id) + } + + override fun deactivateMode(zenMode: ZenMode) { + deactivateMode(zenMode.id) + } + + fun activateMode(id: String) { + val oldMode = mutableModesFlow.value.find { it.id == id } ?: return + removeMode(id) + mutableModesFlow.value += TestModeBuilder(oldMode).setActive(true).build() + } + fun deactivateMode(id: String) { val oldMode = mutableModesFlow.value.find { it.id == id } ?: return removeMode(id) diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt index b2fcb5f6da41..0ff7f84a08b9 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt +++ b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt @@ -30,6 +30,7 @@ import android.provider.Settings import com.android.settingslib.flags.Flags import com.android.settingslib.notification.modes.ZenMode import com.android.settingslib.notification.modes.ZenModesBackend +import java.time.Duration import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose @@ -57,6 +58,10 @@ interface ZenModeRepository { /** A list of all existing priority modes. */ val modes: Flow<List<ZenMode>> + + fun activateMode(zenMode: ZenMode, duration: Duration? = null) + + fun deactivateMode(zenMode: ZenMode) } @SuppressLint("SharedFlowCreation") @@ -178,4 +183,12 @@ class ZenModeRepositoryImpl( flowOf(emptyList()) } } + + override fun activateMode(zenMode: ZenMode, duration: Duration?) { + backend.activateMode(zenMode, duration) + } + + override fun deactivateMode(zenMode: ZenMode) { + backend.deactivateMode(zenMode) + } } 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 7b994d59d963..2f7cdd617081 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java +++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java @@ -37,6 +37,13 @@ public class TestModeBuilder { private ZenModeConfig.ZenRule mConfigZenRule; public static final ZenMode EXAMPLE = new TestModeBuilder().build(); + public static final ZenMode MANUAL_DND = ZenMode.manualDndMode( + new AutomaticZenRule.Builder("Manual DND", Uri.parse("rule://dnd")) + .setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().disallowAllSounds().build()) + .build(), + true /* isActive */ + ); public TestModeBuilder() { // Reasonable defaults diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/inputmethod/InputMethodPreferenceTest.java b/packages/SettingsLib/tests/integ/src/com/android/settingslib/inputmethod/InputMethodPreferenceTest.java index 1e75014d2017..3906749ae494 100644 --- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/inputmethod/InputMethodPreferenceTest.java +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/inputmethod/InputMethodPreferenceTest.java @@ -24,9 +24,9 @@ import android.os.UserHandle; import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodSubtype; -import androidx.test.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; import org.junit.Test; import org.junit.runner.RunWith; @@ -109,7 +109,7 @@ public class InputMethodPreferenceTest { final boolean systemIme, final String name) { return new InputMethodPreference( - InstrumentationRegistry.getTargetContext(), + InstrumentationRegistry.getInstrumentation().getTargetContext(), createInputMethodInfo(systemIme, name), title, true /* isAllowedByOrganization */, @@ -119,7 +119,8 @@ public class InputMethodPreferenceTest { private static InputMethodInfo createInputMethodInfo( final boolean systemIme, final String name) { - final Context targetContext = InstrumentationRegistry.getTargetContext(); + final Context targetContext = + InstrumentationRegistry.getInstrumentation().getTargetContext(); final Locale systemLocale = targetContext .getResources() .getConfiguration() diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/inputmethod/InputMethodSubtypePreferenceTest.java b/packages/SettingsLib/tests/integ/src/com/android/settingslib/inputmethod/InputMethodSubtypePreferenceTest.java index f1c0beae0dc0..2c3478d6e235 100644 --- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/inputmethod/InputMethodSubtypePreferenceTest.java +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/inputmethod/InputMethodSubtypePreferenceTest.java @@ -18,9 +18,9 @@ package com.android.settingslib.inputmethod; import android.text.TextUtils; -import androidx.test.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; import org.junit.Test; import org.junit.runner.RunWith; @@ -104,7 +104,7 @@ public class InputMethodSubtypePreferenceTest { final Locale subtypeLocale = TextUtils.isEmpty(subtypeLanguageTag) ? null : Locale.forLanguageTag(subtypeLanguageTag); return new InputMethodSubtypePreference( - InstrumentationRegistry.getTargetContext(), + InstrumentationRegistry.getInstrumentation().getTargetContext(), key, subtypeName, subtypeLocale, diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index a17076b525f4..4e01a71df113 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -539,6 +539,7 @@ android_library { "androidx.preference_preference", "androidx.appcompat_appcompat", "androidx.concurrent_concurrent-futures", + "androidx.concurrent_concurrent-futures-ktx", "androidx.mediarouter_mediarouter", "androidx.palette_palette", "androidx.legacy_legacy-preference-v14", diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 8e2f7c186d8c..8ff73ad2590e 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -870,6 +870,13 @@ flag { } flag { + name: "qs_ui_refactor_compose_fragment" + namespace: "systemui" + description: "Uses a different QS fragment in NPVC that uses the new compose UI and recommended architecture. This flag depends on qs_ui_refactor flag." + bug: "325099249" +} + +flag { name: "remove_dream_overlay_hide_on_touch" namespace: "systemui" description: "Removes logic to hide the dream overlay on user interaction, as it conflicts with various transitions" @@ -981,6 +988,16 @@ flag { } flag { + name: "communal_scene_ktf_refactor" + namespace: "systemui" + description: "refactors the syncing mechanism between communal STL and KTF state." + bug: "327225415" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "app_clips_backlinks" namespace: "systemui" description: "Enables Backlinks improvement feature in App Clips" @@ -1189,6 +1206,16 @@ flag { } flag { + namespace: "systemui" + name: "remove_update_listener_in_qs_icon_view_impl" + description: "Remove update listeners in QsIconViewImpl class to avoid memory leak." + bug: "327078684" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "sim_pin_race_condition_on_restart" namespace: "systemui" description: "The SIM PIN screen may be shown incorrectly on reboot" @@ -1196,4 +1223,14 @@ flag { metadata { purpose: PURPOSE_BUGFIX } -}
\ No newline at end of file +} + +flag { + name: "sim_pin_talkback_fix_for_double_submit" + namespace: "systemui" + description: "The SIM PIN entry screens show the wrong message due" + bug: "346932439" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt index 92f03d792554..35db9e0c2bb8 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt @@ -209,7 +209,6 @@ fun CommunalContainer( backgroundType = backgroundType, colors = colors, content = content, - modifier = Modifier.horizontalNestedScrollToScene(), ) } } 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 768e6533ac7d..1c02d3f7662b 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 @@ -969,6 +969,8 @@ private fun WidgetContent( val clickActionLabel = stringResource(R.string.accessibility_action_label_select_widget) val removeWidgetActionLabel = stringResource(R.string.accessibility_action_label_remove_widget) val placeWidgetActionLabel = stringResource(R.string.accessibility_action_label_place_widget) + val unselectWidgetActionLabel = + stringResource(R.string.accessibility_action_label_unselect_widget) val selectedKey by viewModel.selectedKey.collectAsStateWithLifecycle() val selectedIndex = selectedKey?.let { key -> contentListState.list.indexOfFirst { it.key == key } } @@ -1009,18 +1011,7 @@ private fun WidgetContent( contentListState.onSaveList() true } - val selectWidgetAction = - CustomAccessibilityAction(clickActionLabel) { - val currentWidgetKey = - index?.let { - keyAtIndexIfEditable(contentListState.list, index) - } - viewModel.setSelectedKey(currentWidgetKey) - true - } - - val actions = mutableListOf(selectWidgetAction, deleteAction) - + val actions = mutableListOf(deleteAction) if (selectedIndex != null && selectedIndex != index) { actions.add( CustomAccessibilityAction(placeWidgetActionLabel) { @@ -1032,6 +1023,21 @@ private fun WidgetContent( ) } + if (!selected) { + actions.add( + CustomAccessibilityAction(clickActionLabel) { + viewModel.setSelectedKey(model.key) + true + } + ) + } else { + actions.add( + CustomAccessibilityAction(unselectWidgetActionLabel) { + viewModel.setSelectedKey(null) + true + } + ) + } customActions = actions } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenLongPress.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenLongPress.kt index c34fb38f8558..c9938552a3d5 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenLongPress.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenLongPress.kt @@ -55,7 +55,7 @@ fun LockscreenLongPress( modifier .pointerInput(isEnabled) { if (isEnabled) { - detectLongPressGesture { viewModel.onLongPress() } + detectLongPressGesture { viewModel.onLongPress(isA11yAction = false) } } } .pointerInput(Unit) { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt b/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt index d629eec1c45a..f8bd633d99a6 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt @@ -35,7 +35,7 @@ import com.android.systemui.media.controls.ui.view.MediaHost import com.android.systemui.res.R import com.android.systemui.util.animation.MeasurementInput -private object MediaCarousel { +object MediaCarousel { object Elements { internal val Content = ElementKey(debugName = "MediaCarouselContent", scenePicker = MediaScenePicker) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaScenePicker.kt b/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaScenePicker.kt index 039813390f1e..a22bc34381fd 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaScenePicker.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaScenePicker.kt @@ -25,7 +25,7 @@ import com.android.systemui.scene.shared.model.Scenes /** [ElementScenePicker] implementation for the media carousel object. */ object MediaScenePicker : ElementScenePicker { - private val shadeLockscreenFraction = 0.65f + const val SHADE_FRACTION = 0.66f private val scenes = setOf( Scenes.Lockscreen, @@ -44,7 +44,7 @@ object MediaScenePicker : ElementScenePicker { return when { // TODO: 352052894 - update with the actual scene picking transition.isTransitioning(from = Scenes.Lockscreen, to = Scenes.Shade) -> { - if (transition.progress < shadeLockscreenFraction) { + if (transition.progress < SHADE_FRACTION) { Scenes.Lockscreen } else { Scenes.Shade @@ -53,7 +53,7 @@ object MediaScenePicker : ElementScenePicker { // TODO: 345467290 - update with the actual scene picking transition.isTransitioning(from = Scenes.Shade, to = Scenes.Lockscreen) -> { - if (transition.progress < 1f - shadeLockscreenFraction) { + if (transition.progress < 1f - SHADE_FRACTION) { Scenes.Shade } else { Scenes.Lockscreen diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt index 42ec2d253421..3cf8e70d458f 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt @@ -305,8 +305,7 @@ private fun SceneScope.QuickSettingsScene( if (isCustomizerShowing) { Modifier.fillMaxHeight().align(Alignment.TopCenter) } else { - Modifier.verticalNestedScrollToScene() - .verticalScroll( + Modifier.verticalScroll( scrollState, enabled = isScrollable, ) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt index df47cbab4b90..7d46c7570dac 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt @@ -25,6 +25,8 @@ import com.android.compose.animation.scene.Edge import com.android.compose.animation.scene.TransitionBuilder import com.android.compose.animation.scene.UserActionDistance import com.android.compose.animation.scene.UserActionDistanceScope +import com.android.systemui.media.controls.ui.composable.MediaCarousel +import com.android.systemui.media.controls.ui.composable.MediaScenePicker import com.android.systemui.notifications.ui.composable.Notifications import com.android.systemui.qs.ui.composable.QuickSettings import com.android.systemui.scene.shared.model.Scenes @@ -59,10 +61,13 @@ fun TransitionBuilder.toShadeTransition( fade(QuickSettings.Elements.SplitShadeQuickSettings) fade(QuickSettings.Elements.FooterActions) } - translate( - QuickSettings.Elements.QuickQuickSettings, - y = -ShadeHeader.Dimensions.CollapsedHeight * .66f - ) + + val qsTranslation = ShadeHeader.Dimensions.CollapsedHeight * MediaScenePicker.SHADE_FRACTION + val qsExpansionDiff = + ShadeHeader.Dimensions.ExpandedHeight - ShadeHeader.Dimensions.CollapsedHeight + + translate(QuickSettings.Elements.QuickQuickSettings, y = -qsTranslation) + translate(MediaCarousel.Elements.Content, y = -(qsExpansionDiff + qsTranslation)) translate(Notifications.Elements.NotificationScrim, Edge.Top, false) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt index 805351ea8bbe..ece8b40ad332 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt @@ -514,8 +514,7 @@ private fun SceneScope.SplitShade( .sysuiResTag("expanded_qs_scroll_view") .weight(1f) .thenIf(!isCustomizerShowing) { - Modifier.verticalNestedScrollToScene() - .verticalScroll( + Modifier.verticalScroll( quickSettingsScrollState, enabled = isScrollable ) diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt index 20b1303ae6bd..78ba7defe77c 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt @@ -64,6 +64,7 @@ internal class DraggableHandlerImpl( internal val orientation: Orientation, internal val coroutineScope: CoroutineScope, ) : DraggableHandler { + internal val nestedScrollKey = Any() /** The [DraggableHandler] can only have one active [DragController] at a time. */ private var dragController: DragControllerImpl? = null @@ -912,9 +913,9 @@ private class Swipes( internal class NestedScrollHandlerImpl( private val layoutImpl: SceneTransitionLayoutImpl, private val orientation: Orientation, - private val topOrLeftBehavior: NestedScrollBehavior, - private val bottomOrRightBehavior: NestedScrollBehavior, - private val isExternalOverscrollGesture: () -> Boolean, + internal var topOrLeftBehavior: NestedScrollBehavior, + internal var bottomOrRightBehavior: NestedScrollBehavior, + internal var isExternalOverscrollGesture: () -> Boolean, private val pointersInfoOwner: PointersInfoOwner, ) { private val layoutState = layoutImpl.state diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt index 615d393f8bee..2b78b5adaa62 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt @@ -41,7 +41,6 @@ import androidx.compose.ui.node.DelegatingNode import androidx.compose.ui.node.ModifierNodeElement import androidx.compose.ui.node.ObserverModifierNode import androidx.compose.ui.node.PointerInputModifierNode -import androidx.compose.ui.node.TraversableNode import androidx.compose.ui.node.currentValueOf import androidx.compose.ui.node.findNearestAncestor import androidx.compose.ui.node.observeReads @@ -139,16 +138,12 @@ internal class MultiPointerDraggableNode( DelegatingNode(), PointerInputModifierNode, CompositionLocalConsumerModifierNode, - TraversableNode, - PointersInfoOwner, ObserverModifierNode { private val pointerInputHandler: suspend PointerInputScope.() -> Unit = { pointerInput() } private val delegate = delegate(SuspendingPointerInputModifierNode(pointerInputHandler)) private val velocityTracker = VelocityTracker() private var previousEnabled: Boolean = false - override val traverseKey: Any = TRAVERSE_KEY - var enabled: () -> Boolean = enabled set(value) { // Reset the pointer input whenever enabled changed. @@ -208,7 +203,7 @@ internal class MultiPointerDraggableNode( private var startedPosition: Offset? = null private var pointersDown: Int = 0 - override fun pointersInfo(): PointersInfo { + internal fun pointersInfo(): PointersInfo { return PointersInfo( startedPosition = startedPosition, // Note: We could have 0 pointers during fling or for other reasons. diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt index ddff2f709082..945043d8fe95 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt @@ -18,12 +18,13 @@ package com.android.compose.animation.scene import androidx.compose.foundation.gestures.Orientation import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.nestedScrollModifierNode -import androidx.compose.ui.node.DelegatableNode import androidx.compose.ui.node.DelegatingNode import androidx.compose.ui.node.ModifierNodeElement import androidx.compose.ui.platform.InspectorInfo -import com.android.compose.nestedscroll.PriorityNestedScrollConnection /** * Defines the behavior of the [SceneTransitionLayout] when a scrollable component is scrolled. @@ -67,7 +68,11 @@ enum class NestedScrollBehavior(val canStartOnPostFling: Boolean) { * In addition, during scene transitions, scroll events are consumed by the * [SceneTransitionLayout] instead of the scrollable component. */ - EdgeAlways(canStartOnPostFling = true), + EdgeAlways(canStartOnPostFling = true); + + companion object { + val Default = EdgeNoPreview + } } internal fun Modifier.nestedScrollToScene( @@ -122,37 +127,60 @@ private data class NestedScrollToSceneElement( } private class NestedScrollToSceneNode( - layoutImpl: SceneTransitionLayoutImpl, - orientation: Orientation, - topOrLeftBehavior: NestedScrollBehavior, - bottomOrRightBehavior: NestedScrollBehavior, - isExternalOverscrollGesture: () -> Boolean, + private var layoutImpl: SceneTransitionLayoutImpl, + private var orientation: Orientation, + private var topOrLeftBehavior: NestedScrollBehavior, + private var bottomOrRightBehavior: NestedScrollBehavior, + private var isExternalOverscrollGesture: () -> Boolean, ) : DelegatingNode() { - lateinit var pointersInfoOwner: PointersInfoOwner - private var priorityNestedScrollConnection: PriorityNestedScrollConnection = - scenePriorityNestedScrollConnection( - layoutImpl = layoutImpl, - orientation = orientation, - topOrLeftBehavior = topOrLeftBehavior, - bottomOrRightBehavior = bottomOrRightBehavior, - isExternalOverscrollGesture = isExternalOverscrollGesture, - pointersInfoOwner = { pointersInfoOwner.pointersInfo() } - ) - - private var nestedScrollNode: DelegatableNode = - nestedScrollModifierNode( - connection = priorityNestedScrollConnection, - dispatcher = null, - ) + private var scrollBehaviorOwner: ScrollBehaviorOwner? = null + + private fun requireScrollBehaviorOwner(): ScrollBehaviorOwner { + var behaviorOwner = scrollBehaviorOwner + if (behaviorOwner == null) { + behaviorOwner = requireScrollBehaviorOwner(layoutImpl.draggableHandler(orientation)) + scrollBehaviorOwner = behaviorOwner + } + return behaviorOwner + } - override fun onAttach() { - pointersInfoOwner = requireAncestorPointersInfoOwner() - delegate(nestedScrollNode) + private val updateScrollBehaviorsConnection = + object : NestedScrollConnection { + /** + * When using [NestedScrollConnection.onPostScroll], we can specify the desired behavior + * before our parent components. This gives them the option to override our behavior if + * they choose. + * + * The behavior can be communicated at every scroll gesture to ensure that the hierarchy + * is respected, even if one of our descendant nodes changes behavior after we set it. + */ + override fun onPostScroll( + consumed: Offset, + available: Offset, + source: NestedScrollSource, + ): Offset { + // If we have some remaining scroll, that scroll can be used to initiate a + // transition between scenes. We can assume that the behavior is only needed if + // there is some remaining amount. + if (available != Offset.Zero) { + requireScrollBehaviorOwner() + .updateScrollBehaviors( + topOrLeftBehavior = topOrLeftBehavior, + bottomOrRightBehavior = bottomOrRightBehavior, + isExternalOverscrollGesture = isExternalOverscrollGesture, + ) + } + + return Offset.Zero + } + } + + init { + delegate(nestedScrollModifierNode(updateScrollBehaviorsConnection, dispatcher = null)) } override fun onDetach() { - // Make sure we reset the scroll connection when this modifier is removed from composition - priorityNestedScrollConnection.reset() + scrollBehaviorOwner = null } fun update( @@ -162,43 +190,10 @@ private class NestedScrollToSceneNode( bottomOrRightBehavior: NestedScrollBehavior, isExternalOverscrollGesture: () -> Boolean, ) { - // Clean up the old nested scroll connection - priorityNestedScrollConnection.reset() - undelegate(nestedScrollNode) - - // Create a new nested scroll connection - priorityNestedScrollConnection = - scenePriorityNestedScrollConnection( - layoutImpl = layoutImpl, - orientation = orientation, - topOrLeftBehavior = topOrLeftBehavior, - bottomOrRightBehavior = bottomOrRightBehavior, - isExternalOverscrollGesture = isExternalOverscrollGesture, - pointersInfoOwner = pointersInfoOwner, - ) - nestedScrollNode = - nestedScrollModifierNode( - connection = priorityNestedScrollConnection, - dispatcher = null, - ) - delegate(nestedScrollNode) + this.layoutImpl = layoutImpl + this.orientation = orientation + this.topOrLeftBehavior = topOrLeftBehavior + this.bottomOrRightBehavior = bottomOrRightBehavior + this.isExternalOverscrollGesture = isExternalOverscrollGesture } } - -private fun scenePriorityNestedScrollConnection( - layoutImpl: SceneTransitionLayoutImpl, - orientation: Orientation, - topOrLeftBehavior: NestedScrollBehavior, - bottomOrRightBehavior: NestedScrollBehavior, - isExternalOverscrollGesture: () -> Boolean, - pointersInfoOwner: PointersInfoOwner, -) = - NestedScrollHandlerImpl( - layoutImpl = layoutImpl, - orientation = orientation, - topOrLeftBehavior = topOrLeftBehavior, - bottomOrRightBehavior = bottomOrRightBehavior, - isExternalOverscrollGesture = isExternalOverscrollGesture, - pointersInfoOwner = pointersInfoOwner, - ) - .connection diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt index 0c467b181cd8..82275a9ac0a6 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt @@ -207,8 +207,8 @@ interface BaseSceneScope : ElementStateScope { * @param rightBehavior when we should perform the overscroll animation at the right. */ fun Modifier.horizontalNestedScrollToScene( - leftBehavior: NestedScrollBehavior = NestedScrollBehavior.EdgeNoPreview, - rightBehavior: NestedScrollBehavior = NestedScrollBehavior.EdgeNoPreview, + leftBehavior: NestedScrollBehavior = NestedScrollBehavior.Default, + rightBehavior: NestedScrollBehavior = NestedScrollBehavior.Default, isExternalOverscrollGesture: () -> Boolean = { false }, ): Modifier @@ -220,8 +220,8 @@ interface BaseSceneScope : ElementStateScope { * @param bottomBehavior when we should perform the overscroll animation at the bottom. */ fun Modifier.verticalNestedScrollToScene( - topBehavior: NestedScrollBehavior = NestedScrollBehavior.EdgeNoPreview, - bottomBehavior: NestedScrollBehavior = NestedScrollBehavior.EdgeNoPreview, + topBehavior: NestedScrollBehavior = NestedScrollBehavior.Default, + bottomBehavior: NestedScrollBehavior = NestedScrollBehavior.Default, isExternalOverscrollGesture: () -> Boolean = { false }, ): Modifier diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt index aeb62628a8f4..b8010f25f9a4 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt @@ -20,11 +20,15 @@ import androidx.compose.foundation.gestures.Orientation import androidx.compose.runtime.Stable import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.nestedscroll.nestedScrollModifierNode import androidx.compose.ui.input.pointer.PointerEvent import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.node.DelegatableNode import androidx.compose.ui.node.DelegatingNode import androidx.compose.ui.node.ModifierNodeElement import androidx.compose.ui.node.PointerInputModifierNode +import androidx.compose.ui.node.TraversableNode +import androidx.compose.ui.node.findNearestAncestor import androidx.compose.ui.unit.IntSize /** @@ -53,7 +57,7 @@ private class SwipeToSceneNode( draggableHandler: DraggableHandlerImpl, swipeDetector: SwipeDetector, ) : DelegatingNode(), PointerInputModifierNode { - private val delegate = + private val multiPointerDraggableNode = delegate( MultiPointerDraggableNode( orientation = draggableHandler.orientation, @@ -74,21 +78,41 @@ private class SwipeToSceneNode( // Make sure to update the delegate orientation. Note that this will automatically // reset the underlying pointer input handler, so previous gestures will be // cancelled. - delegate.orientation = value.orientation + multiPointerDraggableNode.orientation = value.orientation } } + private val nestedScrollHandlerImpl = + NestedScrollHandlerImpl( + layoutImpl = draggableHandler.layoutImpl, + orientation = draggableHandler.orientation, + topOrLeftBehavior = NestedScrollBehavior.Default, + bottomOrRightBehavior = NestedScrollBehavior.Default, + isExternalOverscrollGesture = { false }, + pointersInfoOwner = { multiPointerDraggableNode.pointersInfo() }, + ) + + init { + delegate(nestedScrollModifierNode(nestedScrollHandlerImpl.connection, dispatcher = null)) + delegate(ScrollBehaviorOwnerNode(draggableHandler.nestedScrollKey, nestedScrollHandlerImpl)) + } + + override fun onDetach() { + // Make sure we reset the scroll connection when this modifier is removed from composition + nestedScrollHandlerImpl.connection.reset() + } + override fun onPointerEvent( pointerEvent: PointerEvent, pass: PointerEventPass, bounds: IntSize, - ) = delegate.onPointerEvent(pointerEvent, pass, bounds) + ) = multiPointerDraggableNode.onPointerEvent(pointerEvent, pass, bounds) - override fun onCancelPointerInput() = delegate.onCancelPointerInput() + override fun onCancelPointerInput() = multiPointerDraggableNode.onCancelPointerInput() private fun enabled(): Boolean { return draggableHandler.isDrivingTransition || - currentScene().shouldEnableSwipes(delegate.orientation) + currentScene().shouldEnableSwipes(multiPointerDraggableNode.orientation) } private fun currentScene(): Scene { @@ -118,3 +142,43 @@ private class SwipeToSceneNode( return currentScene().shouldEnableSwipes(oppositeOrientation) } } + +/** Find the [ScrollBehaviorOwner] for the current orientation. */ +internal fun DelegatableNode.requireScrollBehaviorOwner( + draggableHandler: DraggableHandlerImpl +): ScrollBehaviorOwner { + val ancestorNode = + checkNotNull(findNearestAncestor(draggableHandler.nestedScrollKey)) { + "This should never happen! Couldn't find a ScrollBehaviorOwner. " + + "Are we inside an SceneTransitionLayout?" + } + return ancestorNode as ScrollBehaviorOwner +} + +internal fun interface ScrollBehaviorOwner { + fun updateScrollBehaviors( + topOrLeftBehavior: NestedScrollBehavior, + bottomOrRightBehavior: NestedScrollBehavior, + isExternalOverscrollGesture: () -> Boolean, + ) +} + +/** + * We need a node that receives the desired behavior. + * + * TODO(b/353234530) move this logic into [SwipeToSceneNode] + */ +private class ScrollBehaviorOwnerNode( + override val traverseKey: Any, + val nestedScrollHandlerImpl: NestedScrollHandlerImpl +) : Modifier.Node(), TraversableNode, ScrollBehaviorOwner { + override fun updateScrollBehaviors( + topOrLeftBehavior: NestedScrollBehavior, + bottomOrRightBehavior: NestedScrollBehavior, + isExternalOverscrollGesture: () -> Boolean + ) { + nestedScrollHandlerImpl.topOrLeftBehavior = topOrLeftBehavior + nestedScrollHandlerImpl.bottomOrRightBehavior = bottomOrRightBehavior + nestedScrollHandlerImpl.isExternalOverscrollGesture = isExternalOverscrollGesture + } +} diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt index 7988e0e4e416..c91151e41605 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt @@ -797,8 +797,6 @@ class ElementTest { scene(SceneB, userActions = mapOf(Swipe.Up to SceneA)) { Box( Modifier - // Unconsumed scroll gesture will be intercepted by STL - .verticalNestedScrollToScene() // A scrollable that does not consume the scroll gesture .scrollable( rememberScrollableState(consumeScrollDelta = { 0f }), @@ -875,8 +873,6 @@ class ElementTest { ) { Box( Modifier - // Unconsumed scroll gesture will be intercepted by STL - .verticalNestedScrollToScene() // A scrollable that does not consume the scroll gesture .scrollable( rememberScrollableState(consumeScrollDelta = { 0f }), diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/NestedScrollToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/NestedScrollToSceneTest.kt new file mode 100644 index 000000000000..311a58018840 --- /dev/null +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/NestedScrollToSceneTest.kt @@ -0,0 +1,269 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.compose.animation.scene + +import androidx.compose.foundation.gestures.Orientation.Vertical +import androidx.compose.foundation.gestures.rememberScrollableState +import androidx.compose.foundation.gestures.scrollable +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.platform.LocalViewConfiguration +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onRoot +import androidx.compose.ui.test.performTouchInput +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.compose.animation.scene.TestScenes.SceneA +import com.android.compose.animation.scene.TestScenes.SceneB +import com.android.compose.animation.scene.subjects.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class NestedScrollToSceneTest { + @get:Rule val rule = createComposeRule() + + private var touchSlop = 0f + private val layoutWidth: Dp = 200.dp + private val layoutHeight = 400.dp + + private fun setup2ScenesAndScrollTouchSlop( + modifierSceneA: @Composable SceneScope.() -> Modifier = { Modifier }, + ): MutableSceneTransitionLayoutState { + val state = + rule.runOnUiThread { + MutableSceneTransitionLayoutState(SceneA, transitions = EmptyTestTransitions) + } + + rule.setContent { + touchSlop = LocalViewConfiguration.current.touchSlop + SceneTransitionLayout( + state = state, + modifier = Modifier.size(layoutWidth, layoutHeight) + ) { + scene(SceneA, userActions = mapOf(Swipe.Up to SceneB)) { + Spacer(modifierSceneA().fillMaxSize()) + } + scene(SceneB, userActions = mapOf(Swipe.Down to SceneA)) { + Spacer(Modifier.fillMaxSize()) + } + } + } + + pointerDownAndScrollTouchSlop() + + assertThat(state.transitionState).isIdle() + + return state + } + + private fun pointerDownAndScrollTouchSlop() { + rule.onRoot().performTouchInput { + val middleTop = Offset((layoutWidth / 2).toPx(), 0f) + down(middleTop) + // Scroll touchSlop + moveBy(Offset(0f, touchSlop), delayMillis = 1_000) + } + } + + private fun scrollDown(percent: Float = 1f) { + rule.onRoot().performTouchInput { + moveBy(Offset(0f, layoutHeight.toPx() * percent), delayMillis = 1_000) + } + } + + private fun scrollUp(percent: Float = 1f) = scrollDown(-percent) + + private fun pointerUp() { + rule.onRoot().performTouchInput { up() } + } + + @Test + fun scrollableElementsInSTL_shouldHavePriority() { + val state = setup2ScenesAndScrollTouchSlop { + Modifier + // A scrollable that consumes the scroll gesture + .scrollable(rememberScrollableState { it }, Vertical) + } + + scrollUp(percent = 0.5f) + + // Consumed by the scrollable element + assertThat(state.transitionState).isIdle() + } + + @Test + fun unconsumedScrollEvents_canBeConsumedBySTLByDefault() { + val state = setup2ScenesAndScrollTouchSlop { + Modifier + // A scrollable that does not consume the scroll gesture + .scrollable(rememberScrollableState { 0f }, Vertical) + } + + scrollUp(percent = 0.5f) + // STL will start a transition with the remaining scroll + val transition = assertThat(state.transitionState).isTransition() + assertThat(transition).hasProgress(0.5f) + + scrollUp(percent = 1f) + assertThat(transition).hasProgress(1.5f) + } + + @Test + fun customizeStlNestedScrollBehavior_DuringTransitionBetweenScenes() { + var canScroll = true + val state = setup2ScenesAndScrollTouchSlop { + Modifier.verticalNestedScrollToScene( + bottomBehavior = NestedScrollBehavior.DuringTransitionBetweenScenes + ) + .scrollable(rememberScrollableState { if (canScroll) it else 0f }, Vertical) + } + + scrollUp(percent = 0.5f) + assertThat(state.transitionState).isIdle() + + // Reach the end of the scrollable element + canScroll = false + scrollUp(percent = 0.5f) + assertThat(state.transitionState).isIdle() + + pointerUp() + assertThat(state.transitionState).isIdle() + + // Start a new gesture + pointerDownAndScrollTouchSlop() + scrollUp(percent = 0.5f) + assertThat(state.transitionState).isIdle() + } + + @Test + fun customizeStlNestedScrollBehavior_EdgeNoPreview() { + var canScroll = true + val state = setup2ScenesAndScrollTouchSlop { + Modifier.verticalNestedScrollToScene( + bottomBehavior = NestedScrollBehavior.EdgeNoPreview + ) + .scrollable(rememberScrollableState { if (canScroll) it else 0f }, Vertical) + } + + scrollUp(percent = 0.5f) + assertThat(state.transitionState).isIdle() + + // Reach the end of the scrollable element + canScroll = false + scrollUp(percent = 0.5f) + assertThat(state.transitionState).isIdle() + + pointerUp() + assertThat(state.transitionState).isIdle() + + // Start a new gesture + pointerDownAndScrollTouchSlop() + scrollUp(percent = 0.5f) + val transition = assertThat(state.transitionState).isTransition() + assertThat(transition).hasProgress(0.5f) + + pointerUp() + rule.waitForIdle() + assertThat(state.transitionState).isIdle() + assertThat(state.transitionState).hasCurrentScene(SceneB) + } + + @Test + fun customizeStlNestedScrollBehavior_EdgeWithPreview() { + var canScroll = true + val state = setup2ScenesAndScrollTouchSlop { + Modifier.verticalNestedScrollToScene( + bottomBehavior = NestedScrollBehavior.EdgeWithPreview + ) + .scrollable(rememberScrollableState { if (canScroll) it else 0f }, Vertical) + } + + scrollUp(percent = 0.5f) + assertThat(state.transitionState).isIdle() + + // Reach the end of the scrollable element + canScroll = false + scrollUp(percent = 0.5f) + val transition1 = assertThat(state.transitionState).isTransition() + assertThat(transition1).hasProgress(0.5f) + + pointerUp() + rule.waitForIdle() + assertThat(state.transitionState).isIdle() + assertThat(state.transitionState).hasCurrentScene(SceneA) + + // Start a new gesture + pointerDownAndScrollTouchSlop() + scrollUp(percent = 0.5f) + val transition2 = assertThat(state.transitionState).isTransition() + assertThat(transition2).hasProgress(0.5f) + + pointerUp() + rule.waitForIdle() + assertThat(state.transitionState).isIdle() + assertThat(state.transitionState).hasCurrentScene(SceneB) + } + + @Test + fun customizeStlNestedScrollBehavior_EdgeAlways() { + var canScroll = true + val state = setup2ScenesAndScrollTouchSlop { + Modifier.verticalNestedScrollToScene(bottomBehavior = NestedScrollBehavior.EdgeAlways) + .scrollable(rememberScrollableState { if (canScroll) it else 0f }, Vertical) + } + + scrollUp(percent = 0.5f) + assertThat(state.transitionState).isIdle() + + // Reach the end of the scrollable element + canScroll = false + scrollUp(percent = 0.5f) + val transition = assertThat(state.transitionState).isTransition() + assertThat(transition).hasProgress(0.5f) + + pointerUp() + rule.waitForIdle() + assertThat(state.transitionState).isIdle() + assertThat(state.transitionState).hasCurrentScene(SceneB) + } + + @Test + fun customizeStlNestedScrollBehavior_multipleRequests() { + val state = setup2ScenesAndScrollTouchSlop { + Modifier + // This verticalNestedScrollToScene is closer the STL (an ancestor node) + .verticalNestedScrollToScene(bottomBehavior = NestedScrollBehavior.EdgeAlways) + // Another verticalNestedScrollToScene modifier + .verticalNestedScrollToScene( + bottomBehavior = NestedScrollBehavior.DuringTransitionBetweenScenes + ) + .scrollable(rememberScrollableState { 0f }, Vertical) + } + + scrollUp(percent = 0.5f) + // EdgeAlways always consume the remaining scroll, DuringTransitionBetweenScenes does not. + val transition = assertThat(state.transitionState).isTransition() + assertThat(transition).hasProgress(0.5f) + } +} 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 5cf4f16aed62..7fd9ce20ab92 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 @@ -146,6 +146,14 @@ class ShadeTouchHandlerTest : SysuiTestCase() { verify(mShadeViewController, never()).handleExternalTouch(any()) } + @Test + fun testCancelMotionEvent_popsTouchSession() { + swipe(Direction.DOWN) + val event = MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0) + mInputListenerCaptor.lastValue.onInputEvent(event) + verify(mTouchSession).pop() + } + /** * Simulates a swipe in the given direction and returns true if the touch was intercepted by the * touch handler's gesture listener. diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryImplTest.kt index a8bdc7c632d2..1f5e30ca4a0d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryImplTest.kt @@ -88,24 +88,6 @@ class CommunalPrefsRepositoryImplTest : SysuiTestCase() { } @Test - fun isDisclaimerDismissed_byDefault_isFalse() = - testScope.runTest { - val isDisclaimerDismissed by - collectLastValue(underTest.isDisclaimerDismissed(MAIN_USER)) - assertThat(isDisclaimerDismissed).isFalse() - } - - @Test - fun isDisclaimerDismissed_onSet_isTrue() = - testScope.runTest { - val isDisclaimerDismissed by - collectLastValue(underTest.isDisclaimerDismissed(MAIN_USER)) - - underTest.setDisclaimerDismissed(MAIN_USER) - assertThat(isDisclaimerDismissed).isTrue() - } - - @Test fun getSharedPreferences_whenFileRestored() = testScope.runTest { val isCtaDismissed by collectLastValue(underTest.isCtaDismissed(MAIN_USER)) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt index 5cdbe9ce5856..9539c0492056 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt @@ -84,6 +84,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 @@ -1059,6 +1060,25 @@ class CommunalInteractorTest : SysuiTestCase() { ) } + @Test + fun dismissDisclaimerSetsDismissedFlag() = + testScope.runTest { + val disclaimerDismissed by collectLastValue(underTest.isDisclaimerDismissed) + assertThat(disclaimerDismissed).isFalse() + underTest.setDisclaimerDismissed() + assertThat(disclaimerDismissed).isTrue() + } + + @Test + fun dismissDisclaimerTimeoutResetsDismissedFlag() = + testScope.runTest { + val disclaimerDismissed by collectLastValue(underTest.isDisclaimerDismissed) + underTest.setDisclaimerDismissed() + assertThat(disclaimerDismissed).isTrue() + advanceTimeBy(CommunalInteractor.DISCLAIMER_RESET_MILLIS) + assertThat(disclaimerDismissed).isFalse() + } + private fun setKeyguardFeaturesDisabled(user: UserInfo, disabledFlags: Int) { whenever(kosmos.devicePolicyManager.getKeyguardDisabledFeatures(nullable(), eq(user.id))) .thenReturn(disabledFlags) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalPrefsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalPrefsInteractorTest.kt index 7b79d2817478..9a92f76f90c6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalPrefsInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalPrefsInteractorTest.kt @@ -74,40 +74,6 @@ class CommunalPrefsInteractorTest : SysuiTestCase() { assertThat(isCtaDismissed).isFalse() } - @Test - fun setDisclaimerDismissed_currentUser() = - testScope.runTest { - setSelectedUser(MAIN_USER) - val isDisclaimerDismissed by collectLastValue(underTest.isDisclaimerDismissed) - - assertThat(isDisclaimerDismissed).isFalse() - underTest.setDisclaimerDismissed(MAIN_USER) - assertThat(isDisclaimerDismissed).isTrue() - } - - @Test - fun setDisclaimerDismissed_anotherUser() = - testScope.runTest { - setSelectedUser(MAIN_USER) - val isDisclaimerDismissed by collectLastValue(underTest.isDisclaimerDismissed) - - assertThat(isDisclaimerDismissed).isFalse() - underTest.setDisclaimerDismissed(SECONDARY_USER) - assertThat(isDisclaimerDismissed).isFalse() - } - - @Test - fun isDisclaimerDismissed_userSwitch() = - testScope.runTest { - setSelectedUser(MAIN_USER) - underTest.setDisclaimerDismissed(MAIN_USER) - val isDisclaimerDismissed by collectLastValue(underTest.isDisclaimerDismissed) - - assertThat(isDisclaimerDismissed).isTrue() - setSelectedUser(SECONDARY_USER) - assertThat(isDisclaimerDismissed).isFalse() - } - private suspend fun setSelectedUser(user: UserInfo) { with(kosmos.fakeUserRepository) { setUserInfos(listOf(user)) 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 new file mode 100644 index 000000000000..f7f70c154ce6 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorTest.kt @@ -0,0 +1,685 @@ +/* + * 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.domain.interactor + +import android.platform.test.annotations.EnableFlags +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.compose.animation.scene.ObservableTransitionState +import com.android.compose.animation.scene.ObservableTransitionState.Idle +import com.android.systemui.Flags.FLAG_COMMUNAL_HUB +import com.android.systemui.Flags.FLAG_COMMUNAL_SCENE_KTF_REFACTOR +import com.android.systemui.SysuiTestCase +import com.android.systemui.communal.data.repository.communalSceneRepository +import com.android.systemui.communal.shared.model.CommunalScenes +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.coroutines.collectValues +import com.android.systemui.flags.DisableSceneContainer +import com.android.systemui.flags.Flags +import com.android.systemui.flags.fakeFeatureFlagsClassic +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository +import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository +import com.android.systemui.keyguard.data.repository.realKeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING +import com.android.systemui.keyguard.shared.model.KeyguardState.GLANCEABLE_HUB +import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN +import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED +import com.android.systemui.keyguard.shared.model.TransitionInfo +import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled +import com.android.systemui.keyguard.shared.model.TransitionState.CANCELED +import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED +import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING +import com.android.systemui.keyguard.shared.model.TransitionState.STARTED +import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +@EnableFlags(FLAG_COMMUNAL_HUB, FLAG_COMMUNAL_SCENE_KTF_REFACTOR) +@DisableSceneContainer +class CommunalSceneTransitionInteractorTest : SysuiTestCase() { + + private val kosmos = + testKosmos().apply { keyguardTransitionRepository = realKeyguardTransitionRepository } + private val testScope = kosmos.testScope + + private val underTest by lazy { kosmos.communalSceneTransitionInteractor } + private val keyguardTransitionRepository by lazy { kosmos.realKeyguardTransitionRepository } + + private val ownerName = CommunalSceneTransitionInteractor::class.java.simpleName + private val progress = MutableSharedFlow<Float>() + + private val sceneTransitions = + MutableStateFlow<ObservableTransitionState>(Idle(CommunalScenes.Blank)) + + private val blankToHub = + ObservableTransitionState.Transition( + fromScene = CommunalScenes.Blank, + toScene = CommunalScenes.Communal, + currentScene = flowOf(CommunalScenes.Blank), + progress = progress, + isInitiatedByUserInput = false, + isUserInputOngoing = flowOf(false), + ) + + private val hubToBlank = + ObservableTransitionState.Transition( + fromScene = CommunalScenes.Communal, + toScene = CommunalScenes.Blank, + currentScene = flowOf(CommunalScenes.Communal), + progress = progress, + isInitiatedByUserInput = false, + isUserInputOngoing = flowOf(false), + ) + + @Before + fun setup() { + kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true) + underTest.start() + kosmos.communalSceneRepository.setTransitionState(sceneTransitions) + testScope.launch { keyguardTransitionRepository.emitInitialStepsFromOff(LOCKSCREEN) } + } + + /** Transition from blank to glanceable hub. This is the default case. */ + @Test + fun transition_from_blank_end_in_hub() = + testScope.runTest { + sceneTransitions.value = blankToHub + + val currentStep by collectLastValue(keyguardTransitionRepository.transitions) + + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = LOCKSCREEN, + to = GLANCEABLE_HUB, + transitionState = STARTED, + value = 0f, + ownerName = ownerName, + ) + ) + + progress.emit(0.4f) + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = LOCKSCREEN, + to = GLANCEABLE_HUB, + transitionState = RUNNING, + value = 0.4f, + ownerName = ownerName, + ) + ) + + progress.emit(1f) + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = LOCKSCREEN, + to = GLANCEABLE_HUB, + transitionState = RUNNING, + value = 1f, + ownerName = ownerName, + ) + ) + + sceneTransitions.value = Idle(CommunalScenes.Communal) + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = LOCKSCREEN, + to = GLANCEABLE_HUB, + transitionState = FINISHED, + value = 1f, + ownerName = ownerName, + ) + ) + } + + /** Transition from hub to lockscreen. */ + @Test + fun transition_from_hub_end_in_lockscreen() = + testScope.runTest { + sceneTransitions.value = hubToBlank + + val currentStep by collectLastValue(keyguardTransitionRepository.transitions) + + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = GLANCEABLE_HUB, + to = LOCKSCREEN, + transitionState = STARTED, + value = 0f, + ownerName = ownerName, + ) + ) + + progress.emit(0.4f) + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = GLANCEABLE_HUB, + to = LOCKSCREEN, + transitionState = RUNNING, + value = 0.4f, + ownerName = ownerName, + ) + ) + + sceneTransitions.value = Idle(CommunalScenes.Blank) + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = GLANCEABLE_HUB, + to = LOCKSCREEN, + transitionState = FINISHED, + value = 1f, + ownerName = ownerName, + ) + ) + } + + /** Transition from hub to dream. */ + @Test + fun transition_from_hub_end_in_dream() = + testScope.runTest { + kosmos.fakeKeyguardRepository.setDreaming(true) + runCurrent() + + sceneTransitions.value = hubToBlank + + val currentStep by collectLastValue(keyguardTransitionRepository.transitions) + + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = GLANCEABLE_HUB, + to = DREAMING, + transitionState = STARTED, + value = 0f, + ownerName = ownerName, + ) + ) + + progress.emit(0.4f) + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = GLANCEABLE_HUB, + to = DREAMING, + transitionState = RUNNING, + value = 0.4f, + ownerName = ownerName, + ) + ) + + sceneTransitions.value = Idle(CommunalScenes.Blank) + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = GLANCEABLE_HUB, + to = DREAMING, + transitionState = FINISHED, + value = 1f, + ownerName = ownerName, + ) + ) + } + + /** Transition from blank to hub, then settle back in blank. */ + @Test + fun transition_from_blank_end_in_blank() = + testScope.runTest { + sceneTransitions.value = blankToHub + + val currentStep by collectLastValue(keyguardTransitionRepository.transitions) + val allSteps by collectValues(keyguardTransitionRepository.transitions) + + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = LOCKSCREEN, + to = GLANCEABLE_HUB, + transitionState = STARTED, + value = 0f, + ownerName = ownerName, + ) + ) + + progress.emit(0.4f) + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = LOCKSCREEN, + to = GLANCEABLE_HUB, + transitionState = RUNNING, + value = 0.4f, + ownerName = ownerName, + ) + ) + + val numToDrop = allSteps.size + // Settle back in blank + sceneTransitions.value = Idle(CommunalScenes.Blank) + + // Assert that KTF reversed transition back to lockscreen. + assertThat(allSteps.drop(numToDrop)) + .containsExactly( + TransitionStep( + from = LOCKSCREEN, + to = GLANCEABLE_HUB, + transitionState = CANCELED, + value = 0.4f, + ownerName = ownerName, + ), + // Transition back to lockscreen + TransitionStep( + from = GLANCEABLE_HUB, + to = LOCKSCREEN, + transitionState = STARTED, + value = 0.6f, + ownerName = ownerName, + ), + TransitionStep( + from = GLANCEABLE_HUB, + to = LOCKSCREEN, + transitionState = FINISHED, + value = 1f, + ownerName = ownerName, + ), + ) + .inOrder() + } + + @Test + fun transition_to_occluded_with_changed_scene_respected_just_once() = + testScope.runTest { + underTest.onSceneAboutToChange(CommunalScenes.Blank, OCCLUDED) + runCurrent() + sceneTransitions.value = hubToBlank + + val currentStep by collectLastValue(keyguardTransitionRepository.transitions) + + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = GLANCEABLE_HUB, + to = OCCLUDED, + transitionState = STARTED, + value = 0f, + ownerName = ownerName, + ) + ) + + sceneTransitions.value = blankToHub + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = OCCLUDED, + to = GLANCEABLE_HUB, + transitionState = STARTED, + value = 0f, + ownerName = ownerName, + ) + ) + + sceneTransitions.value = hubToBlank + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = GLANCEABLE_HUB, + to = LOCKSCREEN, + transitionState = STARTED, + value = 0f, + ownerName = ownerName, + ) + ) + } + + @Test + fun transition_from_blank_interrupted() = + testScope.runTest { + sceneTransitions.value = blankToHub + + val currentStep by collectLastValue(keyguardTransitionRepository.transitions) + val allSteps by collectValues(keyguardTransitionRepository.transitions) + + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = LOCKSCREEN, + to = GLANCEABLE_HUB, + transitionState = STARTED, + value = 0f, + ownerName = ownerName, + ) + ) + + progress.emit(0.4f) + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = LOCKSCREEN, + to = GLANCEABLE_HUB, + transitionState = RUNNING, + value = 0.4f, + ownerName = ownerName, + ) + ) + + val numToDrop = allSteps.size + // Transition back from hub to blank, interrupting + // the current transition. + sceneTransitions.value = hubToBlank + + assertThat(allSteps.drop(numToDrop)) + .containsExactly( + TransitionStep( + from = LOCKSCREEN, + to = GLANCEABLE_HUB, + value = 1f, + transitionState = FINISHED, + ownerName = ownerName, + ), + TransitionStep( + from = GLANCEABLE_HUB, + to = LOCKSCREEN, + value = 0f, + transitionState = STARTED, + ownerName = ownerName, + ), + ) + .inOrder() + + progress.emit(0.1f) + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = GLANCEABLE_HUB, + to = LOCKSCREEN, + transitionState = RUNNING, + value = 0.1f, + ownerName = ownerName, + ) + ) + } + + /** + * Blank -> Hub transition interrupted by a new Blank -> Hub transition. KTF state should not be + * updated in this case. + */ + @Test + fun transition_to_hub_duplicate_does_not_change_ktf() = + testScope.runTest { + sceneTransitions.value = + ObservableTransitionState.Transition( + fromScene = CommunalScenes.Blank, + toScene = CommunalScenes.Communal, + currentScene = flowOf(CommunalScenes.Blank), + progress = progress, + isInitiatedByUserInput = false, + isUserInputOngoing = flowOf(false), + ) + + val currentStep by collectLastValue(keyguardTransitionRepository.transitions) + val allSteps by collectValues(keyguardTransitionRepository.transitions) + + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = LOCKSCREEN, + to = GLANCEABLE_HUB, + transitionState = STARTED, + value = 0f, + ownerName = ownerName, + ) + ) + + progress.emit(0.4f) + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = LOCKSCREEN, + to = GLANCEABLE_HUB, + transitionState = RUNNING, + value = 0.4f, + ownerName = ownerName, + ) + ) + + val sizeBefore = allSteps.size + val newProgress = MutableSharedFlow<Float>() + sceneTransitions.value = + ObservableTransitionState.Transition( + fromScene = CommunalScenes.Blank, + toScene = CommunalScenes.Communal, + currentScene = flowOf(CommunalScenes.Blank), + progress = newProgress, + isInitiatedByUserInput = true, + isUserInputOngoing = flowOf(true), + ) + + // No new KTF steps emitted as a result of the new transition. + assertThat(allSteps).hasSize(sizeBefore) + + // Progress is now tracked by the new flow. + newProgress.emit(0.1f) + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = LOCKSCREEN, + to = GLANCEABLE_HUB, + transitionState = RUNNING, + value = 0.1f, + ownerName = ownerName, + ) + ) + } + + /** + * STL: Hub -> Blank, then interrupt in KTF LS -> OCCLUDED, then STL still finishes in Blank. + * After a KTF transition is started (GLANCEABLE_HUB -> LOCKSCREEN) KTF immediately considers + * the active scene to be LOCKSCREEN. This means that all listeners for LOCKSCREEN are active + * and may start a new transition LOCKSCREEN -> *. Here we test LOCKSCREEN -> OCCLUDED. + * + * KTF is allowed to already start and play the other transition, while the STL transition may + * finish later (gesture completes much later). When we eventually settle the STL transition in + * Blank we do not want to force KTF back to its original destination (LOCKSCREEN). Instead, for + * this scenario the settle can be ignored. + */ + @Test + fun transition_to_blank_interrupted_by_ktf_transition_then_finish_in_blank() = + testScope.runTest { + sceneTransitions.value = hubToBlank + + val currentStep by collectLastValue(keyguardTransitionRepository.transitions) + + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = GLANCEABLE_HUB, + to = LOCKSCREEN, + transitionState = STARTED, + value = 0f, + ownerName = ownerName, + ) + ) + + progress.emit(0.4f) + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = GLANCEABLE_HUB, + to = LOCKSCREEN, + transitionState = RUNNING, + value = 0.4f, + ownerName = ownerName, + ) + ) + + // Start another transition externally while our scene + // transition is happening. + keyguardTransitionRepository.startTransition( + TransitionInfo( + ownerName = "external", + from = LOCKSCREEN, + to = OCCLUDED, + animator = null, + modeOnCanceled = TransitionModeOnCanceled.RESET + ) + ) + + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = LOCKSCREEN, + to = OCCLUDED, + transitionState = STARTED, + value = 0f, + ownerName = "external", + ) + ) + + // Scene progress should not affect KTF transition anymore + progress.emit(0.7f) + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = LOCKSCREEN, + to = OCCLUDED, + transitionState = STARTED, + value = 0f, + ownerName = "external", + ) + ) + + // Scene transition still finishes but should not impact KTF transition + sceneTransitions.value = Idle(CommunalScenes.Blank) + + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = LOCKSCREEN, + to = OCCLUDED, + transitionState = STARTED, + value = 0f, + ownerName = "external", + ) + ) + } + + /** + * STL: Hub -> Blank, then interrupt in KTF LS -> OCCLUDED, then STL finishes back in Hub. + * + * This is similar to the previous scenario but the gesture may have been interrupted by any + * other transition. KTF needs to immediately finish in GLANCEABLE_HUB (there is a jump cut). + */ + @Test + fun transition_to_blank_interrupted_by_ktf_transition_then_finish_in_hub() = + testScope.runTest { + sceneTransitions.value = hubToBlank + + val currentStep by collectLastValue(keyguardTransitionRepository.transitions) + + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = GLANCEABLE_HUB, + to = LOCKSCREEN, + transitionState = STARTED, + value = 0f, + ownerName = ownerName, + ) + ) + + progress.emit(0.4f) + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = GLANCEABLE_HUB, + to = LOCKSCREEN, + transitionState = RUNNING, + value = 0.4f, + ownerName = ownerName, + ) + ) + + // Start another transition externally while our scene + // transition is happening. + keyguardTransitionRepository.startTransition( + TransitionInfo( + ownerName = "external", + from = LOCKSCREEN, + to = OCCLUDED, + animator = null, + modeOnCanceled = TransitionModeOnCanceled.RESET + ) + ) + + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = LOCKSCREEN, + to = OCCLUDED, + transitionState = STARTED, + value = 0f, + ownerName = "external", + ) + ) + + // Scene progress should not affect KTF transition anymore + progress.emit(0.7f) + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = LOCKSCREEN, + to = OCCLUDED, + transitionState = STARTED, + value = 0f, + ownerName = "external", + ) + ) + + // We land back in communal. + sceneTransitions.value = Idle(CommunalScenes.Communal) + + assertThat(currentStep) + .isEqualTo( + TransitionStep( + from = OCCLUDED, + to = GLANCEABLE_HUB, + transitionState = FINISHED, + value = 1f, + ownerName = ownerName, + ) + ) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt index b138fb3b779a..f8906adc33d4 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt @@ -65,6 +65,7 @@ import com.android.systemui.user.data.repository.fakeUserRepository import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runTest import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue @@ -352,6 +353,21 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { } @Test + fun showDisclaimer_trueWhenTimeout() = + testScope.runTest { + underTest.setEditModeState(EditModeState.SHOWING) + kosmos.fakeUserRepository.setSelectedUserInfo(MAIN_USER_INFO) + + val showDisclaimer by collectLastValue(underTest.showDisclaimer) + + assertThat(showDisclaimer).isTrue() + underTest.onDisclaimerDismissed() + assertThat(showDisclaimer).isFalse() + advanceTimeBy(CommunalInteractor.DISCLAIMER_RESET_MILLIS) + assertThat(showDisclaimer).isTrue() + } + + @Test fun scrollPosition_persistedOnEditCleanup() { val index = 2 val offset = 30 diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt index 73ef77540398..88ba0411b414 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt @@ -84,7 +84,7 @@ class DreamOverlayAnimationsControllerTest : SysuiTestCase() { verify(mockAnimator, atLeastOnce()).addListener(captor.capture()) captor.allValues.forEach { it.onAnimationEnd(mockAnimator) } - verify(stateController).setExitAnimationsRunning(false) + verify(stateController, times(2)).setExitAnimationsRunning(false) } @Test @@ -154,4 +154,10 @@ class DreamOverlayAnimationsControllerTest : SysuiTestCase() { } ) } + + @Test + fun testCancelAnimations_clearsExitAnimationsRunning() { + controller.cancelAnimations() + verify(stateController).setExitAnimationsRunning(false) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadStatsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadStatsInteractorTest.kt new file mode 100644 index 000000000000..ee51e37faba7 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadStatsInteractorTest.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.education.domain.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.education.data.repository.contextualEducationRepository +import com.android.systemui.education.data.repository.fakeEduClock +import com.android.systemui.kosmos.testScope +import com.android.systemui.shared.education.GestureType.BACK_GESTURE +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class KeyboardTouchpadStatsInteractorTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val underTest = kosmos.keyboardTouchpadEduStatsInteractor + + @Test + fun dataUpdatedOnIncrementSignalCount() = + testScope.runTest { + val model by + collectLastValue( + kosmos.contextualEducationRepository.readGestureEduModelFlow(BACK_GESTURE) + ) + val originalValue = model!!.signalCount + underTest.incrementSignalCount(BACK_GESTURE) + assertThat(model?.signalCount).isEqualTo(originalValue + 1) + } + + @Test + fun dataAddedOnUpdateShortcutTriggerTime() = + testScope.runTest { + val model by + collectLastValue( + kosmos.contextualEducationRepository.readGestureEduModelFlow(BACK_GESTURE) + ) + assertThat(model?.lastShortcutTriggeredTime).isNull() + underTest.updateShortcutTriggerTime(BACK_GESTURE) + assertThat(model?.lastShortcutTriggeredTime).isEqualTo(kosmos.fakeEduClock.instant()) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt index 96b4b4325408..2b2c121fb79a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt @@ -171,14 +171,15 @@ class KeyguardTouchHandlingInteractorTest : SysuiTestCase() { } @Test - fun longPressed_openWppDirectlyEnabled_doesNotShowMenu_opensSettings() = + fun longPressed_isA11yAction_doesNotShowMenu_opensSettings() = testScope.runTest { - createUnderTest(isOpenWppDirectlyEnabled = true) + createUnderTest() val isMenuVisible by collectLastValue(underTest.isMenuVisible) val shouldOpenSettings by collectLastValue(underTest.shouldOpenSettings) + val isA11yAction = true runCurrent() - underTest.onLongPress() + underTest.onLongPress(isA11yAction) assertThat(isMenuVisible).isFalse() assertThat(shouldOpenSettings).isTrue() @@ -284,7 +285,6 @@ class KeyguardTouchHandlingInteractorTest : SysuiTestCase() { private suspend fun createUnderTest( isLongPressFeatureEnabled: Boolean = true, isRevampedWppFeatureEnabled: Boolean = true, - isOpenWppDirectlyEnabled: Boolean = false, ) { // This needs to be re-created for each test outside of kosmos since the flag values are // read during initialization to set up flows. Maybe there is a better way to handle that. @@ -298,7 +298,6 @@ class KeyguardTouchHandlingInteractorTest : SysuiTestCase() { featureFlags = kosmos.fakeFeatureFlagsClassic.apply { set(Flags.LOCK_SCREEN_LONG_PRESS_ENABLED, isLongPressFeatureEnabled) - set(Flags.LOCK_SCREEN_LONG_PRESS_DIRECT_TO_WPP, isOpenWppDirectlyEnabled) }, broadcastDispatcher = fakeBroadcastDispatcher, accessibilityManager = kosmos.accessibilityManagerWrapper, diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt index fc3b35d637f7..a3959d2109c8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt @@ -1574,30 +1574,6 @@ class KeyguardTransitionScenariosTest(flags: FlagsParameterization?) : SysuiTest } @Test - @BrokenWithSceneContainer(339465026) - fun aodToOccluded() = - testScope.runTest { - // GIVEN a prior transition has run to AOD - runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.AOD) - runCurrent() - - // WHEN the keyguard is occluded - keyguardRepository.setKeyguardOccluded(true) - runCurrent() - - // THEN a transition to OCCLUDED should occur - assertThat(transitionRepository) - .startedTransition( - ownerName = "FromAodTransitionInteractor(isOccluded = true)", - from = KeyguardState.AOD, - to = KeyguardState.OCCLUDED, - animatorAssertion = { it.isNotNull() }, - ) - - coroutineContext.cancelChildren() - } - - @Test @DisableSceneContainer fun aodToPrimaryBouncer() = testScope.runTest { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModelTest.kt index be0d899fd946..9e696011e285 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModelTest.kt @@ -18,8 +18,11 @@ package com.android.systemui.keyguard.ui.viewmodel +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.Flags as AConfigFlags import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue @@ -74,9 +77,9 @@ class AodAlphaViewModelTest : SysuiTestCase() { @Test @DisableSceneContainer + @DisableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) fun alpha_WhenNotGone_clockMigrationFlagIsOff_emitsKeyguardAlpha() = testScope.runTest { - mSetFlagsRule.disableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) val alpha by collectLastValue(underTest.alpha) keyguardTransitionRepository.sendTransitionSteps( @@ -186,9 +189,9 @@ class AodAlphaViewModelTest : SysuiTestCase() { @Test @DisableSceneContainer + @EnableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) fun alpha_whenGone_equalsZero() = testScope.runTest { - mSetFlagsRule.enableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) val alpha by collectLastValue(underTest.alpha) keyguardTransitionRepository.sendTransitionStep( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt index 63d06a409522..41c5b7332a4f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt @@ -18,6 +18,8 @@ package com.android.systemui.keyguard.ui.viewmodel +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 as AConfigFlags @@ -69,10 +71,11 @@ class AodBurnInViewModelTest : SysuiTestCase() { private val burnInFlow = MutableStateFlow(BurnInModel()) @Before + @DisableFlags( + AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT, + AConfigFlags.FLAG_COMPOSE_LOCKSCREEN + ) fun setUp() { - mSetFlagsRule.disableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) - mSetFlagsRule.disableFlags(AConfigFlags.FLAG_COMPOSE_LOCKSCREEN) - MockitoAnnotations.initMocks(this) whenever(burnInInteractor.burnIn(anyInt(), anyInt())).thenReturn(burnInFlow) kosmos.burnInInteractor = burnInInteractor @@ -174,10 +177,9 @@ class AodBurnInViewModelTest : SysuiTestCase() { } @Test + @DisableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) fun translationAndScale_whenFullyDozing_MigrationFlagOff_staysOutOfTopInset() = testScope.runTest { - mSetFlagsRule.disableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) - burnInParameters = burnInParameters.copy( minViewY = 100, @@ -226,10 +228,9 @@ class AodBurnInViewModelTest : SysuiTestCase() { } @Test + @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) fun translationAndScale_whenFullyDozing_MigrationFlagOn_staysOutOfTopInset() = testScope.runTest { - mSetFlagsRule.enableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) - burnInParameters = burnInParameters.copy( minViewY = 100, @@ -310,104 +311,99 @@ class AodBurnInViewModelTest : SysuiTestCase() { } @Test + @DisableFlags(AConfigFlags.FLAG_COMPOSE_LOCKSCREEN) + @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) fun translationAndScale_composeFlagOff_weatherLargeClock() = testBurnInViewModelForClocks( isSmallClock = false, isWeatherClock = true, expectedScaleOnly = false, - enableMigrateClocksToBlueprintFlag = true, - enableComposeLockscreenFlag = false ) @Test + @DisableFlags(AConfigFlags.FLAG_COMPOSE_LOCKSCREEN) + @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) fun translationAndScale_composeFlagOff_weatherSmallClock() = testBurnInViewModelForClocks( isSmallClock = true, isWeatherClock = true, expectedScaleOnly = false, - enableMigrateClocksToBlueprintFlag = true, - enableComposeLockscreenFlag = false ) @Test + @DisableFlags(AConfigFlags.FLAG_COMPOSE_LOCKSCREEN) + @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) fun translationAndScale_composeFlagOff_nonWeatherLargeClock() = testBurnInViewModelForClocks( isSmallClock = false, isWeatherClock = false, expectedScaleOnly = true, - enableMigrateClocksToBlueprintFlag = true, - enableComposeLockscreenFlag = false ) @Test + @DisableFlags(AConfigFlags.FLAG_COMPOSE_LOCKSCREEN) + @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) fun translationAndScale_composeFlagOff_nonWeatherSmallClock() = testBurnInViewModelForClocks( isSmallClock = true, isWeatherClock = false, expectedScaleOnly = false, - enableMigrateClocksToBlueprintFlag = true, - enableComposeLockscreenFlag = false ) @Test + @EnableFlags( + AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT, + AConfigFlags.FLAG_COMPOSE_LOCKSCREEN + ) fun translationAndScale_composeFlagOn_weatherLargeClock() = testBurnInViewModelForClocks( isSmallClock = false, isWeatherClock = true, expectedScaleOnly = false, - enableMigrateClocksToBlueprintFlag = true, - enableComposeLockscreenFlag = true ) @Test + @EnableFlags( + AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT, + AConfigFlags.FLAG_COMPOSE_LOCKSCREEN + ) fun translationAndScale_composeFlagOn_weatherSmallClock() = testBurnInViewModelForClocks( isSmallClock = true, isWeatherClock = true, expectedScaleOnly = false, - enableMigrateClocksToBlueprintFlag = true, - enableComposeLockscreenFlag = true ) @Test + @EnableFlags( + AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT, + AConfigFlags.FLAG_COMPOSE_LOCKSCREEN + ) fun translationAndScale_composeFlagOn_nonWeatherLargeClock() = testBurnInViewModelForClocks( isSmallClock = false, isWeatherClock = false, expectedScaleOnly = true, - enableMigrateClocksToBlueprintFlag = true, - enableComposeLockscreenFlag = true ) @Test + @EnableFlags( + AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT, + AConfigFlags.FLAG_COMPOSE_LOCKSCREEN + ) fun translationAndScale_composeFlagOn_nonWeatherSmallClock() = testBurnInViewModelForClocks( isSmallClock = true, isWeatherClock = false, expectedScaleOnly = false, - enableMigrateClocksToBlueprintFlag = true, - enableComposeLockscreenFlag = true ) private fun testBurnInViewModelForClocks( isSmallClock: Boolean, isWeatherClock: Boolean, expectedScaleOnly: Boolean, - enableMigrateClocksToBlueprintFlag: Boolean, - enableComposeLockscreenFlag: Boolean ) = testScope.runTest { - if (enableMigrateClocksToBlueprintFlag) { - mSetFlagsRule.enableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) - } else { - mSetFlagsRule.disableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) - } - - if (enableComposeLockscreenFlag) { - mSetFlagsRule.enableFlags(AConfigFlags.FLAG_COMPOSE_LOCKSCREEN) - } else { - mSetFlagsRule.disableFlags(AConfigFlags.FLAG_COMPOSE_LOCKSCREEN) - } if (isSmallClock) { keyguardClockRepository.setClockSize(ClockSize.SMALL) // we need the following step to update stateFlow value diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt index 278c90a5f068..fd2e33504ec9 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt @@ -159,8 +159,9 @@ class PrimaryBouncerToGoneTransitionViewModelTest : SysuiTestCase() { ) assertThat(values[0]).isEqualTo(1f) - // Should fade to zero between here assertThat(values[1]).isEqualTo(0f) + // Should always finish with 1f to show HUNs + assertThat(values[2]).isEqualTo(1f) } @Test @@ -177,7 +178,7 @@ class PrimaryBouncerToGoneTransitionViewModelTest : SysuiTestCase() { testScope, ) - assertThat(values.size).isEqualTo(2) + assertThat(values.size).isEqualTo(3) // Shade stays open, and alpha should remain visible values.forEach { assertThat(it).isEqualTo(1f) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt index 9b9e584a936e..d5c910248942 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt @@ -21,14 +21,23 @@ import android.provider.Settings import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.qs.tiles.base.actions.FakeQSTileIntentUserInputHandler import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandlerSubject import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesTileModel +import com.android.systemui.statusbar.phone.SystemUIDialog +import com.android.systemui.statusbar.policy.ui.dialog.ModesDialogDelegate import com.google.common.truth.Truth +import kotlin.coroutines.EmptyCoroutineContext 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.verify +import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidJUnit4::class) @@ -36,7 +45,33 @@ import org.junit.runner.RunWith class ModesTileUserActionInteractorTest : SysuiTestCase() { private val inputHandler = FakeQSTileIntentUserInputHandler() - val underTest = ModesTileUserActionInteractor(inputHandler) + @Mock private lateinit var dialogTransitionAnimator: DialogTransitionAnimator + @Mock private lateinit var dialogDelegate: ModesDialogDelegate + @Mock private lateinit var mockDialog: SystemUIDialog + + private lateinit var underTest: ModesTileUserActionInteractor + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + + whenever(dialogDelegate.createDialog()).thenReturn(mockDialog) + + underTest = + ModesTileUserActionInteractor( + EmptyCoroutineContext, + inputHandler, + dialogTransitionAnimator, + dialogDelegate, + ) + } + + @Test + fun handleClick() = runTest { + underTest.handleInput(QSTileInputTestKtx.click(ModesTileModel(false))) + + verify(mockDialog).show() + } @Test fun handleLongClick() = runTest { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapperTest.kt new file mode 100644 index 000000000000..3baf2f40f175 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapperTest.kt @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles.impl.modes.ui + +import android.graphics.drawable.TestStubDrawable +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesTileModel +import com.android.systemui.qs.tiles.viewmodel.QSTileConfigTestBuilder +import com.android.systemui.qs.tiles.viewmodel.QSTileState +import com.android.systemui.qs.tiles.viewmodel.QSTileUIConfig +import com.android.systemui.res.R +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class ModesTileMapperTest : SysuiTestCase() { + val config = + QSTileConfigTestBuilder.build { + uiConfig = + QSTileUIConfig.Resource( + iconRes = R.drawable.qs_dnd_icon_off, + labelRes = R.string.quick_settings_modes_label, + ) + } + + val underTest = + ModesTileMapper( + context.orCreateTestableResources + .apply { + addOverride(R.drawable.qs_dnd_icon_on, TestStubDrawable()) + addOverride(R.drawable.qs_dnd_icon_off, TestStubDrawable()) + } + .resources, + context.theme, + ) + + @Test + fun inactiveState() { + val model = ModesTileModel(isActivated = false) + + val state = underTest.map(config, model) + + assertThat(state.activationState).isEqualTo(QSTileState.ActivationState.INACTIVE) + assertThat(state.iconRes).isEqualTo(R.drawable.qs_dnd_icon_off) + } + + @Test + fun activeState() { + val model = ModesTileModel(isActivated = true) + + val state = underTest.map(config, model) + + assertThat(state.activationState).isEqualTo(QSTileState.ActivationState.ACTIVE) + assertThat(state.iconRes).isEqualTo(R.drawable.qs_dnd_icon_on) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt index fd1b21332973..540a85aeb695 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt @@ -175,12 +175,14 @@ class SceneContainerStartableTest : SysuiTestCase() { transitionStateFlow.value = ObservableTransitionState.Idle(Scenes.Gone) assertThat(isVisible).isFalse() - kosmos.headsUpNotificationRepository.activeHeadsUpRows.value = + kosmos.headsUpNotificationRepository.setNotifications( buildNotificationRows(isPinned = true) + ) assertThat(isVisible).isTrue() - kosmos.headsUpNotificationRepository.activeHeadsUpRows.value = + kosmos.headsUpNotificationRepository.setNotifications( buildNotificationRows(isPinned = false) + ) assertThat(isVisible).isFalse() } @@ -1559,6 +1561,63 @@ class SceneContainerStartableTest : SysuiTestCase() { verify(dismissCallback).onDismissCancelled() } + @Test + fun refreshLockscreenEnabled() = + testScope.runTest { + val transitionState = + prepareState( + isDeviceUnlocked = true, + initialSceneKey = Scenes.Gone, + ) + underTest.start() + val isLockscreenEnabled by + collectLastValue(kosmos.deviceEntryInteractor.isLockscreenEnabled) + assertThat(isLockscreenEnabled).isTrue() + + kosmos.fakeDeviceEntryRepository.setPendingLockscreenEnabled(false) + runCurrent() + // Pending value didn't propagate yet. + assertThat(isLockscreenEnabled).isTrue() + + // Starting a transition to Lockscreen should refresh the value, causing the pending + // value + // to propagate to the real flow: + transitionState.value = + ObservableTransitionState.Transition( + fromScene = Scenes.Gone, + toScene = Scenes.Lockscreen, + currentScene = flowOf(Scenes.Gone), + progress = flowOf(0.1f), + isInitiatedByUserInput = false, + isUserInputOngoing = flowOf(false), + ) + runCurrent() + assertThat(isLockscreenEnabled).isFalse() + + kosmos.fakeDeviceEntryRepository.setPendingLockscreenEnabled(true) + runCurrent() + // Pending value didn't propagate yet. + assertThat(isLockscreenEnabled).isFalse() + transitionState.value = ObservableTransitionState.Idle(Scenes.Gone) + runCurrent() + assertThat(isLockscreenEnabled).isFalse() + + // Starting another transition to Lockscreen should refresh the value, causing the + // pending + // value to propagate to the real flow: + transitionState.value = + ObservableTransitionState.Transition( + fromScene = Scenes.Gone, + toScene = Scenes.Lockscreen, + currentScene = flowOf(Scenes.Gone), + progress = flowOf(0.1f), + isInitiatedByUserInput = false, + isUserInputOngoing = flowOf(false), + ) + runCurrent() + assertThat(isLockscreenEnabled).isTrue() + } + private fun TestScope.emulateSceneTransition( transitionStateFlow: MutableStateFlow<ObservableTransitionState>, toScene: SceneKey, @@ -1642,8 +1701,8 @@ class SceneContainerStartableTest : SysuiTestCase() { return transitionStateFlow } - private fun buildNotificationRows(isPinned: Boolean = false): Set<HeadsUpRowRepository> = - setOf( + private fun buildNotificationRows(isPinned: Boolean = false): List<HeadsUpRowRepository> = + listOf( fakeHeadsUpRowRepository(key = "0", isPinned = isPinned), fakeHeadsUpRowRepository(key = "1", isPinned = isPinned), fakeHeadsUpRowRepository(key = "2", isPinned = isPinned), diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt new file mode 100644 index 000000000000..8810ade1d851 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt @@ -0,0 +1,510 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.android.systemui.statusbar.notification.collection.coordinator + +import android.app.Notification +import android.app.NotificationManager.IMPORTANCE_DEFAULT +import android.app.NotificationManager.IMPORTANCE_LOW +import android.os.UserHandle +import android.platform.test.annotations.EnableFlags +import android.provider.Settings +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.keyguard.shared.model.StatusBarState +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.plugins.statusbar.statusBarStateController +import com.android.systemui.shade.shadeTestUtil +import com.android.systemui.statusbar.SysuiStatusBarStateController +import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder +import com.android.systemui.statusbar.notification.collection.NotifPipeline +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder +import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeTransformGroupsListener +import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter +import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner +import com.android.systemui.statusbar.notification.collection.modifyEntry +import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener +import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository +import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository +import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype +import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository +import com.android.systemui.testKosmos +import com.android.systemui.util.settings.FakeSettings +import com.android.systemui.util.settings.fakeSettings +import com.google.common.truth.StringSubject +import com.google.common.truth.Truth.assertThat +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestCoroutineScheduler +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@SmallTest +@RunWith(AndroidJUnit4::class) +@EnableFlags(NotificationMinimalismPrototype.FLAG_NAME) +class LockScreenMinimalismCoordinatorTest : SysuiTestCase() { + + private val kosmos = + testKosmos().apply { + testDispatcher = UnconfinedTestDispatcher() + statusBarStateController = + mock<SysuiStatusBarStateController>().also { mock -> + doAnswer { statusBarState.ordinal }.whenever(mock).state + } + fakeSettings.putInt(Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, 1) + } + private val notifPipeline: NotifPipeline = mock() + private var statusBarState: StatusBarState = StatusBarState.KEYGUARD + + @Test + fun topUnseenSectioner() { + val solo = NotificationEntryBuilder().setTag("solo").build() + val child1 = NotificationEntryBuilder().setTag("child1").build() + val child2 = NotificationEntryBuilder().setTag("child2").build() + val parent = NotificationEntryBuilder().setTag("parent").build() + val group = GroupEntryBuilder().addChild(child1).addChild(child2).setSummary(parent).build() + + runCoordinatorTest { + kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = solo.key + assertThat(topUnseenSectioner.isInSection(solo)).isTrue() + assertThat(topUnseenSectioner.isInSection(child1)).isFalse() + assertThat(topUnseenSectioner.isInSection(child2)).isFalse() + assertThat(topUnseenSectioner.isInSection(parent)).isFalse() + assertThat(topUnseenSectioner.isInSection(group)).isFalse() + + kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = child1.key + assertThat(topUnseenSectioner.isInSection(solo)).isFalse() + assertThat(topUnseenSectioner.isInSection(child1)).isTrue() + assertThat(topUnseenSectioner.isInSection(child2)).isFalse() + assertThat(topUnseenSectioner.isInSection(parent)).isFalse() + assertThat(topUnseenSectioner.isInSection(group)).isTrue() + + kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = parent.key + assertThat(topUnseenSectioner.isInSection(solo)).isFalse() + assertThat(topUnseenSectioner.isInSection(child1)).isFalse() + assertThat(topUnseenSectioner.isInSection(child2)).isFalse() + assertThat(topUnseenSectioner.isInSection(parent)).isTrue() + assertThat(topUnseenSectioner.isInSection(group)).isTrue() + + kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = solo.key + kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = null + assertThat(topUnseenSectioner.isInSection(solo)).isFalse() + assertThat(topUnseenSectioner.isInSection(child1)).isFalse() + assertThat(topUnseenSectioner.isInSection(child2)).isFalse() + assertThat(topUnseenSectioner.isInSection(parent)).isFalse() + assertThat(topUnseenSectioner.isInSection(group)).isFalse() + } + } + + @Test + fun topOngoingSectioner() { + val solo = NotificationEntryBuilder().setTag("solo").build() + val child1 = NotificationEntryBuilder().setTag("child1").build() + val child2 = NotificationEntryBuilder().setTag("child2").build() + val parent = NotificationEntryBuilder().setTag("parent").build() + val group = GroupEntryBuilder().addChild(child1).addChild(child2).setSummary(parent).build() + + runCoordinatorTest { + kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = solo.key + assertThat(topOngoingSectioner.isInSection(solo)).isTrue() + assertThat(topOngoingSectioner.isInSection(child1)).isFalse() + assertThat(topOngoingSectioner.isInSection(child2)).isFalse() + assertThat(topOngoingSectioner.isInSection(parent)).isFalse() + assertThat(topOngoingSectioner.isInSection(group)).isFalse() + + kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = child1.key + assertThat(topOngoingSectioner.isInSection(solo)).isFalse() + assertThat(topOngoingSectioner.isInSection(child1)).isTrue() + assertThat(topOngoingSectioner.isInSection(child2)).isFalse() + assertThat(topOngoingSectioner.isInSection(parent)).isFalse() + assertThat(topOngoingSectioner.isInSection(group)).isTrue() + + kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = parent.key + assertThat(topOngoingSectioner.isInSection(solo)).isFalse() + assertThat(topOngoingSectioner.isInSection(child1)).isFalse() + assertThat(topOngoingSectioner.isInSection(child2)).isFalse() + assertThat(topOngoingSectioner.isInSection(parent)).isTrue() + assertThat(topOngoingSectioner.isInSection(group)).isTrue() + + kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = null + kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = solo.key + assertThat(topOngoingSectioner.isInSection(solo)).isFalse() + assertThat(topOngoingSectioner.isInSection(child1)).isFalse() + assertThat(topOngoingSectioner.isInSection(child2)).isFalse() + assertThat(topOngoingSectioner.isInSection(parent)).isFalse() + assertThat(topOngoingSectioner.isInSection(group)).isFalse() + } + } + + @Test + fun testPromoter() { + val child1 = NotificationEntryBuilder().setTag("child1").build() + val child2 = NotificationEntryBuilder().setTag("child2").build() + val child3 = NotificationEntryBuilder().setTag("child3").build() + val parent = NotificationEntryBuilder().setTag("parent").build() + GroupEntryBuilder() + .addChild(child1) + .addChild(child2) + .addChild(child3) + .setSummary(parent) + .build() + + runCoordinatorTest { + kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = null + kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = null + assertThat(promoter.shouldPromoteToTopLevel(child1)).isFalse() + assertThat(promoter.shouldPromoteToTopLevel(child2)).isFalse() + assertThat(promoter.shouldPromoteToTopLevel(child3)).isFalse() + assertThat(promoter.shouldPromoteToTopLevel(parent)).isFalse() + + kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = child1.key + kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = null + assertThat(promoter.shouldPromoteToTopLevel(child1)).isTrue() + assertThat(promoter.shouldPromoteToTopLevel(child2)).isFalse() + assertThat(promoter.shouldPromoteToTopLevel(child3)).isFalse() + assertThat(promoter.shouldPromoteToTopLevel(parent)).isFalse() + + kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = null + kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = child2.key + assertThat(promoter.shouldPromoteToTopLevel(child1)).isFalse() + assertThat(promoter.shouldPromoteToTopLevel(child2)) + .isEqualTo(NotificationMinimalismPrototype.ungroupTopUnseen) + assertThat(promoter.shouldPromoteToTopLevel(child3)).isFalse() + assertThat(promoter.shouldPromoteToTopLevel(parent)).isFalse() + + kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = child1.key + kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = child2.key + assertThat(promoter.shouldPromoteToTopLevel(child1)).isTrue() + assertThat(promoter.shouldPromoteToTopLevel(child2)) + .isEqualTo(NotificationMinimalismPrototype.ungroupTopUnseen) + assertThat(promoter.shouldPromoteToTopLevel(child3)).isFalse() + assertThat(promoter.shouldPromoteToTopLevel(parent)).isFalse() + } + } + + @Test + fun topOngoingIdentifier() { + val solo1 = defaultEntryBuilder().setTag("solo1").setRank(1).build() + val solo2 = defaultEntryBuilder().setTag("solo2").setRank(2).build() + val parent = defaultEntryBuilder().setTag("parent").setRank(3).build() + val child1 = defaultEntryBuilder().setTag("child1").setRank(4).build() + val child2 = defaultEntryBuilder().setTag("child2").setRank(5).build() + val group = GroupEntryBuilder().setSummary(parent).addChild(child1).addChild(child2).build() + val listEntryList = listOf(group, solo1, solo2) + + runCoordinatorTest { + // TEST: base case - no entries in the list + onBeforeTransformGroupsListener.onBeforeTransformGroups(emptyList()) + assertThatTopOngoingKey().isEqualTo(null) + assertThatTopUnseenKey().isEqualTo(null) + + // TEST: none of these are unseen or ongoing yet, so don't pick them + onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList) + assertThatTopOngoingKey().isEqualTo(null) + assertThatTopUnseenKey().isEqualTo(null) + + // TEST: when solo2 is the only one colorized, it gets picked up + solo2.setColorizedFgs(true) + onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList) + assertThatTopOngoingKey().isEqualTo(solo2.key) + assertThatTopUnseenKey().isEqualTo(null) + + // TEST: once solo1 is colorized, it takes priority for being ranked higher + solo1.setColorizedFgs(true) + onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList) + assertThatTopOngoingKey().isEqualTo(solo1.key) + assertThatTopUnseenKey().isEqualTo(null) + + // TEST: changing just the rank of solo1 causes it to pick up solo2 instead + solo1.modifyEntry { setRank(20) } + onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList) + assertThatTopOngoingKey().isEqualTo(solo2.key) + assertThatTopUnseenKey().isEqualTo(null) + + // TEST: switching to SHADE disables the whole thing + statusBarState = StatusBarState.SHADE + onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList) + assertThatTopOngoingKey().isEqualTo(null) + assertThatTopUnseenKey().isEqualTo(null) + + // TEST: switching back to KEYGUARD picks up the same entry again + statusBarState = StatusBarState.KEYGUARD + onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList) + assertThatTopOngoingKey().isEqualTo(solo2.key) + assertThatTopUnseenKey().isEqualTo(null) + + // TEST: updating to not colorized revokes the top-ongoing status + solo2.setColorizedFgs(false) + onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList) + assertThatTopOngoingKey().isEqualTo(solo1.key) + assertThatTopUnseenKey().isEqualTo(null) + + // TEST: updating the importance to LOW revokes top-ongoing status + solo1.modifyEntry { setImportance(IMPORTANCE_LOW) } + onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList) + assertThatTopOngoingKey().isEqualTo(null) + assertThatTopUnseenKey().isEqualTo(null) + } + } + + @Test + fun topUnseenIdentifier() { + val solo1 = defaultEntryBuilder().setTag("solo1").setRank(1).build() + val solo2 = defaultEntryBuilder().setTag("solo2").setRank(2).build() + val parent = defaultEntryBuilder().setTag("parent").setRank(4).build() + val child1 = defaultEntryBuilder().setTag("child1").setRank(5).build() + val child2 = defaultEntryBuilder().setTag("child2").setRank(6).build() + val group = GroupEntryBuilder().setSummary(parent).addChild(child1).addChild(child2).build() + val listEntryList = listOf(group, solo1, solo2) + val notificationEntryList = listOf(solo1, solo2, parent, child1, child2) + + runCoordinatorTest { + // All entries are added (and now unseen) + notificationEntryList.forEach { collectionListener.onEntryAdded(it) } + + // TEST: Filtered out entries are ignored + onBeforeTransformGroupsListener.onBeforeTransformGroups(emptyList()) + assertThatTopOngoingKey().isEqualTo(null) + assertThatTopUnseenKey().isEqualTo(null) + + // TEST: top-ranked unseen child is selected (not the summary) + onBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(group)) + assertThatTopOngoingKey().isEqualTo(null) + assertThatTopUnseenKey().isEqualTo(child1.key) + + // TEST: top-ranked entry is picked + onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList) + assertThatTopOngoingKey().isEqualTo(null) + assertThatTopUnseenKey().isEqualTo(solo1.key) + + // TEST: if top-ranked unseen is colorized, fall back to #2 ranked unseen + solo1.setColorizedFgs(true) + onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList) + assertThatTopOngoingKey().isEqualTo(solo1.key) + assertThatTopUnseenKey().isEqualTo(solo2.key) + + // TEST: no more colorized entries + solo1.setColorizedFgs(false) + onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList) + assertThatTopOngoingKey().isEqualTo(null) + assertThatTopUnseenKey().isEqualTo(solo1.key) + + // TEST: if the rank of solo1 is reduced, solo2 will be preferred + solo1.modifyEntry { setRank(3) } + onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList) + assertThatTopOngoingKey().isEqualTo(null) + assertThatTopUnseenKey().isEqualTo(solo2.key) + + // TEST: switching to SHADE state will disable the entire selector + statusBarState = StatusBarState.SHADE + onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList) + assertThatTopOngoingKey().isEqualTo(null) + assertThatTopUnseenKey().isEqualTo(null) + + // TEST: switching back to KEYGUARD re-enables the selector + statusBarState = StatusBarState.KEYGUARD + onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList) + assertThatTopOngoingKey().isEqualTo(null) + assertThatTopUnseenKey().isEqualTo(solo2.key) + + // TEST: QS Expansion does not mark entries as seen + setShadeAndQsExpansionThenWait(0f, 1f) + onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList) + assertThatTopOngoingKey().isEqualTo(null) + assertThatTopUnseenKey().isEqualTo(solo2.key) + + // TEST: Shade expansion does mark entries as seen + setShadeAndQsExpansionThenWait(1f, 0f) + onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList) + assertThatTopOngoingKey().isEqualTo(null) + assertThatTopUnseenKey().isEqualTo(null) + + // TEST: Entries updated while shade is expanded are NOT marked unseen + collectionListener.onEntryUpdated(solo1) + collectionListener.onEntryUpdated(solo2) + onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList) + assertThatTopOngoingKey().isEqualTo(null) + assertThatTopUnseenKey().isEqualTo(null) + + // TEST: Entries updated after shade is collapsed ARE marked unseen + setShadeAndQsExpansionThenWait(0f, 0f) + collectionListener.onEntryUpdated(solo1) + collectionListener.onEntryUpdated(solo2) + onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList) + assertThatTopOngoingKey().isEqualTo(null) + assertThatTopUnseenKey().isEqualTo(solo2.key) + + // TEST: low importance disqualifies the entry for top unseen + solo2.modifyEntry { setImportance(IMPORTANCE_LOW) } + onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList) + assertThatTopOngoingKey().isEqualTo(null) + assertThatTopUnseenKey().isEqualTo(solo1.key) + } + } + + @Test + fun topUnseenIdentifier_headsUpMarksSeen() { + val solo1 = defaultEntryBuilder().setTag("solo1").setRank(1).build() + val solo2 = defaultEntryBuilder().setTag("solo2").setRank(2).build() + val listEntryList = listOf(solo1, solo2) + val notificationEntryList = listOf(solo1, solo2) + + val hunRepo1 = solo1.fakeHeadsUpRowRepository() + val hunRepo2 = solo2.fakeHeadsUpRowRepository() + + runCoordinatorTest { + // All entries are added (and now unseen) + notificationEntryList.forEach { collectionListener.onEntryAdded(it) } + + // TEST: top-ranked entry is picked + onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList) + assertThatTopUnseenKey().isEqualTo(solo1.key) + + // TEST: heads up state and waiting isn't enough to be seen + kosmos.headsUpNotificationRepository.orderedHeadsUpRows.value = + listOf(hunRepo1, hunRepo2) + testScheduler.advanceTimeBy(1.seconds) + onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList) + assertThatTopUnseenKey().isEqualTo(solo1.key) + + // TEST: even being pinned doesn't take effect immediately + hunRepo1.isPinned.value = true + testScheduler.advanceTimeBy(0.5.seconds) + onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList) + assertThatTopUnseenKey().isEqualTo(solo1.key) + + // TEST: after being pinned a full second, solo1 is seen + testScheduler.advanceTimeBy(0.5.seconds) + onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList) + assertThatTopUnseenKey().isEqualTo(solo2.key) + + // TEST: repeat; being heads up and pinned for 1 second triggers seen + kosmos.headsUpNotificationRepository.orderedHeadsUpRows.value = listOf(hunRepo2) + hunRepo1.isPinned.value = false + hunRepo2.isPinned.value = true + testScheduler.advanceTimeBy(1.seconds) + onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList) + assertThatTopUnseenKey().isEqualTo(null) + } + } + + private fun NotificationEntry.fakeHeadsUpRowRepository() = + FakeHeadsUpRowRepository(key = key, elementKey = Any()) + + private fun KeyguardCoordinatorTestScope.setShadeAndQsExpansionThenWait( + shadeExpansion: Float, + qsExpansion: Float + ) { + kosmos.shadeTestUtil.setShadeAndQsExpansion(shadeExpansion, qsExpansion) + // The coordinator waits a fraction of a second for the shade expansion to stick. + testScheduler.advanceTimeBy(1.seconds) + } + + private fun defaultEntryBuilder() = NotificationEntryBuilder().setImportance(IMPORTANCE_DEFAULT) + + private fun runCoordinatorTest(testBlock: suspend KeyguardCoordinatorTestScope.() -> Unit) { + kosmos.lockScreenMinimalismCoordinator.attach(notifPipeline) + kosmos.testScope.runTest(dispatchTimeoutMs = 1.seconds.inWholeMilliseconds) { + KeyguardCoordinatorTestScope( + kosmos.lockScreenMinimalismCoordinator, + kosmos.testScope, + kosmos.fakeSettings, + ) + .testBlock() + } + } + + private inner class KeyguardCoordinatorTestScope( + private val coordinator: LockScreenMinimalismCoordinator, + private val scope: TestScope, + private val fakeSettings: FakeSettings, + ) : CoroutineScope by scope { + fun assertThatTopOngoingKey(): StringSubject { + return assertThat( + kosmos.activeNotificationListRepository.topOngoingNotificationKey.value + ) + } + + fun assertThatTopUnseenKey(): StringSubject { + return assertThat( + kosmos.activeNotificationListRepository.topUnseenNotificationKey.value + ) + } + + val testScheduler: TestCoroutineScheduler + get() = scope.testScheduler + + val promoter: NotifPromoter + get() = coordinator.unseenNotifPromoter + + val topUnseenSectioner: NotifSectioner + get() = coordinator.topUnseenSectioner + + val topOngoingSectioner: NotifSectioner + get() = coordinator.topOngoingSectioner + + val onBeforeTransformGroupsListener: OnBeforeTransformGroupsListener = + argumentCaptor { verify(notifPipeline).addOnBeforeTransformGroupsListener(capture()) } + .lastValue + + val collectionListener: NotifCollectionListener = + argumentCaptor { verify(notifPipeline).addCollectionListener(capture()) }.lastValue + + var showOnlyUnseenNotifsOnKeyguardSetting: Boolean + get() = + fakeSettings.getIntForUser( + Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, + UserHandle.USER_CURRENT, + ) == 1 + set(value) { + fakeSettings.putIntForUser( + Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, + if (value) 1 else 2, + UserHandle.USER_CURRENT, + ) + } + } + + companion object { + + private fun NotificationEntry.setColorizedFgs(colorized: Boolean) { + sbn.notification.setColorizedFgs(colorized) + } + + private fun Notification.setColorizedFgs(colorized: Boolean) { + extras.putBoolean(Notification.EXTRA_COLORIZED, colorized) + flags = + if (colorized) { + flags or Notification.FLAG_FOREGROUND_SERVICE + } else { + flags and Notification.FLAG_FOREGROUND_SERVICE.inv() + } + } + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinatorTest.kt new file mode 100644 index 000000000000..6ddc07432a16 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinatorTest.kt @@ -0,0 +1,683 @@ +/* + * 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. + */ +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.android.systemui.statusbar.notification.collection.coordinator + +import android.app.Notification +import android.os.UserHandle +import android.platform.test.flag.junit.FlagsParameterization +import android.provider.Settings +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.dump.DumpManager +import com.android.systemui.flags.andSceneContainer +import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository +import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.log.logcatLogBuffer +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.scene.data.repository.Idle +import com.android.systemui.scene.data.repository.setTransition +import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder +import com.android.systemui.statusbar.notification.collection.NotifPipeline +import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder +import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter +import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Pluggable +import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener +import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository +import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow +import com.android.systemui.statusbar.policy.HeadsUpManager +import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener +import com.android.systemui.util.settings.FakeSettings +import com.google.common.truth.Truth.assertThat +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestCoroutineScheduler +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.same +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters + +@SmallTest +@RunWith(ParameterizedAndroidJunit4::class) +class OriginalUnseenKeyguardCoordinatorTest(flags: FlagsParameterization) : SysuiTestCase() { + + private val kosmos = Kosmos() + + private val headsUpManager: HeadsUpManager = mock() + private val keyguardRepository = FakeKeyguardRepository() + private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository + private val notifPipeline: NotifPipeline = mock() + private val statusBarStateController: StatusBarStateController = mock() + + init { + mSetFlagsRule.setFlagsParameterization(flags) + } + + @Test + fun unseenFilterSuppressesSeenNotifWhileKeyguardShowing() { + // GIVEN: Keyguard is not showing, shade is expanded, and a notification is present + keyguardRepository.setKeyguardShowing(false) + whenever(statusBarStateController.isExpanded).thenReturn(true) + runKeyguardCoordinatorTest { + val fakeEntry = NotificationEntryBuilder().build() + collectionListener.onEntryAdded(fakeEntry) + + // WHEN: The keyguard is now showing + keyguardRepository.setKeyguardShowing(true) + testScheduler.runCurrent() + + // THEN: The notification is recognized as "seen" and is filtered out. + assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue() + + // WHEN: The keyguard goes away + keyguardRepository.setKeyguardShowing(false) + testScheduler.runCurrent() + + // THEN: The notification is shown regardless + assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse() + } + } + + @Test + fun unseenFilterStopsMarkingSeenNotifWhenTransitionToAod() { + // GIVEN: Keyguard is not showing, shade is not expanded, and a notification is present + keyguardRepository.setKeyguardShowing(false) + whenever(statusBarStateController.isExpanded).thenReturn(false) + runKeyguardCoordinatorTest { + val fakeEntry = NotificationEntryBuilder().build() + collectionListener.onEntryAdded(fakeEntry) + + // WHEN: The device transitions to AOD + keyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.GONE, + to = KeyguardState.AOD, + this.testScheduler, + ) + testScheduler.runCurrent() + + // THEN: We are no longer listening for shade expansions + verify(statusBarStateController, never()).addCallback(any()) + } + } + + @Test + fun unseenFilter_headsUpMarkedAsSeen() { + // GIVEN: Keyguard is not showing, shade is not expanded + keyguardRepository.setKeyguardShowing(false) + whenever(statusBarStateController.isExpanded).thenReturn(false) + runKeyguardCoordinatorTest { + kosmos.setTransition( + sceneTransition = Idle(Scenes.Gone), + stateTransition = TransitionStep(KeyguardState.LOCKSCREEN, KeyguardState.GONE) + ) + + // WHEN: A notification is posted + val fakeEntry = NotificationEntryBuilder().build() + collectionListener.onEntryAdded(fakeEntry) + + // WHEN: That notification is heads up + onHeadsUpChangedListener.onHeadsUpStateChanged(fakeEntry, /* isHeadsUp= */ true) + testScheduler.runCurrent() + + // WHEN: The keyguard is now showing + keyguardRepository.setKeyguardShowing(true) + kosmos.setTransition( + sceneTransition = Idle(Scenes.Lockscreen), + stateTransition = TransitionStep(KeyguardState.GONE, KeyguardState.AOD) + ) + + // THEN: The notification is recognized as "seen" and is filtered out. + assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue() + + // WHEN: The keyguard goes away + keyguardRepository.setKeyguardShowing(false) + kosmos.setTransition( + sceneTransition = Idle(Scenes.Gone), + stateTransition = TransitionStep(KeyguardState.AOD, KeyguardState.GONE) + ) + + // THEN: The notification is shown regardless + assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse() + } + } + + @Test + fun unseenFilterDoesNotSuppressSeenOngoingNotifWhileKeyguardShowing() { + // GIVEN: Keyguard is not showing, shade is expanded, and an ongoing notification is present + keyguardRepository.setKeyguardShowing(false) + whenever(statusBarStateController.isExpanded).thenReturn(true) + runKeyguardCoordinatorTest { + val fakeEntry = + NotificationEntryBuilder() + .setNotification(Notification.Builder(mContext, "id").setOngoing(true).build()) + .build() + collectionListener.onEntryAdded(fakeEntry) + + // WHEN: The keyguard is now showing + keyguardRepository.setKeyguardShowing(true) + testScheduler.runCurrent() + + // THEN: The notification is recognized as "ongoing" and is not filtered out. + assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse() + } + } + + @Test + fun unseenFilterDoesNotSuppressSeenMediaNotifWhileKeyguardShowing() { + // GIVEN: Keyguard is not showing, shade is expanded, and a media notification is present + keyguardRepository.setKeyguardShowing(false) + whenever(statusBarStateController.isExpanded).thenReturn(true) + runKeyguardCoordinatorTest { + val fakeEntry = + NotificationEntryBuilder().build().apply { + row = + mock<ExpandableNotificationRow>().apply { + whenever(isMediaRow).thenReturn(true) + } + } + collectionListener.onEntryAdded(fakeEntry) + + // WHEN: The keyguard is now showing + keyguardRepository.setKeyguardShowing(true) + testScheduler.runCurrent() + + // THEN: The notification is recognized as "media" and is not filtered out. + assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse() + } + } + + @Test + fun unseenFilterUpdatesSeenProviderWhenSuppressing() { + // GIVEN: Keyguard is not showing, shade is expanded, and a notification is present + keyguardRepository.setKeyguardShowing(false) + whenever(statusBarStateController.isExpanded).thenReturn(true) + runKeyguardCoordinatorTest { + val fakeEntry = NotificationEntryBuilder().build() + collectionListener.onEntryAdded(fakeEntry) + + // WHEN: The keyguard is now showing + keyguardRepository.setKeyguardShowing(true) + testScheduler.runCurrent() + + // THEN: The notification is recognized as "seen" and is filtered out. + assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue() + + // WHEN: The filter is cleaned up + unseenFilter.onCleanup() + + // THEN: The SeenNotificationProvider has been updated to reflect the suppression + assertThat(seenNotificationsInteractor.hasFilteredOutSeenNotifications.value).isTrue() + } + } + + @Test + fun unseenFilterInvalidatesWhenSettingChanges() { + // GIVEN: Keyguard is not showing, and shade is expanded + keyguardRepository.setKeyguardShowing(false) + whenever(statusBarStateController.isExpanded).thenReturn(true) + runKeyguardCoordinatorTest { + // GIVEN: A notification is present + val fakeEntry = NotificationEntryBuilder().build() + collectionListener.onEntryAdded(fakeEntry) + + // GIVEN: The setting for filtering unseen notifications is disabled + showOnlyUnseenNotifsOnKeyguardSetting = false + + // GIVEN: The pipeline has registered the unseen filter for invalidation + val invalidationListener: Pluggable.PluggableListener<NotifFilter> = mock() + unseenFilter.setInvalidationListener(invalidationListener) + + // WHEN: The keyguard is now showing + keyguardRepository.setKeyguardShowing(true) + testScheduler.runCurrent() + + // THEN: The notification is not filtered out + assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse() + + // WHEN: The secure setting is changed + showOnlyUnseenNotifsOnKeyguardSetting = true + + // THEN: The pipeline is invalidated + verify(invalidationListener).onPluggableInvalidated(same(unseenFilter), any()) + + // THEN: The notification is recognized as "seen" and is filtered out. + assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue() + } + } + + @Test + fun unseenFilterAllowsNewNotif() { + // GIVEN: Keyguard is showing, no notifications present + keyguardRepository.setKeyguardShowing(true) + runKeyguardCoordinatorTest { + // WHEN: A new notification is posted + val fakeEntry = NotificationEntryBuilder().build() + collectionListener.onEntryAdded(fakeEntry) + + // THEN: The notification is recognized as "unseen" and is not filtered out. + assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse() + } + } + + @Test + fun unseenFilterSeenGroupSummaryWithUnseenChild() { + // GIVEN: Keyguard is not showing, shade is expanded, and a notification is present + keyguardRepository.setKeyguardShowing(false) + whenever(statusBarStateController.isExpanded).thenReturn(true) + runKeyguardCoordinatorTest { + // WHEN: A new notification is posted + val fakeSummary = NotificationEntryBuilder().build() + val fakeChild = + NotificationEntryBuilder() + .setGroup(context, "group") + .setGroupSummary(context, false) + .build() + GroupEntryBuilder().setSummary(fakeSummary).addChild(fakeChild).build() + + collectionListener.onEntryAdded(fakeSummary) + collectionListener.onEntryAdded(fakeChild) + + // WHEN: Keyguard is now showing, both notifications are marked as seen + keyguardRepository.setKeyguardShowing(true) + testScheduler.runCurrent() + + // WHEN: The child notification is now unseen + collectionListener.onEntryUpdated(fakeChild) + + // THEN: The summary is not filtered out, because the child is unseen + assertThat(unseenFilter.shouldFilterOut(fakeSummary, 0L)).isFalse() + } + } + + @Test + fun unseenNotificationIsMarkedAsSeenWhenKeyguardGoesAway() { + // GIVEN: Keyguard is showing, not dozing, unseen notification is present + keyguardRepository.setKeyguardShowing(true) + keyguardRepository.setIsDozing(false) + runKeyguardCoordinatorTest { + val fakeEntry = NotificationEntryBuilder().build() + collectionListener.onEntryAdded(fakeEntry) + keyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.AOD, + to = KeyguardState.LOCKSCREEN, + this.testScheduler, + ) + testScheduler.runCurrent() + + // WHEN: five seconds have passed + testScheduler.advanceTimeBy(5.seconds) + testScheduler.runCurrent() + + // WHEN: Keyguard is no longer showing + keyguardRepository.setKeyguardShowing(false) + kosmos.setTransition( + sceneTransition = Idle(Scenes.Gone), + stateTransition = TransitionStep(KeyguardState.LOCKSCREEN, KeyguardState.GONE) + ) + + // WHEN: Keyguard is shown again + keyguardRepository.setKeyguardShowing(true) + kosmos.setTransition( + sceneTransition = Idle(Scenes.Lockscreen), + stateTransition = TransitionStep(KeyguardState.GONE, KeyguardState.AOD) + ) + + // THEN: The notification is now recognized as "seen" and is filtered out. + assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue() + } + } + + @Test + fun unseenNotificationIsNotMarkedAsSeenIfShadeNotExpanded() { + // GIVEN: Keyguard is showing, unseen notification is present + keyguardRepository.setKeyguardShowing(true) + runKeyguardCoordinatorTest { + keyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.GONE, + to = KeyguardState.LOCKSCREEN, + this.testScheduler, + ) + val fakeEntry = NotificationEntryBuilder().build() + collectionListener.onEntryAdded(fakeEntry) + + // WHEN: Keyguard is no longer showing + keyguardRepository.setKeyguardShowing(false) + keyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + this.testScheduler, + ) + + // WHEN: Keyguard is shown again + keyguardRepository.setKeyguardShowing(true) + testScheduler.runCurrent() + + // THEN: The notification is not recognized as "seen" and is not filtered out. + assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse() + } + } + + @Test + fun unseenNotificationIsNotMarkedAsSeenIfNotOnKeyguardLongEnough() { + // GIVEN: Keyguard is showing, not dozing, unseen notification is present + keyguardRepository.setKeyguardShowing(true) + keyguardRepository.setIsDozing(false) + runKeyguardCoordinatorTest { + kosmos.setTransition( + sceneTransition = Idle(Scenes.Lockscreen), + stateTransition = TransitionStep(KeyguardState.GONE, KeyguardState.LOCKSCREEN) + ) + val firstEntry = NotificationEntryBuilder().setId(1).build() + collectionListener.onEntryAdded(firstEntry) + testScheduler.runCurrent() + + // WHEN: one second has passed + testScheduler.advanceTimeBy(1.seconds) + testScheduler.runCurrent() + + // WHEN: another unseen notification is posted + val secondEntry = NotificationEntryBuilder().setId(2).build() + collectionListener.onEntryAdded(secondEntry) + testScheduler.runCurrent() + + // WHEN: four more seconds have passed + testScheduler.advanceTimeBy(4.seconds) + testScheduler.runCurrent() + + // WHEN: the keyguard is no longer showing + keyguardRepository.setKeyguardShowing(false) + kosmos.setTransition( + sceneTransition = Idle(Scenes.Gone), + stateTransition = TransitionStep(KeyguardState.LOCKSCREEN, KeyguardState.GONE) + ) + + // WHEN: Keyguard is shown again + keyguardRepository.setKeyguardShowing(true) + kosmos.setTransition( + sceneTransition = Idle(Scenes.Lockscreen), + stateTransition = TransitionStep(KeyguardState.GONE, KeyguardState.LOCKSCREEN) + ) + + // THEN: The first notification is considered seen and is filtered out. + assertThat(unseenFilter.shouldFilterOut(firstEntry, 0L)).isTrue() + + // THEN: The second notification is still considered unseen and is not filtered out + assertThat(unseenFilter.shouldFilterOut(secondEntry, 0L)).isFalse() + } + } + + @Test + fun unseenNotificationOnKeyguardNotMarkedAsSeenIfRemovedAfterThreshold() { + // GIVEN: Keyguard is showing, not dozing + keyguardRepository.setKeyguardShowing(true) + keyguardRepository.setIsDozing(false) + runKeyguardCoordinatorTest { + keyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.GONE, + to = KeyguardState.LOCKSCREEN, + this.testScheduler, + ) + testScheduler.runCurrent() + + // WHEN: a new notification is posted + val entry = NotificationEntryBuilder().setId(1).build() + collectionListener.onEntryAdded(entry) + testScheduler.runCurrent() + + // WHEN: five more seconds have passed + testScheduler.advanceTimeBy(5.seconds) + testScheduler.runCurrent() + + // WHEN: the notification is removed + collectionListener.onEntryRemoved(entry, 0) + testScheduler.runCurrent() + + // WHEN: the notification is re-posted + collectionListener.onEntryAdded(entry) + testScheduler.runCurrent() + + // WHEN: one more second has passed + testScheduler.advanceTimeBy(1.seconds) + testScheduler.runCurrent() + + // WHEN: the keyguard is no longer showing + keyguardRepository.setKeyguardShowing(false) + keyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + this.testScheduler, + ) + testScheduler.runCurrent() + + // WHEN: Keyguard is shown again + keyguardRepository.setKeyguardShowing(true) + keyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.GONE, + to = KeyguardState.LOCKSCREEN, + this.testScheduler, + ) + testScheduler.runCurrent() + + // THEN: The notification is considered unseen and is not filtered out. + assertThat(unseenFilter.shouldFilterOut(entry, 0L)).isFalse() + } + } + + @Test + fun unseenNotificationOnKeyguardNotMarkedAsSeenIfRemovedBeforeThreshold() { + // GIVEN: Keyguard is showing, not dozing + keyguardRepository.setKeyguardShowing(true) + keyguardRepository.setIsDozing(false) + runKeyguardCoordinatorTest { + keyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.GONE, + to = KeyguardState.LOCKSCREEN, + this.testScheduler, + ) + testScheduler.runCurrent() + + // WHEN: a new notification is posted + val entry = NotificationEntryBuilder().setId(1).build() + collectionListener.onEntryAdded(entry) + testScheduler.runCurrent() + + // WHEN: one second has passed + testScheduler.advanceTimeBy(1.seconds) + testScheduler.runCurrent() + + // WHEN: the notification is removed + collectionListener.onEntryRemoved(entry, 0) + testScheduler.runCurrent() + + // WHEN: the notification is re-posted + collectionListener.onEntryAdded(entry) + testScheduler.runCurrent() + + // WHEN: one more second has passed + testScheduler.advanceTimeBy(1.seconds) + testScheduler.runCurrent() + + // WHEN: the keyguard is no longer showing + keyguardRepository.setKeyguardShowing(false) + keyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + this.testScheduler, + ) + testScheduler.runCurrent() + + // WHEN: Keyguard is shown again + keyguardRepository.setKeyguardShowing(true) + keyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.GONE, + to = KeyguardState.LOCKSCREEN, + this.testScheduler, + ) + testScheduler.runCurrent() + + // THEN: The notification is considered unseen and is not filtered out. + assertThat(unseenFilter.shouldFilterOut(entry, 0L)).isFalse() + } + } + + @Test + fun unseenNotificationOnKeyguardNotMarkedAsSeenIfUpdatedBeforeThreshold() { + // GIVEN: Keyguard is showing, not dozing + keyguardRepository.setKeyguardShowing(true) + keyguardRepository.setIsDozing(false) + runKeyguardCoordinatorTest { + keyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.GONE, + to = KeyguardState.LOCKSCREEN, + this.testScheduler, + ) + testScheduler.runCurrent() + + // WHEN: a new notification is posted + val entry = NotificationEntryBuilder().setId(1).build() + collectionListener.onEntryAdded(entry) + testScheduler.runCurrent() + + // WHEN: one second has passed + testScheduler.advanceTimeBy(1.seconds) + testScheduler.runCurrent() + + // WHEN: the notification is updated + collectionListener.onEntryUpdated(entry) + testScheduler.runCurrent() + + // WHEN: four more seconds have passed + testScheduler.advanceTimeBy(4.seconds) + testScheduler.runCurrent() + + // WHEN: the keyguard is no longer showing + keyguardRepository.setKeyguardShowing(false) + keyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + this.testScheduler, + ) + testScheduler.runCurrent() + + // WHEN: Keyguard is shown again + keyguardRepository.setKeyguardShowing(true) + keyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.GONE, + to = KeyguardState.LOCKSCREEN, + this.testScheduler, + ) + testScheduler.runCurrent() + + // THEN: The notification is considered unseen and is not filtered out. + assertThat(unseenFilter.shouldFilterOut(entry, 0L)).isFalse() + } + } + + private fun runKeyguardCoordinatorTest( + testBlock: suspend KeyguardCoordinatorTestScope.() -> Unit + ) { + val testDispatcher = UnconfinedTestDispatcher() + val testScope = TestScope(testDispatcher) + val fakeSettings = + FakeSettings().apply { + putInt(Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, 1) + } + val seenNotificationsInteractor = + SeenNotificationsInteractor(ActiveNotificationListRepository()) + val keyguardCoordinator = + OriginalUnseenKeyguardCoordinator( + testDispatcher, + mock<DumpManager>(), + headsUpManager, + keyguardRepository, + kosmos.keyguardTransitionInteractor, + KeyguardCoordinatorLogger(logcatLogBuffer()), + testScope.backgroundScope, + fakeSettings, + seenNotificationsInteractor, + statusBarStateController, + ) + keyguardCoordinator.attach(notifPipeline) + testScope.runTest { + KeyguardCoordinatorTestScope( + keyguardCoordinator, + testScope, + seenNotificationsInteractor, + fakeSettings, + ) + .testBlock() + } + } + + private inner class KeyguardCoordinatorTestScope( + private val keyguardCoordinator: OriginalUnseenKeyguardCoordinator, + private val scope: TestScope, + val seenNotificationsInteractor: SeenNotificationsInteractor, + private val fakeSettings: FakeSettings, + ) : CoroutineScope by scope { + val testScheduler: TestCoroutineScheduler + get() = scope.testScheduler + + val unseenFilter: NotifFilter + get() = keyguardCoordinator.unseenNotifFilter + + val collectionListener: NotifCollectionListener = + argumentCaptor { verify(notifPipeline).addCollectionListener(capture()) }.lastValue + + val onHeadsUpChangedListener: OnHeadsUpChangedListener + get() = argumentCaptor { verify(headsUpManager).addListener(capture()) }.lastValue + + var showOnlyUnseenNotifsOnKeyguardSetting: Boolean + get() = + fakeSettings.getIntForUser( + Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, + UserHandle.USER_CURRENT, + ) == 1 + set(value) { + fakeSettings.putIntForUser( + Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, + if (value) 1 else 2, + UserHandle.USER_CURRENT, + ) + } + } + + companion object { + @JvmStatic + @Parameters(name = "{0}") + fun getParams(): List<FlagsParameterization> { + return FlagsParameterization.allCombinationsOf().andSceneContainer() + } + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java index ce134e64bf57..75ecb2c5307c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java @@ -41,8 +41,12 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.communal.shared.model.CommunalScenes; import com.android.systemui.dump.DumpManager; import com.android.systemui.keyguard.WakefulnessLifecycle; +import com.android.systemui.keyguard.shared.model.KeyguardState; +import com.android.systemui.keyguard.shared.model.TransitionState; +import com.android.systemui.keyguard.shared.model.TransitionStep; import com.android.systemui.kosmos.KosmosJavaAdapter; import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.scene.shared.flag.SceneContainerFlag; import com.android.systemui.shade.data.repository.FakeShadeRepository; import com.android.systemui.shade.data.repository.ShadeAnimationRepository; import com.android.systemui.shade.data.repository.ShadeRepository; @@ -91,6 +95,7 @@ public class VisualStabilityCoordinatorTest extends SysuiTestCase { @Mock private HeadsUpManager mHeadsUpManager; @Mock private VisibilityLocationProvider mVisibilityLocationProvider; @Mock private VisualStabilityProvider mVisualStabilityProvider; + @Mock private VisualStabilityCoordinatorLogger mLogger; @Captor private ArgumentCaptor<WakefulnessLifecycle.Observer> mWakefulnessObserverCaptor; @Captor private ArgumentCaptor<StatusBarStateController.StateListener> mSBStateListenerCaptor; @@ -128,7 +133,9 @@ public class VisualStabilityCoordinatorTest extends SysuiTestCase { mVisibilityLocationProvider, mVisualStabilityProvider, mWakefulnessLifecycle, - mKosmos.getCommunalInteractor()); + mKosmos.getCommunalInteractor(), + mKosmos.getKeyguardTransitionInteractor(), + mLogger); mCoordinator.attach(mNotifPipeline); mTestScope.getTestScheduler().runCurrent(); @@ -241,6 +248,38 @@ public class VisualStabilityCoordinatorTest extends SysuiTestCase { } @Test + public void testLockscreenPartlyShowing_groupAndSectionChangesNotAllowed() { + // GIVEN the panel true expanded and device isn't pulsing + setFullyDozed(false); + setSleepy(false); + setLockscreenShowing(0.5f); + setPulsing(false); + + // THEN group changes are NOT allowed + assertFalse(mNotifStabilityManager.isGroupChangeAllowed(mEntry)); + assertFalse(mNotifStabilityManager.isGroupPruneAllowed(mGroupEntry)); + + // THEN section changes are NOT allowed + assertFalse(mNotifStabilityManager.isSectionChangeAllowed(mEntry)); + } + + @Test + public void testLockscreenFullyShowing_groupAndSectionChangesNotAllowed() { + // GIVEN the panel true expanded and device isn't pulsing + setFullyDozed(false); + setSleepy(false); + setLockscreenShowing(1.0f); + setPulsing(false); + + // THEN group changes are NOT allowed + assertFalse(mNotifStabilityManager.isGroupChangeAllowed(mEntry)); + assertFalse(mNotifStabilityManager.isGroupPruneAllowed(mGroupEntry)); + + // THEN section changes are NOT allowed + assertFalse(mNotifStabilityManager.isSectionChangeAllowed(mEntry)); + } + + @Test public void testPulsing_screenOff_groupAndSectionChangesNotAllowed() { // GIVEN the device is pulsing and screen is off setFullyDozed(true); @@ -614,7 +653,37 @@ public class VisualStabilityCoordinatorTest extends SysuiTestCase { } private void setPanelExpanded(boolean expanded) { - mStatusBarStateListener.onExpandedChanged(expanded); + setPanelExpandedAndLockscreenShowing(expanded, /* lockscreenShowing = */ 0.0f); } + private void setLockscreenShowing(float lockscreenShowing) { + setPanelExpandedAndLockscreenShowing(/* panelExpanded = */ false, lockscreenShowing); + } + + private void setPanelExpandedAndLockscreenShowing(boolean panelExpanded, + float lockscreenShowing) { + if (SceneContainerFlag.isEnabled()) { + mStatusBarStateListener.onExpandedChanged(panelExpanded); + mKosmos.getKeyguardTransitionRepository().sendTransitionStepJava( + mTestScope, + makeLockscreenTransitionStep(lockscreenShowing), + /* validateStep = */ false); + } else { + mStatusBarStateListener.onExpandedChanged(panelExpanded || lockscreenShowing > 0.0f); + } + } + + private TransitionStep makeLockscreenTransitionStep(float value) { + if (value <= 0.0f) { + return new TransitionStep(KeyguardState.GONE); + } else if (value >= 1.0f) { + return new TransitionStep(KeyguardState.LOCKSCREEN); + } else { + return new TransitionStep( + KeyguardState.GONE, + KeyguardState.LOCKSCREEN, + value, + TransitionState.RUNNING); + } + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt index 8b4265f552fe..14134ccc34d0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt @@ -33,7 +33,6 @@ import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRo import com.android.systemui.statusbar.notification.data.repository.notificationsKeyguardViewStateRepository import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository -import com.android.systemui.statusbar.notification.stack.data.repository.setNotifications import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModelTest.kt index 5e87f4663d76..61873ad294e3 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModelTest.kt @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.notification.row.ui.viewmodel +import android.app.Notification import android.app.PendingIntent import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -90,7 +91,8 @@ class TimerViewModelTest : SysuiTestCase() { name: String = "example", timeRemaining: Duration = Duration.ofMinutes(3), resumeIntent: PendingIntent? = null, - resetIntent: PendingIntent? = null + addMinuteAction: Notification.Action? = null, + resetAction: Notification.Action? = null ) = TimerContentModel( icon = icon, @@ -99,7 +101,8 @@ class TimerViewModelTest : SysuiTestCase() { Paused( timeRemaining = timeRemaining, resumeIntent = resumeIntent, - resetIntent = resetIntent, + addMinuteAction = addMinuteAction, + resetAction = resetAction, ) ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt index f8e633731c42..f96cf1011fb8 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt @@ -43,7 +43,6 @@ import com.android.systemui.statusbar.notification.data.repository.setActiveNoti import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository -import com.android.systemui.statusbar.notification.stack.data.repository.setNotifications import com.android.systemui.statusbar.policy.data.repository.fakeUserSetupRepository import com.android.systemui.statusbar.policy.data.repository.zenModeRepository import com.android.systemui.statusbar.policy.fakeConfigurationController 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 71cd95f828ef..6f099310f9f7 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 @@ -1096,7 +1096,8 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S ) ) runCurrent() - assertThat(alpha).isEqualTo(0f) + // Resets to 1f after communal scene is hidden + assertThat(alpha).isEqualTo(1f) } @Test @@ -1151,7 +1152,7 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S ) ) runCurrent() - assertThat(alpha).isEqualTo(0f) + assertThat(alpha).isEqualTo(1f) } @Test @@ -1208,7 +1209,8 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S ) ) runCurrent() - assertThat(alpha).isEqualTo(0f) + // Resets to 1f after communal scene is hidden + assertThat(alpha).isEqualTo(1f) } @Test @@ -1263,7 +1265,7 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S ) ) runCurrent() - assertThat(alpha).isEqualTo(0f) + assertThat(alpha).isEqualTo(1f) } private suspend fun TestScope.showLockscreen() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt index b643968c6322..c3c5a48dc197 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt @@ -50,6 +50,7 @@ class ActivityStarterImplTest : SysuiTestCase() { statusBarStateController = statusBarStateController, mainExecutor = mainExecutor, legacyActivityStarter = { legacyActivityStarterInternal }, + activityStarterInternal = { activityStarterInternal }, ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImplTest.kt index 10a2f6407849..d82b9dbfbae8 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImplTest.kt @@ -231,7 +231,6 @@ class LegacyActivityStarterInternalImplTest : SysuiTestCase() { // extra activity options to set on pending intent val activityOptions = mock(ActivityOptions::class.java) activityOptions.splashScreenStyle = SPLASH_SCREEN_STYLE_SOLID_COLOR - activityOptions.isPendingIntentBackgroundActivityLaunchAllowedByPermission = false val bundleCaptor = argumentCaptor<Bundle>() startPendingIntentMaybeDismissingKeyguard( @@ -255,7 +254,8 @@ class LegacyActivityStarterInternalImplTest : SysuiTestCase() { bundleCaptor.capture() ) val options = ActivityOptions.fromBundle(bundleCaptor.firstValue) - assertThat(options.isPendingIntentBackgroundActivityLaunchAllowedByPermission).isFalse() + assertThat(options.getPendingIntentBackgroundActivityStartMode()) + .isEqualTo(ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS) assertThat(options.splashScreenStyle).isEqualTo(SPLASH_SCREEN_STYLE_SOLID_COLOR) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt index 495ab61600f9..8f9da3b2e1e3 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt @@ -180,6 +180,23 @@ class AvalancheControllerTest : SysuiTestCase() { } @Test + fun testDelete_untracked_runnableRuns() { + val headsUpEntry = createHeadsUpEntry(id = 0) + + // None showing + mAvalancheController.headsUpEntryShowing = null + + // Nothing is next + mAvalancheController.clearNext() + + // Delete + mAvalancheController.delete(headsUpEntry, runnableMock!!, "testLabel") + + // Runnable was run + Mockito.verify(runnableMock, Mockito.times(1)).run() + } + + @Test fun testDelete_isNext_removedFromNext_runnableNotRun() { // Entry is next val headsUpEntry = createHeadsUpEntry(id = 0) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.kt index d0ddbffecf9a..5dadc4caf0f6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.kt @@ -33,6 +33,7 @@ import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor import com.android.systemui.statusbar.phone.ConfigurationControllerImpl @@ -42,6 +43,7 @@ import com.android.systemui.testKosmos import com.android.systemui.util.concurrency.DelayableExecutor import com.android.systemui.util.concurrency.mockExecutorHandler import com.android.systemui.util.kotlin.JavaAdapter +import com.android.systemui.util.mockito.mock import com.android.systemui.util.settings.GlobalSettings import com.android.systemui.util.time.SystemClock import junit.framework.Assert @@ -237,6 +239,34 @@ class HeadsUpManagerPhoneTest(flags: FlagsParameterization) : BaseHeadsUpManager } @Test + fun testShowNotification_reorderNotAllowed_notPulsing_seenInShadeTrue() { + whenever(mVSProvider.isReorderingAllowed).thenReturn(false) + val hmp = createHeadsUpManagerPhone() + + val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext) + val row = mock<ExpandableNotificationRow>() + whenever(row.showingPulsing()).thenReturn(false) + notifEntry.row = row + + hmp.showNotification(notifEntry) + Assert.assertTrue(notifEntry.isSeenInShade) + } + + @Test + fun testShowNotification_reorderAllowed_notPulsing_seenInShadeFalse() { + whenever(mVSProvider.isReorderingAllowed).thenReturn(true) + val hmp = createHeadsUpManagerPhone() + + val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext) + val row = mock<ExpandableNotificationRow>() + whenever(row.showingPulsing()).thenReturn(false) + notifEntry.row = row + + hmp.showNotification(notifEntry) + Assert.assertFalse(notifEntry.isSeenInShade) + } + + @Test fun shouldHeadsUpBecomePinned_shadeNotExpanded_true() = testScope.runTest { // GIVEN 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 new file mode 100644 index 000000000000..fdfc7f13abf7 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.android.systemui.statusbar.policy.ui.dialog.viewmodel + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.settingslib.notification.modes.TestModeBuilder +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.statusbar.policy.data.repository.fakeZenModeRepository +import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class ModesDialogViewModelTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + val repository = kosmos.fakeZenModeRepository + val interactor = kosmos.zenModeInteractor + + val underTest = ModesDialogViewModel(context, interactor, kosmos.testDispatcher) + + @Test + fun tiles_filtersOutDisabledModes() = + testScope.runTest { + val tiles by collectLastValue(underTest.tiles) + + repository.addModes( + listOf( + TestModeBuilder().setName("Disabled").setEnabled(false).build(), + TestModeBuilder.MANUAL_DND, + TestModeBuilder() + .setName("Enabled") + .setEnabled(true) + .setManualInvocationAllowed(true) + .build(), + TestModeBuilder() + .setName("Disabled with manual") + .setEnabled(false) + .setManualInvocationAllowed(true) + .build(), + )) + runCurrent() + + assertThat(tiles?.size).isEqualTo(2) + with(tiles?.elementAt(0)!!) { + assertThat(this.text).isEqualTo("Manual DND") + assertThat(this.subtext).isEqualTo("On") + assertThat(this.enabled).isEqualTo(true) + } + with(tiles?.elementAt(1)!!) { + assertThat(this.text).isEqualTo("Enabled") + assertThat(this.subtext).isEqualTo("Off") + assertThat(this.enabled).isEqualTo(false) + } + } + + @Test + fun tiles_filtersOutInactiveModesWithoutManualInvocation() = + testScope.runTest { + val tiles by collectLastValue(underTest.tiles) + + repository.addModes( + listOf( + TestModeBuilder() + .setName("Active without manual") + .setActive(true) + .setManualInvocationAllowed(false) + .build(), + TestModeBuilder() + .setName("Active with manual") + .setTriggerDescription("trigger description") + .setActive(true) + .setManualInvocationAllowed(true) + .build(), + TestModeBuilder() + .setName("Inactive with manual") + .setActive(false) + .setManualInvocationAllowed(true) + .build(), + TestModeBuilder() + .setName("Inactive without manual") + .setActive(false) + .setManualInvocationAllowed(false) + .build(), + )) + runCurrent() + + assertThat(tiles?.size).isEqualTo(3) + with(tiles?.elementAt(0)!!) { + assertThat(this.text).isEqualTo("Active without manual") + assertThat(this.subtext).isEqualTo("On") + assertThat(this.enabled).isEqualTo(true) + } + with(tiles?.elementAt(1)!!) { + assertThat(this.text).isEqualTo("Active with manual") + assertThat(this.subtext).isEqualTo("trigger description") + assertThat(this.enabled).isEqualTo(true) + } + with(tiles?.elementAt(2)!!) { + assertThat(this.text).isEqualTo("Inactive with manual") + assertThat(this.subtext).isEqualTo("Off") + assertThat(this.enabled).isEqualTo(false) + } + } + + @Test + fun onClick_togglesTileState() = + testScope.runTest { + val tiles by collectLastValue(underTest.tiles) + + val modeId = "id" + repository.addModes( + listOf( + TestModeBuilder() + .setId(modeId) + .setName("Test") + .setManualInvocationAllowed(true) + .build() + ) + ) + runCurrent() + + assertThat(tiles?.size).isEqualTo(1) + assertThat(tiles?.elementAt(0)?.enabled).isFalse() + + // Trigger onClick + tiles?.first()?.onClick?.let { it() } + runCurrent() + + assertThat(tiles?.first()?.enabled).isTrue() + + // Trigger onClick + tiles?.first()?.onClick?.let { it() } + runCurrent() + + assertThat(tiles?.first()?.enabled).isFalse() + } +} diff --git a/packages/SystemUI/res/drawable/checkbox_circle_shape.xml b/packages/SystemUI/res/drawable/checkbox_circle_shape.xml new file mode 100644 index 000000000000..2b987e2d1fb1 --- /dev/null +++ b/packages/SystemUI/res/drawable/checkbox_circle_shape.xml @@ -0,0 +1,32 @@ +<?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. + --> +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + <item + android:bottom="12dp" + android:left="12dp" + android:right="12dp" + android:top="12dp"> + <selector> + <item + android:drawable="@drawable/ic_check_circle_filled_24dp" + android:state_checked="true" /> + <item + android:drawable="@drawable/ic_circle_outline_24dp" + android:state_checked="false" /> + </selector> + </item> +</layer-list> diff --git a/packages/SystemUI/res/drawable/ic_check_circle_filled_24dp.xml b/packages/SystemUI/res/drawable/ic_check_circle_filled_24dp.xml new file mode 100644 index 000000000000..16e2a3db2e95 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_check_circle_filled_24dp.xml @@ -0,0 +1,27 @@ +<?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. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:width="24dp" + android:height="24dp" + android:tint="?androidprv:attr/materialColorPrimary" + android:viewportHeight="24" + android:viewportWidth="24"> + <path + android:fillColor="@android:color/white" + android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10c5.52,0 10,-4.48 10,-10S17.52,2 12,2zM10.59,16.6l-4.24,-4.24l1.41,-1.41l2.83,2.83l5.66,-5.66l1.41,1.41L10.59,16.6z" /> +</vector> diff --git a/packages/SystemUI/res/drawable/ic_circle_outline_24dp.xml b/packages/SystemUI/res/drawable/ic_circle_outline_24dp.xml new file mode 100644 index 000000000000..82fa4f08d0c8 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_circle_outline_24dp.xml @@ -0,0 +1,27 @@ +<?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. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:width="24dp" + android:height="24dp" + android:tint="?androidprv:attr/materialColorPrimary" + android:viewportHeight="24" + android:viewportWidth="24"> + <path + android:fillColor="@android:color/white" + android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z" /> +</vector> diff --git a/packages/SystemUI/res/layout/app_clips_screenshot.xml b/packages/SystemUI/res/layout/app_clips_screenshot.xml index 5191895549b6..d7b94ec015ac 100644 --- a/packages/SystemUI/res/layout/app_clips_screenshot.xml +++ b/packages/SystemUI/res/layout/app_clips_screenshot.xml @@ -58,9 +58,10 @@ android:layout_width="wrap_content" android:layout_height="48dp" android:layout_marginStart="16dp" + android:button="@drawable/checkbox_circle_shape" android:checked="true" android:text="@string/backlinks_include_link" - android:textColor="?android:textColorSecondary" + android:textColor="?androidprv:attr/materialColorOnBackground" android:visibility="gone" app:layout_constraintBottom_toTopOf="@id/preview" app:layout_constraintStart_toEndOf="@id/cancel" diff --git a/packages/SystemUI/res/layout/rich_ongoing_timer_notification.xml b/packages/SystemUI/res/layout/rich_ongoing_timer_notification.xml index f2bfbe5c960d..3a679e3c16cb 100644 --- a/packages/SystemUI/res/layout/rich_ongoing_timer_notification.xml +++ b/packages/SystemUI/res/layout/rich_ongoing_timer_notification.xml @@ -33,7 +33,6 @@ android:id="@+id/icon" android:layout_width="24dp" android:layout_height="24dp" - android:src="@drawable/ic_close" app:tint="@android:color/white" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toStartOf="@id/label" @@ -88,11 +87,10 @@ /> <com.android.systemui.statusbar.notification.row.ui.view.TimerButtonView + style="@*android:style/NotificationEmphasizedAction" android:id="@+id/mainButton" android:layout_width="124dp" android:layout_height="wrap_content" - tools:text="Reset" - tools:drawableStart="@android:drawable/ic_menu_add" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toStartOf="@id/altButton" app:layout_constraintTop_toBottomOf="@id/bottomOfTop" @@ -101,15 +99,23 @@ /> <com.android.systemui.statusbar.notification.row.ui.view.TimerButtonView + style="@*android:style/NotificationEmphasizedAction" android:id="@+id/altButton" - tools:text="Reset" - tools:drawableStart="@android:drawable/ic_menu_add" - android:drawablePadding="2dp" - android:drawableTint="@android:color/white" android:layout_width="124dp" android:layout_height="wrap_content" app:layout_constraintTop_toBottomOf="@id/bottomOfTop" app:layout_constraintStart_toEndOf="@id/mainButton" + app:layout_constraintEnd_toEndOf="@id/resetButton" + android:paddingEnd="4dp" + /> + + <com.android.systemui.statusbar.notification.row.ui.view.TimerButtonView + style="@*android:style/NotificationEmphasizedAction" + android:id="@+id/resetButton" + android:layout_width="124dp" + android:layout_height="wrap_content" + app:layout_constraintTop_toBottomOf="@id/bottomOfTop" + app:layout_constraintStart_toEndOf="@id/altButton" app:layout_constraintEnd_toEndOf="parent" android:paddingEnd="4dp" /> diff --git a/packages/SystemUI/res/values/flags.xml b/packages/SystemUI/res/values/flags.xml index 763930db1d55..07a40c86d03a 100644 --- a/packages/SystemUI/res/values/flags.xml +++ b/packages/SystemUI/res/values/flags.xml @@ -33,9 +33,4 @@ <!-- Whether the user switcher chip shows in the status bar. When true, the multi user avatar will no longer show on the lockscreen --> <bool name="flag_user_switcher_chip">false</bool> - - <!-- Whether the battery icon is allowed to display a shield when battery life is being - protected. --> - <bool name="flag_battery_shield_icon">false</bool> - </resources> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 2bd97d9a2f91..68c83c747d73 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -1084,6 +1084,21 @@ <!-- QuickStep: Accessibility to toggle overview [CHAR LIMIT=40] --> <string name="quick_step_accessibility_toggle_overview">Toggle Overview</string> + <!-- Priority modes dialog title [CHAR LIMIT=35] --> + <string name="zen_modes_dialog_title">Priority modes</string> + + <!-- Priority modes dialog confirmation button [CHAR LIMIT=15] --> + <string name="zen_modes_dialog_done">Done</string> + + <!-- Priority modes dialog settings shortcut button [CHAR LIMIT=15] --> + <string name="zen_modes_dialog_settings">Settings</string> + + <!-- Priority modes: label for an active mode [CHAR LIMIT=35] --> + <string name="zen_mode_on">On</string> + + <!-- Priority modes: label for an inactive mode [CHAR LIMIT=35] --> + <string name="zen_mode_off">Off</string> + <!-- Zen mode: Priority only introduction message on first use --> <string name="zen_priority_introduction">You won\'t be disturbed by sounds and vibrations, except from alarms, reminders, events, and callers you specify. You\'ll still hear anything you choose to play including music, videos, and games.</string> @@ -1249,6 +1264,8 @@ <string name="communal_widget_picker_title">Lock screen widgets</string> <!-- Text displayed below the title in the communal widget picker providing additional details about the communal surface. [CHAR LIMIT=80] --> <string name="communal_widget_picker_description">Anyone can view widgets on your lock screen, even if your tablet\'s locked.</string> + <!-- Label for accessibility action to unselect a widget in edit mode. [CHAR LIMIT=NONE] --> + <string name="accessibility_action_label_unselect_widget">unselect widget</string> <!-- Title shown above information regarding lock screen widgets. [CHAR LIMIT=50] --> <string name="communal_widgets_disclaimer_title">Lock screen widgets</string> <!-- Information about lock screen widgets presented to the user. [CHAR LIMIT=NONE] --> @@ -3660,4 +3677,31 @@ <string name="home_controls_dream_description">Quickly access your home controls as a screensaver</string> <!-- Label for volume undo action [CHAR LIMIT=NONE] --> <string name="volume_undo_action">Undo</string> + + <!-- Keyboard touchpad contextual education strings--> + <!-- Education toast text for Back [CHAR_LIMIT=100] --> + <string name="back_edu_toast_content">To go back, swipe left or right with three fingers on the touchpad</string> + <!-- Education toast text for Home [CHAR_LIMIT=100] --> + <string name="home_edu_toast_content">To go home, swipe up with three fingers on the touchpad</string> + <!-- Education toast text for Overview [CHAR_LIMIT=100] --> + <string name="overview_edu_toast_content">To view recent apps, swipe up and hold with three fingers on the touchpad</string> + <!-- Education toast text for All Apps [CHAR_LIMIT=100] --> + <string name="all_apps_edu_toast_content">To view all your apps, press the action key on your keyboard</string> + + <!-- Education notification title for Back [CHAR_LIMIT=100] --> + <string name="back_edu_notification_title">Use your touchpad to go back</string> + <!-- Education notification text for Back [CHAR_LIMIT=100] --> + <string name="back_edu_notification_content">Swipe left or right using three fingers. Tap to learn more gestures.</string> + <!-- Education notification title for Home [CHAR_LIMIT=100] --> + <string name="home_edu_notification_title">Use your touchpad to go home</string> + <!-- Education notification text for Home [CHAR_LIMIT=100] --> + <string name="home_edu_notification_content">Swipe up using three fingers. Tap to learn more gestures.</string> + <!-- Education notification title for Overview [CHAR_LIMIT=100] --> + <string name="overview_edu_notification_title">Use your touchpad to view recent apps</string> + <!-- Education notification text for Overview [CHAR_LIMIT=100] --> + <string name="overview_edu_notification_content">Swipe up and hold using three fingers. Tap to learn more gestures.</string> + <!-- Education notification title for All Apps [CHAR_LIMIT=100] --> + <string name="all_apps_edu_notification_title">Use your keyboard to view all apps</string> + <!-- Education notification text for All Apps [CHAR_LIMIT=100] --> + <string name="all_apps_edu_notification_content">Press the action key at any time. Tap to learn more gestures.</string> </resources> diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java index 845ca5e8b9ec..3019fe796d7d 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java @@ -344,7 +344,7 @@ public class ActivityManagerWrapper { * Shows a voice session identified by {@code token} * @return true if the session was shown, false otherwise */ - public boolean showVoiceSession(@NonNull IBinder token, @NonNull Bundle args, int flags, + public boolean showVoiceSession(IBinder token, @NonNull Bundle args, int flags, @Nullable String attributionTag) { IVoiceInteractionManagerService service = IVoiceInteractionManagerService.Stub.asInterface( ServiceManager.getService(Context.VOICE_INTERACTION_MANAGER_SERVICE)); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java index 10d1891c8405..0f61233ac64f 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java @@ -34,6 +34,7 @@ import com.android.internal.util.LatencyTracker; import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardSecurityModel.SecurityMode; import com.android.keyguard.domain.interactor.KeyguardKeyboardInteractor; +import com.android.systemui.Flags; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.res.R; @@ -130,7 +131,10 @@ public abstract class KeyguardPinBasedInputViewController<T extends KeyguardPinB verifyPasswordAndUnlock(); } }); - okButton.setOnHoverListener(mLiftToActivateListener); + + if (!Flags.simPinTalkbackFixForDoubleSubmit()) { + okButton.setOnHoverListener(mLiftToActivateListener); + } } if (pinInputFieldStyledFocusState()) { collectFlow(mPasswordEntry, mKeyguardKeyboardInteractor.isAnyKeyboardConnected(), diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.java b/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.java index f1fb45c9b16e..baca9594dd2f 100644 --- a/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.java @@ -79,7 +79,8 @@ public class ShadeTouchHandler implements TouchHandler { if (mCapture != null && mCapture) { sendTouchEvent((MotionEvent) ev); } - if (((MotionEvent) ev).getAction() == MotionEvent.ACTION_UP) { + if (((MotionEvent) ev).getAction() == MotionEvent.ACTION_UP + || ((MotionEvent) ev).getAction() == MotionEvent.ACTION_CANCEL) { session.pop(); } } diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java b/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java index 4035e957a2db..efa55e90081e 100644 --- a/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java +++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java @@ -128,8 +128,13 @@ public class TouchMonitor { completer.set(predecessor); } - if (mActiveTouchSessions.isEmpty() && mStopMonitoringPending) { - stopMonitoring(false); + if (mActiveTouchSessions.isEmpty()) { + if (mStopMonitoringPending) { + stopMonitoring(false); + } else { + // restart monitoring to reset any destructive state on the input session + startMonitoring(); + } } }); diff --git a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java index 5c53234f74eb..e63472684585 100644 --- a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java +++ b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java @@ -88,7 +88,6 @@ public class BatteryMeterView extends LinearLayout implements DarkReceiver { private boolean mPowerSaveEnabled; private boolean mIsBatteryDefender; private boolean mIsIncompatibleCharging; - private boolean mDisplayShieldEnabled; // Error state where we know nothing about the current battery state private boolean mBatteryStateUnknown; // Lazily-loaded since this is expected to be a rare-if-ever state @@ -270,7 +269,7 @@ public class BatteryMeterView extends LinearLayout implements DarkReceiver { int resId = 0; if (mPowerSaveEnabled) { resId = R.drawable.battery_unified_attr_powersave; - } else if (mIsBatteryDefender && mDisplayShieldEnabled) { + } else if (mIsBatteryDefender) { resId = R.drawable.battery_unified_attr_defend; } else if (isCharging) { resId = R.drawable.battery_unified_attr_charging; @@ -288,7 +287,7 @@ public class BatteryMeterView extends LinearLayout implements DarkReceiver { private ColorProfile getCurrentColorProfile() { return getColorProfile( mPowerSaveEnabled, - mIsBatteryDefender && mDisplayShieldEnabled, + mIsBatteryDefender, mPluggedIn, mLevel <= 20); } @@ -410,10 +409,6 @@ public class BatteryMeterView extends LinearLayout implements DarkReceiver { mBatteryEstimateFetcher = fetcher; } - void setDisplayShieldEnabled(boolean displayShieldEnabled) { - mDisplayShieldEnabled = displayShieldEnabled; - } - void updatePercentText() { if (!newStatusBarIcons()) { updatePercentTextLegacy(); @@ -659,7 +654,7 @@ public class BatteryMeterView extends LinearLayout implements DarkReceiver { float mainBatteryWidth = res.getDimensionPixelSize(R.dimen.status_bar_battery_icon_width) * iconScaleFactor; - boolean displayShield = mDisplayShieldEnabled && mIsBatteryDefender; + boolean displayShield = mIsBatteryDefender; float fullBatteryIconHeight = BatterySpecs.getFullBatteryHeight(mainBatteryHeight, displayShield); float fullBatteryIconWidth = diff --git a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java index 4f13e6ffd0b5..9a30c213a2f9 100644 --- a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java +++ b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java @@ -34,7 +34,6 @@ import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; -import com.android.systemui.res.R; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.phone.StatusBarLocation; import com.android.systemui.statusbar.phone.ui.StatusBarIconController; @@ -153,8 +152,6 @@ public class BatteryMeterViewController extends ViewController<BatteryMeterView> mBatteryController = batteryController; mView.setBatteryEstimateFetcher(mBatteryController::getEstimatedTimeRemainingString); - mView.setDisplayShieldEnabled( - getContext().getResources().getBoolean(R.bool.flag_battery_shield_icon)); mSlotBattery = getResources().getString(com.android.internal.R.string.status_bar_battery); mSettingObserver = new SettingObserver(mMainHandler); diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt index c95a94e5e388..b10d37e5c27a 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt @@ -34,7 +34,9 @@ import com.android.settingslib.Utils import com.android.systemui.CoreStartable import com.android.systemui.Flags.lightRevealMigration import com.android.systemui.biometrics.data.repository.FacePropertyRepository +import com.android.systemui.biometrics.shared.model.FingerprintSensorType import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams +import com.android.systemui.biometrics.shared.model.toSensorType import com.android.systemui.dagger.SysUISingleton import com.android.systemui.deviceentry.domain.interactor.AuthRippleInteractor import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor @@ -102,6 +104,7 @@ constructor( private var udfpsController: UdfpsController? = null private var udfpsRadius: Float = -1f + private var udfpsType: FingerprintSensorType = FingerprintSensorType.UNKNOWN override fun start() { init() @@ -370,8 +373,11 @@ constructor( private val udfpsControllerCallback = object : UdfpsController.Callback { override fun onFingerDown() { - // only show dwell ripple for device entry - if (keyguardUpdateMonitor.isFingerprintDetectionRunning) { + // only show dwell ripple for device entry non-ultrasonic udfps + if ( + keyguardUpdateMonitor.isFingerprintDetectionRunning && + udfpsType != FingerprintSensorType.UDFPS_ULTRASONIC + ) { showDwellRipple() } } @@ -397,6 +403,7 @@ constructor( if (it.size > 0) { udfpsController = udfpsControllerProvider.get() udfpsRadius = authController.udfpsRadius + udfpsType = it[0].sensorType.toSensorType() if (mView.isAttachedToWindow) { udfpsController?.addCallback(udfpsControllerCallback) diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt b/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt index 85e2bdb43ba5..b6ace81d18ba 100644 --- a/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt +++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt @@ -19,10 +19,14 @@ package com.android.systemui.common.ui.view import android.annotation.SuppressLint import android.content.Context +import android.os.Bundle import android.util.AttributeSet import android.view.MotionEvent import android.view.View import android.view.ViewConfiguration +import android.view.accessibility.AccessibilityNodeInfo +import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat import com.android.systemui.shade.TouchLogger import kotlin.math.pow import kotlin.math.sqrt @@ -44,6 +48,10 @@ class LongPressHandlingView( attrs, ) { + init { + setupAccessibilityDelegate() + } + constructor( context: Context, attrs: AttributeSet?, @@ -55,6 +63,7 @@ class LongPressHandlingView( view: View, x: Int, y: Int, + isA11yAction: Boolean = false, ) /** Notifies that the gesture was too short for a long press, it is actually a click. */ @@ -63,6 +72,8 @@ class LongPressHandlingView( var listener: Listener? = null + var accessibilityHintLongPressAction: AccessibilityAction? = null + private val interactionHandler: LongPressHandlingViewInteractionHandler by lazy { LongPressHandlingViewInteractionHandler( postDelayed = { block, timeoutMs -> @@ -107,6 +118,51 @@ class LongPressHandlingView( override fun onTouchEvent(event: MotionEvent?): Boolean { return interactionHandler.onTouchEvent(event?.toModel()) } + + private fun setupAccessibilityDelegate() { + accessibilityDelegate = + object : AccessibilityDelegate() { + override fun onInitializeAccessibilityNodeInfo( + v: View, + info: AccessibilityNodeInfo + ) { + super.onInitializeAccessibilityNodeInfo(v, info) + if ( + interactionHandler.isLongPressHandlingEnabled && + accessibilityHintLongPressAction != null + ) { + info.addAction(accessibilityHintLongPressAction) + } + } + + override fun performAccessibilityAction( + host: View, + action: Int, + args: Bundle? + ): Boolean { + return if ( + interactionHandler.isLongPressHandlingEnabled && + action == AccessibilityNodeInfoCompat.ACTION_LONG_CLICK + ) { + val longPressHandlingView = host as? LongPressHandlingView + if (longPressHandlingView != null) { + // the coordinates are not available as it is an a11y long press + listener?.onLongPressDetected( + view = longPressHandlingView, + x = 0, + y = 0, + isA11yAction = true, + ) + true + } else { + false + } + } else { + super.performAccessibilityAction(host, action, args) + } + } + } + } } private fun MotionEvent.toModel(): LongPressHandlingViewInteractionHandler.MotionEventModel { diff --git a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt index 3d201a36417b..a44533555a04 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt @@ -17,6 +17,7 @@ package com.android.systemui.communal.dagger import android.content.Context +import com.android.systemui.CoreStartable import com.android.systemui.communal.data.backup.CommunalBackupUtils import com.android.systemui.communal.data.db.CommunalDatabaseModule import com.android.systemui.communal.data.repository.CommunalMediaRepositoryModule @@ -26,6 +27,7 @@ import com.android.systemui.communal.data.repository.CommunalSettingsRepositoryM import com.android.systemui.communal.data.repository.CommunalSmartspaceRepositoryModule import com.android.systemui.communal.data.repository.CommunalTutorialRepositoryModule import com.android.systemui.communal.data.repository.CommunalWidgetRepositoryModule +import com.android.systemui.communal.domain.interactor.CommunalSceneTransitionInteractor import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.communal.util.CommunalColors import com.android.systemui.communal.util.CommunalColorsImpl @@ -40,6 +42,8 @@ import com.android.systemui.scene.shared.model.SceneDataSourceDelegator import dagger.Binds import dagger.Module import dagger.Provides +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap import kotlinx.coroutines.CoroutineScope @Module( @@ -69,6 +73,13 @@ interface CommunalModule { @Binds fun bindCommunalColors(impl: CommunalColorsImpl): CommunalColors + @Binds + @IntoMap + @ClassKey(CommunalSceneTransitionInteractor::class) + abstract fun bindCommunalSceneTransitionInteractor( + impl: CommunalSceneTransitionInteractor + ): CoreStartable + companion object { @Provides @Communal diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalPrefsRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalPrefsRepository.kt index d8067b887c67..4de39c457f3b 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalPrefsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalPrefsRepository.kt @@ -49,14 +49,8 @@ interface CommunalPrefsRepository { /** Whether the CTA tile has been dismissed. */ fun isCtaDismissed(user: UserInfo): Flow<Boolean> - /** Whether the lock screen widget disclaimer has been dismissed by the user. */ - fun isDisclaimerDismissed(user: UserInfo): Flow<Boolean> - /** Save the CTA tile dismissed state for the current user. */ suspend fun setCtaDismissed(user: UserInfo) - - /** Save the lock screen widget disclaimer dismissed state for the current user. */ - suspend fun setDisclaimerDismissed(user: UserInfo) } @OptIn(ExperimentalCoroutinesApi::class) @@ -74,9 +68,6 @@ constructor( override fun isCtaDismissed(user: UserInfo): Flow<Boolean> = readKeyForUser(user, CTA_DISMISSED_STATE) - override fun isDisclaimerDismissed(user: UserInfo): Flow<Boolean> = - readKeyForUser(user, DISCLAIMER_DISMISSED_STATE) - /** * Emits an event each time a Backup & Restore restoration job is completed, and once at the * start of collection. @@ -97,12 +88,6 @@ constructor( logger.i("Dismissed CTA tile") } - override suspend fun setDisclaimerDismissed(user: UserInfo) = - withContext(bgDispatcher) { - getSharedPrefsForUser(user).edit().putBoolean(DISCLAIMER_DISMISSED_STATE, true).apply() - logger.i("Dismissed widget disclaimer") - } - private fun getSharedPrefsForUser(user: UserInfo): SharedPreferences { return userFileManager.getSharedPreferences( FILE_NAME, @@ -124,6 +109,5 @@ constructor( const val TAG = "CommunalPrefsRepository" const val FILE_NAME = "communal_hub_prefs" const val CTA_DISMISSED_STATE = "cta_dismissed" - const val DISCLAIMER_DISMISSED_STATE = "disclaimer_dismissed" } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt index 7a4006d515f7..260dcbad6201 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt @@ -28,7 +28,6 @@ import com.android.systemui.scene.shared.model.SceneDataSource import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -52,7 +51,7 @@ interface CommunalSceneRepository { fun changeScene(toScene: SceneKey, transitionKey: TransitionKey? = null) /** Immediately snaps to the desired scene. */ - fun snapToScene(toScene: SceneKey, delayMillis: Long = 0) + fun snapToScene(toScene: SceneKey) /** * Updates the transition state of the hub [SceneTransitionLayout]. @@ -93,11 +92,10 @@ constructor( } } - override fun snapToScene(toScene: SceneKey, delayMillis: Long) { + override fun snapToScene(toScene: SceneKey) { applicationScope.launch { // SceneTransitionLayout state updates must be triggered on the thread the STL was // created on. - delay(delayMillis) sceneDataSource.snapToScene(toScene) } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneTransitionRepository.kt new file mode 100644 index 000000000000..7d9e1df41e6f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneTransitionRepository.kt @@ -0,0 +1,32 @@ +/* + * 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.data.repository + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.shared.model.KeyguardState +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow + +@SysUISingleton +class CommunalSceneTransitionRepository @Inject constructor() { + /** + * This [KeyguardState] will indicate which sub state within KTF should be navigated to when the + * next transition away from communal scene is started. It will be consumed exactly once and + * after that the state will be set back to null. + */ + val nextLockscreenTargetState: MutableStateFlow<KeyguardState?> = MutableStateFlow(null) +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt index 3fffd76ab6a9..e13161f91f16 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt @@ -23,6 +23,7 @@ import android.content.pm.UserInfo import android.os.UserHandle import android.os.UserManager import android.provider.Settings +import com.android.app.tracing.coroutines.launch import com.android.compose.animation.scene.ObservableTransitionState import com.android.compose.animation.scene.SceneKey import com.android.compose.animation.scene.TransitionKey @@ -64,10 +65,12 @@ import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf import com.android.systemui.util.kotlin.BooleanFlowOperators.not import com.android.systemui.util.kotlin.emitOnStart import javax.inject.Inject +import kotlin.time.Duration.Companion.minutes import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -94,6 +97,7 @@ class CommunalInteractor @Inject constructor( @Application val applicationScope: CoroutineScope, + @Background private val bgScope: CoroutineScope, @Background val bgDispatcher: CoroutineDispatcher, broadcastDispatcher: BroadcastDispatcher, private val widgetRepository: CommunalWidgetRepository, @@ -148,6 +152,17 @@ constructor( replay = 1, ) + private val _isDisclaimerDismissed = MutableStateFlow(false) + val isDisclaimerDismissed: Flow<Boolean> = _isDisclaimerDismissed.asStateFlow() + + fun setDisclaimerDismissed() { + bgScope.launch("$TAG#setDisclaimerDismissed") { + _isDisclaimerDismissed.value = true + delay(DISCLAIMER_RESET_MILLIS) + _isDisclaimerDismissed.value = false + } + } + /** Whether to show communal when exiting the occluded state. */ val showCommunalFromOccluded: Flow<Boolean> = keyguardTransitionInteractor.startedKeyguardTransitionStep @@ -510,6 +525,14 @@ constructor( } companion object { + const val TAG = "CommunalInteractor" + + /** + * The amount of time between showing the widget disclaimer to the user as measured from the + * moment the disclaimer is dimsissed. + */ + val DISCLAIMER_RESET_MILLIS = 30.minutes + /** * The user activity timeout which should be used when the communal hub is opened. A value * of -1 means that the user's chosen screen timeout will be used instead. diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalPrefsInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalPrefsInteractor.kt index 3517650c8cf3..0b5f40d8041e 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalPrefsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalPrefsInteractor.kt @@ -17,7 +17,6 @@ package com.android.systemui.communal.domain.interactor import android.content.pm.UserInfo -import com.android.app.tracing.coroutines.launch import com.android.systemui.communal.data.repository.CommunalPrefsRepository import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background @@ -43,7 +42,7 @@ constructor( private val repository: CommunalPrefsRepository, userInteractor: SelectedUserInteractor, private val userTracker: UserTracker, - @CommunalTableLog tableLogBuffer: TableLogBuffer + @CommunalTableLog tableLogBuffer: TableLogBuffer, ) { val isCtaDismissed: Flow<Boolean> = @@ -64,25 +63,6 @@ constructor( suspend fun setCtaDismissed(user: UserInfo = userTracker.userInfo) = repository.setCtaDismissed(user) - val isDisclaimerDismissed: Flow<Boolean> = - userInteractor.selectedUserInfo - .flatMapLatest { user -> repository.isDisclaimerDismissed(user) } - .logDiffsForTable( - tableLogBuffer = tableLogBuffer, - columnPrefix = "", - columnName = "isDisclaimerDismissed", - initialValue = false, - ) - .stateIn( - scope = bgScope, - started = SharingStarted.WhileSubscribed(), - initialValue = false, - ) - - fun setDisclaimerDismissed(user: UserInfo = userTracker.userInfo) { - bgScope.launch("$TAG#setDisclaimerDismissed") { repository.setDisclaimerDismissed(user) } - } - private companion object { const val TAG = "CommunalPrefsInteractor" } diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt index 122f964713a9..aa9cbd047bbf 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt @@ -16,6 +16,7 @@ package com.android.systemui.communal.domain.interactor +import com.android.app.tracing.coroutines.launch import com.android.compose.animation.scene.ObservableTransitionState import com.android.compose.animation.scene.SceneKey import com.android.compose.animation.scene.TransitionKey @@ -26,9 +27,11 @@ import com.android.systemui.communal.shared.model.CommunalTransitionKeys import com.android.systemui.communal.shared.model.EditModeState import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyguard.shared.model.KeyguardState import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -39,6 +42,7 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch @OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton @@ -57,17 +61,48 @@ constructor( _isLaunchingWidget.value = launching } + fun interface OnSceneAboutToChangeListener { + /** Notifies that the scene is about to change to [toScene]. */ + fun onSceneAboutToChange(toScene: SceneKey, keyguardState: KeyguardState?) + } + + private val onSceneAboutToChangeListener = mutableSetOf<OnSceneAboutToChangeListener>() + + /** Registers a listener which is called when the scene is about to change. */ + fun registerSceneStateProcessor(processor: OnSceneAboutToChangeListener) { + onSceneAboutToChangeListener.add(processor) + } + /** * Asks for an asynchronous scene witch to [newScene], which will use the corresponding * installed transition or the one specified by [transitionKey], if provided. */ - fun changeScene(newScene: SceneKey, transitionKey: TransitionKey? = null) { - communalSceneRepository.changeScene(newScene, transitionKey) + fun changeScene( + newScene: SceneKey, + transitionKey: TransitionKey? = null, + keyguardState: KeyguardState? = null, + ) { + applicationScope.launch { + notifyListeners(newScene, keyguardState) + communalSceneRepository.changeScene(newScene, transitionKey) + } } /** Immediately snaps to the new scene. */ - fun snapToScene(newScene: SceneKey, delayMillis: Long = 0) { - communalSceneRepository.snapToScene(newScene, delayMillis) + fun snapToScene( + newScene: SceneKey, + delayMillis: Long = 0, + keyguardState: KeyguardState? = null + ) { + applicationScope.launch("$TAG#snapToScene") { + delay(delayMillis) + notifyListeners(newScene, keyguardState) + communalSceneRepository.snapToScene(newScene) + } + } + + private fun notifyListeners(newScene: SceneKey, keyguardState: KeyguardState?) { + onSceneAboutToChangeListener.forEach { it.onSceneAboutToChange(newScene, keyguardState) } } /** Changes to Blank scene when starting an activity after dismissing keyguard. */ @@ -164,4 +199,8 @@ constructor( started = SharingStarted.WhileSubscribed(), initialValue = false, ) + + private companion object { + const val TAG = "CommunalSceneInteractor" + } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt new file mode 100644 index 000000000000..8351566fcae6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt @@ -0,0 +1,293 @@ +/* + * 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.domain.interactor + +import com.android.compose.animation.scene.ObservableTransitionState +import com.android.compose.animation.scene.SceneKey +import com.android.compose.animation.scene.SceneTransitionLayout +import com.android.systemui.CoreStartable +import com.android.systemui.Flags.communalSceneKtfRefactor +import com.android.systemui.communal.data.repository.CommunalSceneTransitionRepository +import com.android.systemui.communal.shared.model.CommunalScenes +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyguard.domain.interactor.InternalKeyguardTransitionInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionInfo +import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.scene.shared.flag.SceneContainerFlag +import com.android.systemui.util.kotlin.pairwise +import java.util.UUID +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch + +/** + * This class listens to [SceneTransitionLayout] transitions and manages keyguard transition + * framework (KTF) states accordingly for communal states. + * + * There are a few rules: + * - There are only 2 communal scenes: [CommunalScenes.Communal] and [CommunalScenes.Blank] + * - When scene framework is on [CommunalScenes.Blank], KTF is allowed to change its scenes freely + * - When scene framework is on [CommunalScenes.Communal], KTF is locked into + * [KeyguardState.GLANCEABLE_HUB] + */ +@SysUISingleton +class CommunalSceneTransitionInteractor +@Inject +constructor( + val transitionInteractor: KeyguardTransitionInteractor, + val internalTransitionInteractor: InternalKeyguardTransitionInteractor, + private val settingsInteractor: CommunalSettingsInteractor, + @Application private val applicationScope: CoroutineScope, + private val sceneInteractor: CommunalSceneInteractor, + private val repository: CommunalSceneTransitionRepository, + keyguardInteractor: KeyguardInteractor, +) : CoreStartable, CommunalSceneInteractor.OnSceneAboutToChangeListener { + + private var currentTransitionId: UUID? = null + private var progressJob: Job? = null + + private val currentToState: KeyguardState + get() = internalTransitionInteractor.currentTransitionInfoInternal.value.to + + /** + * The next keyguard state to trigger when exiting [CommunalScenes.Communal]. This is only used + * if the state is changed by user gesture or not explicitly defined by the caller when changing + * scenes programmatically. + * + * This is needed because we do not always want to exit back to the KTF state we came from. For + * example, when going from HUB (Communal) -> OCCLUDED (Blank) -> HUB (Communal) and then + * closing the hub via gesture, we don't want to go back to OCCLUDED but instead either go to + * DREAM or LOCKSCREEN depending on if there is a dream showing. + */ + private val nextKeyguardStateInternal = + combine( + keyguardInteractor.isDreaming, + keyguardInteractor.isKeyguardOccluded, + keyguardInteractor.isKeyguardGoingAway, + ) { dreaming, occluded, keyguardGoingAway -> + if (keyguardGoingAway) { + KeyguardState.GONE + } else if (dreaming) { + KeyguardState.DREAMING + } else if (occluded) { + KeyguardState.OCCLUDED + } else { + KeyguardState.LOCKSCREEN + } + } + + private val nextKeyguardState: StateFlow<KeyguardState> = + combine( + repository.nextLockscreenTargetState, + nextKeyguardStateInternal, + ) { override, nextState -> + override ?: nextState + } + .stateIn( + scope = applicationScope, + started = SharingStarted.Eagerly, + initialValue = KeyguardState.LOCKSCREEN, + ) + + override fun start() { + if ( + communalSceneKtfRefactor() && + settingsInteractor.isCommunalFlagEnabled() && + !SceneContainerFlag.isEnabled + ) { + sceneInteractor.registerSceneStateProcessor(this) + listenForSceneTransitionProgress() + } + } + + /** + * Called when the scene is programmatically changed, allowing callers to specify which KTF + * state should be set when transitioning to [CommunalScenes.Blank] + */ + override fun onSceneAboutToChange(toScene: SceneKey, keyguardState: KeyguardState?) { + if (toScene != CommunalScenes.Blank || keyguardState == null) return + repository.nextLockscreenTargetState.value = keyguardState + } + + /** Monitors [SceneTransitionLayout] state and updates KTF state accordingly. */ + private fun listenForSceneTransitionProgress() { + applicationScope.launch { + sceneInteractor.transitionState + .pairwise(ObservableTransitionState.Idle(CommunalScenes.Blank)) + .collect { (prevTransition, transition) -> + when (transition) { + is ObservableTransitionState.Idle -> handleIdle(prevTransition, transition) + is ObservableTransitionState.Transition -> + handleTransition(prevTransition, transition) + } + } + } + } + + private suspend fun handleIdle( + prevTransition: ObservableTransitionState, + idle: ObservableTransitionState.Idle + ) { + if ( + prevTransition is ObservableTransitionState.Transition && + currentTransitionId != null && + idle.currentScene == prevTransition.toScene + ) { + finishCurrentTransition() + } else { + // We may receive an Idle event without a corresponding Transition + // event, such as when snapping to a scene without an animation. + val targetState = + if (idle.currentScene == CommunalScenes.Blank) { + nextKeyguardState.value + } else { + KeyguardState.GLANCEABLE_HUB + } + transitionKtfTo(targetState) + repository.nextLockscreenTargetState.value = null + } + } + + private fun finishCurrentTransition() { + internalTransitionInteractor.updateTransition( + currentTransitionId!!, + 1f, + TransitionState.FINISHED + ) + resetTransitionData() + } + + private suspend fun finishReversedTransitionTo(state: KeyguardState) { + val newTransition = + TransitionInfo( + ownerName = this::class.java.simpleName, + from = internalTransitionInteractor.currentTransitionInfoInternal.value.to, + to = state, + animator = null, + modeOnCanceled = TransitionModeOnCanceled.REVERSE + ) + currentTransitionId = internalTransitionInteractor.startTransition(newTransition) + internalTransitionInteractor.updateTransition( + currentTransitionId!!, + 1f, + TransitionState.FINISHED + ) + resetTransitionData() + } + + private fun resetTransitionData() { + progressJob?.cancel() + progressJob = null + currentTransitionId = null + } + + private suspend fun handleTransition( + prevTransition: ObservableTransitionState, + transition: ObservableTransitionState.Transition + ) { + if (prevTransition.isTransitioning(from = transition.fromScene, to = transition.toScene)) { + // This is a new transition, but exactly the same as the previous state. Skip resetting + // KTF for this case and just collect the new progress instead. + collectProgress(transition) + } else if (transition.toScene == CommunalScenes.Communal) { + if (currentTransitionId != null) { + if (currentToState == KeyguardState.GLANCEABLE_HUB) { + transitionKtfTo(transitionInteractor.getStartedFromState()) + } + } + startTransitionToGlanceableHub() + collectProgress(transition) + } else if (transition.toScene == CommunalScenes.Blank) { + if (currentTransitionId != null) { + // Another transition started before this one is completed. Transition to the + // GLANCEABLE_HUB state so that we can properly transition away from it. + transitionKtfTo(KeyguardState.GLANCEABLE_HUB) + } + startTransitionFromGlanceableHub() + collectProgress(transition) + } + } + + private suspend fun transitionKtfTo(state: KeyguardState) { + val currentTransition = transitionInteractor.transitionState.value + if (currentTransition.isFinishedIn(state)) { + // This is already the state we want to be in + resetTransitionData() + } else if (currentTransition.isTransitioning(to = state)) { + finishCurrentTransition() + } else { + finishReversedTransitionTo(state) + } + } + + private fun collectProgress(transition: ObservableTransitionState.Transition) { + progressJob?.cancel() + progressJob = applicationScope.launch { transition.progress.collect { updateProgress(it) } } + } + + private suspend fun startTransitionFromGlanceableHub() { + val newTransition = + TransitionInfo( + ownerName = this::class.java.simpleName, + from = KeyguardState.GLANCEABLE_HUB, + to = nextKeyguardState.value, + animator = null, + modeOnCanceled = TransitionModeOnCanceled.RESET, + ) + repository.nextLockscreenTargetState.value = null + startTransition(newTransition) + } + + private suspend fun startTransitionToGlanceableHub() { + val currentState = internalTransitionInteractor.currentTransitionInfoInternal.value.to + val newTransition = + TransitionInfo( + ownerName = this::class.java.simpleName, + from = currentState, + to = KeyguardState.GLANCEABLE_HUB, + animator = null, + modeOnCanceled = TransitionModeOnCanceled.RESET, + ) + startTransition(newTransition) + } + + private suspend fun startTransition(transitionInfo: TransitionInfo) { + if (currentTransitionId != null) { + resetTransitionData() + } + currentTransitionId = internalTransitionInteractor.startTransition(transitionInfo) + } + + private fun updateProgress(progress: Float) { + if (currentTransitionId == null) return + internalTransitionInteractor.updateTransition( + currentTransitionId!!, + progress.coerceIn(0f, 1f), + TransitionState.RUNNING + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt index 19d7ceba2310..01ed2b71bea0 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt @@ -27,6 +27,7 @@ import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor import com.android.systemui.communal.domain.model.CommunalContentModel import com.android.systemui.communal.shared.model.EditModeState import com.android.systemui.communal.widgets.WidgetConfigurator +import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.media.controls.ui.view.MediaHost import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -75,8 +76,16 @@ abstract class BaseCommunalViewModel( communalInteractor.signalUserInteraction() } - fun changeScene(scene: SceneKey, transitionKey: TransitionKey? = null) { - communalSceneInteractor.changeScene(scene, transitionKey) + /** + * Asks for an asynchronous scene witch to [newScene], which will use the corresponding + * installed transition or the one specified by [transitionKey], if provided. + */ + fun changeScene( + scene: SceneKey, + transitionKey: TransitionKey? = null, + keyguardState: KeyguardState? = null + ) { + communalSceneInteractor.changeScene(scene, transitionKey, keyguardState) } fun setEditModeState(state: EditModeState?) = communalSceneInteractor.setEditModeState(state) diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt index 7b0aadfdcebd..0353d2c043e8 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt @@ -82,10 +82,10 @@ constructor( communalSceneInteractor.editModeState.map { it == EditModeState.SHOWING } val showDisclaimer: Flow<Boolean> = - allOf(isCommunalContentVisible, not(communalPrefsInteractor.isDisclaimerDismissed)) + allOf(isCommunalContentVisible, not(communalInteractor.isDisclaimerDismissed)) fun onDisclaimerDismissed() { - communalPrefsInteractor.setDisclaimerDismissed() + communalInteractor.setDisclaimerDismissed() } /** diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/DetailDialog.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/DetailDialog.kt index 7f8103e63684..6864f4e96994 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/DetailDialog.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/DetailDialog.kt @@ -18,7 +18,7 @@ package com.android.systemui.controls.ui import android.app.Activity import android.app.ActivityOptions -import android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED +import android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS import android.app.Dialog import android.app.PendingIntent import android.content.ComponentName @@ -93,8 +93,8 @@ class DetailDialog( 0 /* enterResId */, 0 /* exitResId */ ).apply { - pendingIntentBackgroundActivityStartMode = MODE_BACKGROUND_ACTIVITY_START_ALLOWED - isPendingIntentBackgroundActivityLaunchAllowedByPermission = true + pendingIntentBackgroundActivityStartMode = + MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS taskAlwaysOnTop = true } diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepository.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepository.kt index e2ad7741557f..3f937bba46d4 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepository.kt @@ -13,8 +13,10 @@ import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.withContext @@ -25,7 +27,7 @@ interface DeviceEntryRepository { * chosen any secure authentication method and even if they set the lockscreen to be dismissed * when the user swipes on it. */ - suspend fun isLockscreenEnabled(): Boolean + val isLockscreenEnabled: StateFlow<Boolean> /** * Whether lockscreen bypass is enabled. When enabled, the lockscreen will be automatically @@ -39,6 +41,13 @@ interface DeviceEntryRepository { * the lockscreen. */ val isBypassEnabled: StateFlow<Boolean> + + /** + * Whether the lockscreen is enabled for the current user. This is `true` whenever the user has + * chosen any secure authentication method and even if they set the lockscreen to be dismissed + * when the user swipes on it. + */ + suspend fun isLockscreenEnabled(): Boolean } /** Encapsulates application state for device entry. */ @@ -53,12 +62,8 @@ constructor( private val keyguardBypassController: KeyguardBypassController, ) : DeviceEntryRepository { - override suspend fun isLockscreenEnabled(): Boolean { - return withContext(backgroundDispatcher) { - val selectedUserId = userRepository.getSelectedUserInfo().id - !lockPatternUtils.isLockScreenDisabled(selectedUserId) - } - } + private val _isLockscreenEnabled = MutableStateFlow(true) + override val isLockscreenEnabled: StateFlow<Boolean> = _isLockscreenEnabled.asStateFlow() override val isBypassEnabled: StateFlow<Boolean> = conflatedCallbackFlow { @@ -78,6 +83,15 @@ constructor( SharingStarted.Eagerly, initialValue = keyguardBypassController.bypassEnabled, ) + + override suspend fun isLockscreenEnabled(): Boolean { + return withContext(backgroundDispatcher) { + val selectedUserId = userRepository.getSelectedUserInfo().id + val isEnabled = !lockPatternUtils.isLockScreenDisabled(selectedUserId) + _isLockscreenEnabled.value = isEnabled + isEnabled + } + } } @Module diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt index ea0e59bb6ccc..9b95ac4797c0 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt @@ -28,12 +28,14 @@ import com.android.systemui.utils.coroutines.flow.mapLatestConflated import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -101,6 +103,10 @@ constructor( initialValue = false, ) + val isLockscreenEnabled: Flow<Boolean> by lazy { + repository.isLockscreenEnabled.onStart { refreshLockscreenEnabled() } + } + /** * Whether it's currently possible to swipe up to enter the device without requiring * authentication or when the device is already authenticated using a passive authentication @@ -115,14 +121,14 @@ constructor( */ val canSwipeToEnter: StateFlow<Boolean?> = combine( - // This is true when the user has chosen to show the lockscreen but has not made it - // secure. authenticationInteractor.authenticationMethod.map { - it == AuthenticationMethodModel.None && repository.isLockscreenEnabled() + it == AuthenticationMethodModel.None }, + isLockscreenEnabled, deviceUnlockedInteractor.deviceUnlockStatus, isDeviceEntered - ) { isSwipeAuthMethod, deviceUnlockStatus, isDeviceEntered -> + ) { isNoneAuthMethod, isLockscreenEnabled, deviceUnlockStatus, isDeviceEntered -> + val isSwipeAuthMethod = isNoneAuthMethod && isLockscreenEnabled (isSwipeAuthMethod || (deviceUnlockStatus.isUnlocked && deviceUnlockStatus.deviceUnlockSource?.dismissesLockscreen == false)) && @@ -186,6 +192,17 @@ constructor( } /** + * Forces a refresh of the value of [isLockscreenEnabled] such that the flow emits the latest + * value. + * + * Without calling this method, the flow will have a stale value unless the collector is removed + * and re-added. + */ + suspend fun refreshLockscreenEnabled() { + isLockscreenEnabled() + } + + /** * Whether lockscreen bypass is enabled. When enabled, the lockscreen will be automatically * dismissed once the authentication challenge is completed. For example, completing a biometric * authentication challenge via face unlock or fingerprint sensor can automatically bypass the diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt index 3294c816d67c..b45ebd865c55 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt @@ -256,6 +256,7 @@ constructor( it.cancel() null } + mOverlayStateController.setExitAnimationsRunning(false) } private fun blurAnimator( 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 53b9261991e0..0e2e2e6277f2 100644 --- a/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt +++ b/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt @@ -16,12 +16,21 @@ 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.education.data.repository.ContextualEducationRepository import com.android.systemui.education.data.repository.ContextualEducationRepositoryImpl +import com.android.systemui.education.domain.interactor.ContextualEducationInteractor +import com.android.systemui.education.domain.interactor.KeyboardTouchpadEduStatsInteractor +import com.android.systemui.education.domain.interactor.KeyboardTouchpadEduStatsInteractorImpl +import com.android.systemui.shared.education.GestureType import dagger.Binds +import dagger.Lazy import dagger.Module import dagger.Provides +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap import java.time.Clock import javax.inject.Qualifier import kotlinx.coroutines.CoroutineDispatcher @@ -53,5 +62,41 @@ interface ContextualEducationModule { fun provideEduClock(): Clock { return Clock.systemUTC() } + + @Provides + @IntoMap + @ClassKey(ContextualEducationInteractor::class) + fun provideContextualEducationInteractor( + implLazy: Lazy<ContextualEducationInteractor> + ): CoreStartable { + return if (Flags.keyboardTouchpadContextualEducation()) { + implLazy.get() + } else { + // No-op implementation when the flag is disabled. + return NoOpCoreStartable + } + } + + @Provides + fun provideKeyboardTouchpadEduStatsInteractor( + implLazy: Lazy<KeyboardTouchpadEduStatsInteractorImpl> + ): KeyboardTouchpadEduStatsInteractor { + return if (Flags.keyboardTouchpadContextualEducation()) { + implLazy.get() + } else { + // No-op implementation when the flag is disabled. + return NoOpKeyboardTouchpadEduStatsInteractor + } + } + } + + private object NoOpKeyboardTouchpadEduStatsInteractor : KeyboardTouchpadEduStatsInteractor { + override fun incrementSignalCount(gestureType: GestureType) {} + + override fun updateShortcutTriggerTime(gestureType: GestureType) {} + } + + private object NoOpCoreStartable : CoreStartable { + override fun start() {} } } 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 new file mode 100644 index 000000000000..e2aa9111246b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractor.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.education.domain.interactor + +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.education.data.repository.ContextualEducationRepository +import com.android.systemui.shared.education.GestureType +import com.android.systemui.user.domain.interactor.SelectedUserInteractor +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +/** + * Allows updating education data (e.g. signal count, shortcut time) for different gesture types. + * Change user education repository when user is changed. + */ +@SysUISingleton +class ContextualEducationInteractor +@Inject +constructor( + @Background private val backgroundScope: CoroutineScope, + private val selectedUserInteractor: SelectedUserInteractor, + private val repository: ContextualEducationRepository, +) : CoreStartable { + + override fun start() { + backgroundScope.launch { + selectedUserInteractor.selectedUser.collectLatest { repository.setUser(it) } + } + } + + suspend fun incrementSignalCount(gestureType: GestureType) = + repository.incrementSignalCount(gestureType) + + suspend fun updateShortcutTriggerTime(gestureType: GestureType) = + repository.updateShortcutTriggerTime(gestureType) +} diff --git a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduStatsInteractor.kt b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduStatsInteractor.kt new file mode 100644 index 000000000000..643e571d2927 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduStatsInteractor.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.education.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.shared.education.GestureType +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +/** + * Encapsulates the update functions of KeyboardTouchpadEduStatsInteractor. This encapsulation is + * for having a different implementation of interactor when the feature flag is off. + */ +interface KeyboardTouchpadEduStatsInteractor { + fun incrementSignalCount(gestureType: GestureType) + + fun updateShortcutTriggerTime(gestureType: GestureType) +} + +/** Allow update to education data related to keyboard/touchpad. */ +@SysUISingleton +class KeyboardTouchpadEduStatsInteractorImpl +@Inject +constructor( + @Background private val backgroundScope: CoroutineScope, + private val contextualEducationInteractor: ContextualEducationInteractor +) : KeyboardTouchpadEduStatsInteractor { + + override fun incrementSignalCount(gestureType: GestureType) { + // Todo: check if keyboard/touchpad is connected before update + backgroundScope.launch { contextualEducationInteractor.incrementSignalCount(gestureType) } + } + + override fun updateShortcutTriggerTime(gestureType: GestureType) { + backgroundScope.launch { + contextualEducationInteractor.updateShortcutTriggerTime(gestureType) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt index af7ecf66d107..0e06117b2693 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt @@ -28,6 +28,8 @@ 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 @@ -35,6 +37,8 @@ import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefac import com.android.systemui.statusbar.notification.interruption.VisualInterruptionRefactor import com.android.systemui.statusbar.notification.shared.NotificationAvalancheSuppression import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor +import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype +import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor import com.android.systemui.statusbar.notification.shared.PriorityPeopleSection import javax.inject.Inject @@ -53,6 +57,7 @@ class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, ha FooterViewRefactor.token dependsOn NotificationIconContainerRefactor.token NotificationAvalancheSuppression.token dependsOn VisualInterruptionRefactor.token PriorityPeopleSection.token dependsOn SortBySectionTimeFlag.token + NotificationMinimalismPrototype.token dependsOn NotificationsHeadsUpRefactor.token // SceneContainer dependencies SceneContainerFlag.getFlagDependencies().forEach { (alpha, beta) -> alpha dependsOn beta } @@ -66,14 +71,20 @@ class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, ha // DualShade dependencies DualShade.token dependsOn SceneContainerFlag.getMainAconfigFlag() + + // QS Fragment using Compose dependencies + QSComposeFragment.token dependsOn NewQsUI.token } private inline val politeNotifications get() = FlagToken(FLAG_POLITE_NOTIFICATIONS, politeNotifications()) + private inline val crossAppPoliteNotifications get() = FlagToken(FLAG_CROSS_APP_POLITE_NOTIFICATIONS, crossAppPoliteNotifications()) + private inline val vibrateWhileUnlockedToken: FlagToken get() = FlagToken(FLAG_VIBRATE_WHILE_UNLOCKED, vibrateWhileUnlocked()) + private inline val communalHub get() = FlagToken(FLAG_COMMUNAL_HUB, communalHub()) } diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 3d3584e29def..d0beb7abc25b 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -137,12 +137,6 @@ object Flags { // TODO(b/267722622): Tracking Bug @JvmField val WALLPAPER_PICKER_UI_FOR_AIWP = releasedFlag("wallpaper_picker_ui_for_aiwp") - /** Whether to allow long-press on the lock screen to directly open wallpaper picker. */ - // TODO(b/277220285): Tracking bug. - @JvmField - val LOCK_SCREEN_LONG_PRESS_DIRECT_TO_WPP = - unreleasedFlag("lock_screen_long_press_directly_opens_wallpaper_picker") - /** Whether page transition animations in the wallpaper picker are enabled */ // TODO(b/291710220): Tracking bug. @JvmField diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt index 893835a38c53..59ec87a74980 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt @@ -17,6 +17,7 @@ package com.android.systemui.keyguard.domain.interactor import android.animation.ValueAnimator +import android.util.Log import com.android.app.animation.Interpolators import com.android.app.tracing.coroutines.launch import com.android.systemui.dagger.SysUISingleton @@ -184,11 +185,7 @@ constructor( .filterRelevantKeyguardStateAnd { isOccluded -> isOccluded } .collect { if (!maybeHandleInsecurePowerGesture()) { - startTransitionTo( - toState = KeyguardState.OCCLUDED, - modeOnCanceled = TransitionModeOnCanceled.RESET, - ownerReason = "isOccluded = true", - ) + Log.i(TAG, "Ignoring change to isOccluded to prevent errant AOD->OCCLUDED") } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt index cd28bec938b8..8f50b03eafec 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt @@ -25,7 +25,7 @@ import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.deviceentry.data.repository.DeviceEntryRepository +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor import com.android.systemui.keyguard.KeyguardWmStateRefactor import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.BiometricUnlockMode.Companion.isWakeAndUnlock @@ -59,7 +59,7 @@ constructor( private val communalInteractor: CommunalInteractor, private val communalSceneInteractor: CommunalSceneInteractor, keyguardOcclusionInteractor: KeyguardOcclusionInteractor, - val deviceEntryRepository: DeviceEntryRepository, + val deviceEntryInteractor: DeviceEntryInteractor, private val wakeToGoneInteractor: KeyguardWakeDirectlyToGoneInteractor, private val dreamManager: DreamManager, ) : @@ -146,7 +146,7 @@ constructor( isIdleOnCommunal, canTransitionToGoneOnWake, primaryBouncerShowing) -> - if (!deviceEntryRepository.isLockscreenEnabled()) { + if (!deviceEntryInteractor.isLockscreenEnabled()) { if (SceneContainerFlag.isEnabled) { // TODO(b/336576536): Check if adaptation for scene framework is needed } else { 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 ec03a6d8121f..046e79cd04c4 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 @@ -188,7 +188,7 @@ constructor( * Whether the system is dreaming. [isDreaming] will be always be true when [isDozing] is true, * but not vice-versa. */ - val isDreaming: Flow<Boolean> = repository.isDreaming + val isDreaming: StateFlow<Boolean> = repository.isDreaming /** Whether the system is dreaming with an overlay active */ val isDreamingWithOverlay: Flow<Boolean> = repository.isDreamingWithOverlay @@ -205,7 +205,8 @@ constructor( trySendWithFailureLogging( cameraLaunchSourceIntToModel(source), TAG, - "updated onCameraLaunchGestureDetected") + "updated onCameraLaunchGestureDetected" + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt index 7a06d2fe9254..cd49c6a4d2e0 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt @@ -129,13 +129,16 @@ constructor( } } - /** Notifies that the user has long-pressed on the lock screen. */ - fun onLongPress() { + /** Notifies that the user has long-pressed on the lock screen. + * + * @param isA11yAction: Whether the action was performed as an a11y action + */ + fun onLongPress(isA11yAction: Boolean = false) { if (!isLongPressHandlingEnabled.value) { return } - if (featureFlags.isEnabled(Flags.LOCK_SCREEN_LONG_PRESS_DIRECT_TO_WPP)) { + if (isA11yAction) { showSettings() } else { showMenu() diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt index 76f7749754a2..1b9788f2d401 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt @@ -74,8 +74,8 @@ object DeviceEntryIconViewBinder { val bgView = view.bgView longPressHandlingView.listener = object : LongPressHandlingView.Listener { - override fun onLongPressDetected(view: View, x: Int, y: Int) { - if (falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)) { + override fun onLongPressDetected(view: View, x: Int, y: Int, isA11yAction: Boolean) { + if (!isA11yAction && falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)) { return } vibratorHelper.performHapticFeedback( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardLongPressViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardLongPressViewBinder.kt index 057b4f9a671d..b387855ad553 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardLongPressViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardLongPressViewBinder.kt @@ -18,12 +18,15 @@ package com.android.systemui.keyguard.ui.binder import android.view.View +import android.view.accessibility.AccessibilityNodeInfo +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.app.tracing.coroutines.launch import com.android.systemui.common.ui.view.LongPressHandlingView import com.android.systemui.keyguard.ui.viewmodel.KeyguardTouchHandlingViewModel import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.res.R import com.android.systemui.plugins.FalsingManager object KeyguardLongPressViewBinder { @@ -43,14 +46,19 @@ object KeyguardLongPressViewBinder { onSingleTap: () -> Unit, falsingManager: FalsingManager, ) { + view.accessibilityHintLongPressAction = + AccessibilityNodeInfo.AccessibilityAction( + AccessibilityNodeInfoCompat.ACTION_LONG_CLICK, + view.resources.getString(R.string.lock_screen_settings) + ) view.listener = object : LongPressHandlingView.Listener { - override fun onLongPressDetected(view: View, x: Int, y: Int) { - if (falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)) { + override fun onLongPressDetected(view: View, x: Int, y: Int, isA11yAction: Boolean) { + if (!isA11yAction && falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)) { return } - viewModel.onLongPress() + viewModel.onLongPress(isA11yAction) } override fun onSingleTapDetected(view: View) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModel.kt index bb4fb7961ed0..e9db1d23ec3c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModel.kt @@ -83,6 +83,7 @@ constructor( MathUtils.lerp(startAlpha, 0f, it) } }, + onFinish = { 1f }, ) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardTouchHandlingViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardTouchHandlingViewModel.kt index f1cbf256a00f..1d2edc6d406b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardTouchHandlingViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardTouchHandlingViewModel.kt @@ -33,9 +33,12 @@ constructor( /** Whether the long-press handling feature should be enabled. */ val isLongPressHandlingEnabled: Flow<Boolean> = interactor.isLongPressHandlingEnabled - /** Notifies that the user has long-pressed on the lock screen. */ - fun onLongPress() { - interactor.onLongPress() + /** Notifies that the user has long-pressed on the lock screen. + * + * @param isA11yAction: Whether the action was performed as an a11y action + */ + fun onLongPress(isA11yAction: Boolean) { + interactor.onLongPress(isA11yAction) } /** diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt index 46ba5d13bb6d..8811908e1d45 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt @@ -80,6 +80,7 @@ constructor( 1f - it } }, + onFinish = { 1f }, ) /** Bouncer container alpha */ diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java index d1c9b8ea1803..b2ba0e1cd6a6 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java @@ -169,6 +169,14 @@ public class LogModule { return factory.create("NotifRemoteInputLog", 50 /* maxSize */, false /* systrace */); } + /** Provides a logging buffer for all logs related to notification visual stability. */ + @Provides + @SysUISingleton + @VisualStabilityLog + public static LogBuffer provideVisualStabilityLogBuffer(LogBufferFactory factory) { + return factory.create("VisualStabilityLog", 50 /* maxSize */, false /* systrace */); + } + /** Provides a logging buffer for all logs related to keyguard media controller. */ @Provides @SysUISingleton diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationsRepositoryExt.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/VisualStabilityLog.kt index 9be7dfe9a1a9..b45ffc1128b1 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationsRepositoryExt.kt +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/VisualStabilityLog.kt @@ -14,14 +14,12 @@ * limitations under the License. */ -package com.android.systemui.statusbar.notification.stack.data.repository +package com.android.systemui.log.dagger -import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository +import javax.inject.Qualifier -fun FakeHeadsUpNotificationRepository.setNotifications(notifications: List<HeadsUpRowRepository>) { - setNotifications(*notifications.toTypedArray()) -} - -fun FakeHeadsUpNotificationRepository.setNotifications(vararg notifications: HeadsUpRowRepository) { - this.activeHeadsUpRows.value = notifications.toSet() -} +/** A [com.android.systemui.log.LogBuffer] for visual stability-related messages. */ +@Qualifier +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +annotation class VisualStabilityLog diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt index 46c5c188344e..c5d7b25827ea 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt @@ -888,6 +888,8 @@ constructor( heightInSceneContainerPx = height mediaCarouselScrollHandler.playerWidthPlusPadding = width + context.resources.getDimensionPixelSize(R.dimen.qs_media_padding) + mediaContent.minimumWidth = widthInSceneContainerPx + mediaContent.minimumHeight = heightInSceneContainerPx updatePlayers(recreateMedia = true) } diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBarView.java index 1dbd500c15f1..c4abcd2afc4f 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBarView.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBarView.java @@ -54,6 +54,7 @@ import android.view.WindowManager; import android.view.WindowManagerGlobal; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; +import android.view.inputmethod.Flags; import android.widget.FrameLayout; import androidx.annotation.Nullable; @@ -285,8 +286,11 @@ public class NavigationBarView extends FrameLayout { // Set up the context group of buttons mContextualButtonGroup = new ContextualButtonGroup(R.id.menu_container); + final int switcherResId = Flags.imeSwitcherRevamp() + ? com.android.internal.R.drawable.ic_ime_switcher_new + : R.drawable.ic_ime_switcher_default; final ContextualButton imeSwitcherButton = new ContextualButton(R.id.ime_switcher, - mLightContext, R.drawable.ic_ime_switcher_default); + mLightContext, switcherResId); final ContextualButton accessibilityButton = new ContextualButton(R.id.accessibility_button, mLightContext, R.drawable.ic_sysbar_accessibility_button); diff --git a/packages/SystemUI/src/com/android/systemui/qs/flags/NewQsUI.kt b/packages/SystemUI/src/com/android/systemui/qs/flags/NewQsUI.kt index 8af566523b67..ee709c4cf41a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/flags/NewQsUI.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/flags/NewQsUI.kt @@ -20,7 +20,7 @@ import com.android.systemui.Flags import com.android.systemui.flags.FlagToken import com.android.systemui.flags.RefactorFlagUtils -/** Helper for reading or using the notification avalanche suppression flag state. */ +/** Helper for reading or using the new QS UI flag state. */ @Suppress("NOTHING_TO_INLINE") object NewQsUI { /** The aconfig flag name */ diff --git a/packages/SystemUI/src/com/android/systemui/qs/flags/QSComposeFragment.kt b/packages/SystemUI/src/com/android/systemui/qs/flags/QSComposeFragment.kt new file mode 100644 index 000000000000..664d49607f89 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/flags/QSComposeFragment.kt @@ -0,0 +1,53 @@ +/* + * 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.flags + +import com.android.systemui.Flags +import com.android.systemui.flags.FlagToken +import com.android.systemui.flags.RefactorFlagUtils + +/** Helper for reading or using the new QS UI in NPVC flag state. */ +@Suppress("NOTHING_TO_INLINE") +object QSComposeFragment { + /** The aconfig flag name */ + const val FLAG_NAME = Flags.FLAG_QS_UI_REFACTOR_COMPOSE_FRAGMENT + + /** A token used for dependency declaration */ + val token: FlagToken + get() = FlagToken(FLAG_NAME, isEnabled) + + /** Is the refactor enabled */ + @JvmStatic + inline val isEnabled + get() = Flags.qsUiRefactorComposeFragment() && NewQsUI.isEnabled + + /** + * Called to ensure code is only run when the flag is enabled. This protects users from the + * unintended behaviors caused by accidentally running new logic, while also crashing on an eng + * build to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun isUnexpectedlyInLegacyMode() = + RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME) + + /** + * Called to ensure code is only run when the flag is disabled. This will throw an exception if + * the flag is enabled to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME) +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java index 720120b630d5..5ea8c2183295 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java @@ -14,6 +14,8 @@ package com.android.systemui.qs.tileimpl; +import static com.android.systemui.Flags.removeUpdateListenerInQsIconViewImpl; + import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ArgbEvaluator; @@ -204,6 +206,9 @@ public class QSIconViewImpl extends QSIconView { values.setEvaluator(ArgbEvaluator.getInstance()); mColorAnimator.setValues(values); mColorAnimator.removeAllListeners(); + if (removeUpdateListenerInQsIconViewImpl()) { + mColorAnimator.removeAllUpdateListeners(); + } mColorAnimator.addUpdateListener(animation -> { setTint(iv, (int) animation.getAnimatedValue()); }); diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt index b91891cf7be0..a3000316057f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt @@ -44,6 +44,7 @@ import com.android.systemui.qs.tiles.viewmodel.QSTileState import com.android.systemui.res.R import javax.inject.Inject import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking class ModesTile @Inject @@ -91,8 +92,8 @@ constructor( override fun newTileState() = BooleanState() - override fun handleClick(expandable: Expandable?) { - // TODO(b/346519570) open dialog + override fun handleClick(expandable: Expandable?) = runBlocking { + userActionInteractor.handleClick(expandable) } override fun getLongClickIntent(): Intent = userActionInteractor.longClickIntent @@ -107,6 +108,7 @@ constructor( label = tileLabel secondaryLabel = tileState.secondaryLabel contentDescription = tileState.contentDescription + forceExpandIcon = true } } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractor.kt index fd1f3d8fb23a..4c6563d6c143 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractor.kt @@ -16,19 +16,31 @@ package com.android.systemui.qs.tiles.impl.modes.domain.interactor +//noinspection CleanArchitectureDependencyViolation: dialog needs to be opened on click import android.content.Intent import android.provider.Settings +import com.android.internal.jank.InteractionJankMonitor +import com.android.systemui.animation.DialogCuj +import com.android.systemui.animation.DialogTransitionAnimator +import com.android.systemui.animation.Expandable +import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler import com.android.systemui.qs.tiles.base.interactor.QSTileInput import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesTileModel import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction +import com.android.systemui.statusbar.policy.ui.dialog.ModesDialogDelegate import javax.inject.Inject +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.withContext class ModesTileUserActionInteractor @Inject constructor( + @Main private val coroutineContext: CoroutineContext, private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler, + private val dialogTransitionAnimator: DialogTransitionAnimator, + private val dialogDelegate: ModesDialogDelegate, ) : QSTileUserActionInteractor<ModesTileModel> { val longClickIntent = Intent(Settings.ACTION_ZEN_MODE_SETTINGS) @@ -36,7 +48,7 @@ constructor( with(input) { when (action) { is QSTileUserAction.Click -> { - // TODO(b/346519570) open dialog + handleClick(action.expandable) } is QSTileUserAction.LongClick -> { qsTileIntentUserActionHandler.handle(action.expandable, longClickIntent) @@ -44,4 +56,24 @@ constructor( } } } + + suspend fun handleClick(expandable: Expandable?) { + // Show a dialog with the list of modes to configure. Dialogs shown by the + // DialogTransitionAnimator must be created and shown on the main thread, so we post it to + // the UI handler. + withContext(coroutineContext) { + val dialog = dialogDelegate.createDialog() + + expandable + ?.dialogTransitionController( + DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, INTERACTION_JANK_TAG) + ) + ?.let { controller -> dialogTransitionAnimator.show(dialog, controller) } + ?: dialog.show() + } + } + + companion object { + private const val INTERACTION_JANK_TAG = "configure_priority_modes" + } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt index 26b9a4c7f416..7048adab329d 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt @@ -59,5 +59,6 @@ constructor( QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK, ) + sideViewIcon = QSTileState.SideViewIcon.Chevron } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt index 8711e8878525..72f37fc98e17 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt @@ -149,6 +149,7 @@ constructor( resetShadeSessions() handleKeyguardEnabledness() notifyKeyguardDismissCallbacks() + refreshLockscreenEnabled() } else { sceneLogger.logFrameworkEnabled( isEnabled = false, @@ -186,7 +187,6 @@ constructor( applicationScope.launch { // TODO(b/296114544): Combine with some global hun state to make it visible! deviceProvisioningInteractor.isDeviceProvisioned - .distinctUntilChanged() .flatMapLatest { isAllowedToBeVisible -> if (isAllowedToBeVisible) { combine( @@ -735,4 +735,22 @@ constructor( } } } + + /** + * Keeps the value of [DeviceEntryInteractor.isLockscreenEnabled] fresh. + * + * This is needed because that value is sourced from a non-observable data source + * (`LockPatternUtils`, which doesn't expose a listener or callback for this value). Therefore, + * every time a transition to the `Lockscreen` scene is started, the value is re-fetched and + * cached. + */ + private fun refreshLockscreenEnabled() { + applicationScope.launch { + sceneInteractor.transitionState + .map { it.isTransitioning(to = Scenes.Lockscreen) } + .distinctUntilChanged() + .filter { it } + .collectLatest { deviceEntryInteractor.refreshLockscreenEnabled() } + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/policy/PolicyRequestProcessor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/policy/PolicyRequestProcessor.kt index 4f27b9e4dbf0..b3d5c9e9691c 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/policy/PolicyRequestProcessor.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/policy/PolicyRequestProcessor.kt @@ -99,7 +99,7 @@ class PolicyRequestProcessor( original, updates.component, updates.owner, - type.displayId + type.displayId, ) } return updated @@ -120,6 +120,7 @@ class PolicyRequestProcessor( return replaceWithScreenshot( original = original, componentName = topMainRootTask?.topActivity ?: defaultComponent, + taskId = topMainRootTask?.taskId, owner = defaultOwner, displayId = original.displayId ) @@ -144,11 +145,12 @@ class PolicyRequestProcessor( ) } - suspend fun replaceWithScreenshot( + private suspend fun replaceWithScreenshot( original: ScreenshotData, componentName: ComponentName?, owner: UserHandle?, displayId: Int, + taskId: Int? = null, ): ScreenshotData { Log.i(TAG, "Capturing screenshot: $componentName / $owner") val screenshot = captureDisplay(displayId) @@ -157,7 +159,8 @@ class PolicyRequestProcessor( bitmap = screenshot, userHandle = owner, topComponent = componentName, - screenBounds = Rect(0, 0, screenshot?.width ?: 0, screenshot?.height ?: 0) + screenBounds = Rect(0, 0, screenshot?.width ?: 0, screenshot?.height ?: 0), + taskId = taskId ?: -1, ) } diff --git a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt index d870fe69463d..d090aea4cee5 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt @@ -56,13 +56,12 @@ import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.scene.shared.model.SceneDataSourceDelegator import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController -import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf -import com.android.systemui.util.kotlin.BooleanFlowOperators.not import com.android.systemui.util.kotlin.collectFlow import java.util.function.Consumer import javax.inject.Inject import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch /** @@ -165,7 +164,14 @@ constructor( * * Based on [ShadeInteractor.isAnyFullyExpanded] and [ShadeInteractor.isUserInteracting]. */ - private var shadeShowing = false + private var shadeShowingAndConsumingTouches = false + + /** + * True if the shade ever fully expands and the user isn't interacting with it (aka finger on + * screen dragging). In this case, the shade should handle all touch events until it has fully + * collapsed. + */ + private var userNotInteractiveAtShadeFullyExpanded = false /** * True if the device is dreaming, in which case we shouldn't do anything for top/bottom swipes @@ -317,9 +323,25 @@ constructor( ) collectFlow( containerView, - allOf(shadeInteractor.isAnyFullyExpanded, not(shadeInteractor.isUserInteracting)), - { - shadeShowing = it + combine( + shadeInteractor.isAnyFullyExpanded, + shadeInteractor.isUserInteracting, + shadeInteractor.isShadeFullyCollapsed, + ::Triple + ), + { (isFullyExpanded, isUserInteracting, isShadeFullyCollapsed) -> + val expandedAndNotInteractive = isFullyExpanded && !isUserInteracting + + // If we ever are fully expanded and not interacting, capture this state as we + // should not handle touches until we fully collapse again + userNotInteractiveAtShadeFullyExpanded = + !isShadeFullyCollapsed && + (userNotInteractiveAtShadeFullyExpanded || expandedAndNotInteractive) + + // If the shade reaches full expansion without interaction, then we should allow it + // to consume touches rather than handling it here until it disappears. + shadeShowingAndConsumingTouches = + userNotInteractiveAtShadeFullyExpanded || expandedAndNotInteractive updateTouchHandlingState() } ) @@ -337,7 +359,8 @@ constructor( * Also clears gesture exclusion zones when the hub is occluded or gone. */ private fun updateTouchHandlingState() { - val shouldInterceptGestures = hubShowing && !(shadeShowing || anyBouncerShowing) + val shouldInterceptGestures = + hubShowing && !(shadeShowingAndConsumingTouches || anyBouncerShowing) if (shouldInterceptGestures) { lifecycleRegistry.currentState = Lifecycle.State.RESUMED } else { @@ -395,11 +418,12 @@ constructor( private fun handleTouchEventOnCommunalView(view: View, ev: MotionEvent): Boolean { val isDown = ev.actionMasked == MotionEvent.ACTION_DOWN val isUp = ev.actionMasked == MotionEvent.ACTION_UP + val isMove = ev.actionMasked == MotionEvent.ACTION_MOVE val isCancel = ev.actionMasked == MotionEvent.ACTION_CANCEL - val hubOccluded = anyBouncerShowing || shadeShowing + val hubOccluded = anyBouncerShowing || shadeShowingAndConsumingTouches - if (isDown && !hubOccluded) { + if ((isDown || isMove) && !hubOccluded) { isTrackingHubTouch = true } diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java index 4f6a64f043d2..bd0868530cba 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java @@ -1265,20 +1265,20 @@ public class QuickSettingsControllerImpl implements QuickSettingsController, Dum mTranslationForFullShadeTransition = qsTranslation; updateQsFrameTranslation(); float currentTranslation = mQsFrame.getTranslationY(); - int clipTop = mEnableClipping - ? (int) (top - currentTranslation - mQsFrame.getTop()) : 0; - int clipBottom = mEnableClipping - ? (int) (bottom - currentTranslation - mQsFrame.getTop()) : 0; + int clipTop = (int) (top - currentTranslation - mQsFrame.getTop()); + int clipBottom = (int) (bottom - currentTranslation - mQsFrame.getTop()); mVisible = qsVisible; mQs.setQsVisible(qsVisible); - mQs.setFancyClipping( - mDisplayLeftInset, - clipTop, - mDisplayRightInset, - clipBottom, - radius, - qsVisible && !mSplitShadeEnabled, - mIsFullWidth); + if (mEnableClipping) { + mQs.setFancyClipping( + mDisplayLeftInset, + clipTop, + mDisplayRightInset, + clipBottom, + radius, + qsVisible && !mSplitShadeEnabled, + mIsFullWidth); + } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt index 5065baa04623..23e2620ac6d6 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt @@ -76,7 +76,7 @@ constructor( scope.launch { shadeInteractor.isAnyExpanded.collect { if (!it) { - runPostCollapseActions() + withContext(mainDispatcher) { runPostCollapseActions() } } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt index 11ccdff687a1..59fd0ca4513e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt @@ -57,7 +57,7 @@ constructor( interactor.ongoingCallState .map { state -> when (state) { - is OngoingCallModel.NoCall -> OngoingActivityChipModel.Hidden + is OngoingCallModel.NoCall -> OngoingActivityChipModel.Hidden() is OngoingCallModel.InCall -> { // This block mimics OngoingCallController#updateChip. if (state.startTimeMs <= 0L) { @@ -82,7 +82,7 @@ constructor( } } } - .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden) + .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden()) private fun getOnClickListener(state: OngoingCallModel.InCall): View.OnClickListener? { if (state.intent == null) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegate.kt index bafec38efe9d..6ea72b97cb3a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegate.kt @@ -44,9 +44,10 @@ class EndCastScreenToOtherDeviceDialogDelegate( // No custom on-click, because the dialog will automatically be dismissed when the // button is clicked anyway. setNegativeButton(R.string.close_dialog_button, /* onClick= */ null) - setPositiveButton(R.string.cast_to_other_device_stop_dialog_button) { _, _ -> - stopAction.invoke() - } + setPositiveButton( + R.string.cast_to_other_device_stop_dialog_button, + endMediaProjectionDialogHelper.wrapStopAction(stopAction), + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegate.kt index 7dc9b255badc..b0c832172776 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegate.kt @@ -55,9 +55,10 @@ class EndGenericCastToOtherDeviceDialogDelegate( // No custom on-click, because the dialog will automatically be dismissed when the // button is clicked anyway. setNegativeButton(R.string.close_dialog_button, /* onClick= */ null) - setPositiveButton(R.string.cast_to_other_device_stop_dialog_button) { _, _ -> - stopAction.invoke() - } + setPositiveButton( + R.string.cast_to_other_device_stop_dialog_button, + endMediaProjectionDialogHelper.wrapStopAction(stopAction), + ) } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt index afa9ccefab86..d9b0504308f8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt @@ -18,6 +18,9 @@ package com.android.systemui.statusbar.chips.casttootherdevice.ui.viewmodel import android.content.Context import androidx.annotation.DrawableRes +import com.android.internal.jank.Cuj +import com.android.systemui.animation.DialogCuj +import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.dagger.SysUISingleton @@ -35,6 +38,7 @@ import com.android.systemui.statusbar.chips.mediaprojection.domain.model.Project import com.android.systemui.statusbar.chips.mediaprojection.ui.view.EndMediaProjectionDialogHelper import com.android.systemui.statusbar.chips.ui.model.ColorsModel 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.time.SystemClock @@ -60,6 +64,7 @@ constructor( private val mediaProjectionChipInteractor: MediaProjectionChipInteractor, private val mediaRouterChipInteractor: MediaRouterChipInteractor, private val systemClock: SystemClock, + private val dialogTransitionAnimator: DialogTransitionAnimator, private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper, @StatusBarChipsLog private val logger: LogBuffer, ) : OngoingActivityChipViewModel { @@ -74,18 +79,18 @@ constructor( mediaProjectionChipInteractor.projection .map { projectionModel -> when (projectionModel) { - is ProjectionChipModel.NotProjecting -> OngoingActivityChipModel.Hidden + is ProjectionChipModel.NotProjecting -> OngoingActivityChipModel.Hidden() is ProjectionChipModel.Projecting -> { if (projectionModel.type != ProjectionChipModel.Type.CAST_TO_OTHER_DEVICE) { - OngoingActivityChipModel.Hidden + OngoingActivityChipModel.Hidden() } else { createCastScreenToOtherDeviceChip(projectionModel) } } } } - // See b/347726238. - .stateIn(scope, SharingStarted.Lazily, OngoingActivityChipModel.Hidden) + // See b/347726238 for [SharingStarted.Lazily] reasoning. + .stateIn(scope, SharingStarted.Lazily, OngoingActivityChipModel.Hidden()) /** * The cast chip to show, based only on MediaRouter API events. @@ -109,7 +114,7 @@ constructor( mediaRouterChipInteractor.mediaRouterCastingState .map { routerModel -> when (routerModel) { - is MediaRouterCastModel.DoingNothing -> OngoingActivityChipModel.Hidden + is MediaRouterCastModel.DoingNothing -> OngoingActivityChipModel.Hidden() is MediaRouterCastModel.Casting -> { // A consequence of b/269975671 is that MediaRouter will mark a device as // casting before casting has actually started. To alleviate this bug a bit, @@ -123,9 +128,9 @@ constructor( } } } - .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden) + .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden()) - override val chip: StateFlow<OngoingActivityChipModel> = + private val internalChip: StateFlow<OngoingActivityChipModel> = combine(projectionChip, routerChip) { projection, router -> logger.log( TAG, @@ -159,17 +164,24 @@ constructor( router } } - .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden) + .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden()) + + private val hideChipDuringDialogTransitionHelper = ChipTransitionHelper(scope) + + override val chip: StateFlow<OngoingActivityChipModel> = + hideChipDuringDialogTransitionHelper.createChipFlow(internalChip) /** Stops the currently active projection. */ - private fun stopProjecting() { - logger.log(TAG, LogLevel.INFO, {}, { "Stop casting requested (projection)" }) + private fun stopProjectingFromDialog() { + logger.log(TAG, LogLevel.INFO, {}, { "Stop casting requested from dialog (projection)" }) + hideChipDuringDialogTransitionHelper.onActivityStoppedFromDialog() mediaProjectionChipInteractor.stopProjecting() } /** Stops the currently active media route. */ - private fun stopMediaRouterCasting() { - logger.log(TAG, LogLevel.INFO, {}, { "Stop casting requested (router)" }) + private fun stopMediaRouterCastingFromDialog() { + logger.log(TAG, LogLevel.INFO, {}, { "Stop casting requested from dialog (router)" }) + hideChipDuringDialogTransitionHelper.onActivityStoppedFromDialog() mediaRouterChipInteractor.stopCasting() } @@ -190,6 +202,8 @@ constructor( startTimeMs = systemClock.elapsedRealtime(), createDialogLaunchOnClickListener( createCastScreenToOtherDeviceDialogDelegate(state), + dialogTransitionAnimator, + DialogCuj(Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP, tag = "Cast to other device"), logger, TAG, ), @@ -207,6 +221,11 @@ constructor( colors = ColorsModel.Red, createDialogLaunchOnClickListener( createGenericCastToOtherDeviceDialogDelegate(deviceName), + dialogTransitionAnimator, + DialogCuj( + Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP, + tag = "Cast to other device audio only", + ), logger, TAG, ), @@ -219,7 +238,7 @@ constructor( EndCastScreenToOtherDeviceDialogDelegate( endMediaProjectionDialogHelper, context, - stopAction = this::stopProjecting, + stopAction = this::stopProjectingFromDialog, state, ) @@ -228,7 +247,7 @@ constructor( endMediaProjectionDialogHelper, context, deviceName, - stopAction = this::stopMediaRouterCasting, + stopAction = this::stopMediaRouterCastingFromDialog, ) companion object { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelper.kt index 600436557efb..2d9ccb7b09b0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelper.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelper.kt @@ -17,7 +17,9 @@ package com.android.systemui.statusbar.chips.mediaprojection.ui.view import android.app.ActivityManager +import android.content.DialogInterface import android.content.pm.PackageManager +import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.dagger.SysUISingleton import com.android.systemui.mediaprojection.data.model.MediaProjectionState import com.android.systemui.statusbar.phone.SystemUIDialog @@ -29,6 +31,7 @@ class EndMediaProjectionDialogHelper @Inject constructor( private val dialogFactory: SystemUIDialog.Factory, + private val dialogTransitionAnimator: DialogTransitionAnimator, private val packageManager: PackageManager, ) { /** Creates a new [SystemUIDialog] using the given delegate. */ @@ -36,6 +39,28 @@ constructor( return dialogFactory.create(delegate) } + /** + * Returns the click listener that should be invoked if a user clicks "Stop" on the end media + * projection dialog. + * + * The click listener will invoke [stopAction] and also do some UI manipulation. + * + * @param stopAction an action that, when invoked, should notify system API(s) that the media + * projection should be stopped. + */ + fun wrapStopAction(stopAction: () -> Unit): DialogInterface.OnClickListener { + return DialogInterface.OnClickListener { _, _ -> + // If the projection is stopped, then the chip will disappear, so we don't want the + // dialog to animate back into the chip just for the chip to disappear in a few frames. + dialogTransitionAnimator.disableAllCurrentDialogsExitAnimations() + stopAction.invoke() + // TODO(b/332662551): If the projection is stopped, there's a brief moment where the + // dialog closes and the chip re-shows because the system APIs haven't come back and + // told SysUI that the projection has officially stopped. It would be great for the chip + // to not re-show at all. + } + } + fun getAppName(state: MediaProjectionState.Projecting): CharSequence? { val specificTaskInfo = if (state is MediaProjectionState.Projecting.SingleTask) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegate.kt index 1eca827d55c4..72656ca1934c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegate.kt @@ -52,9 +52,10 @@ class EndScreenRecordingDialogDelegate( // No custom on-click, because the dialog will automatically be dismissed when the // button is clicked anyway. setNegativeButton(R.string.close_dialog_button, /* onClick= */ null) - setPositiveButton(R.string.screenrecord_stop_dialog_button) { _, _ -> - stopAction.invoke() - } + setPositiveButton( + R.string.screenrecord_stop_dialog_button, + endMediaProjectionDialogHelper.wrapStopAction(stopAction), + ) } } } 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 0c349810257a..fcf3de42eb32 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 @@ -19,6 +19,9 @@ package com.android.systemui.statusbar.chips.screenrecord.ui.viewmodel import android.app.ActivityManager import android.content.Context import androidx.annotation.DrawableRes +import com.android.internal.jank.Cuj +import com.android.systemui.animation.DialogCuj +import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.dagger.SysUISingleton @@ -32,8 +35,10 @@ import com.android.systemui.statusbar.chips.mediaprojection.ui.view.EndMediaProj import com.android.systemui.statusbar.chips.screenrecord.domain.interactor.ScreenRecordChipInteractor import com.android.systemui.statusbar.chips.screenrecord.domain.model.ScreenRecordChipModel import com.android.systemui.statusbar.chips.screenrecord.ui.view.EndScreenRecordingDialogDelegate +import com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel.ShareToAppChipViewModel import com.android.systemui.statusbar.chips.ui.model.ColorsModel 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.time.SystemClock @@ -52,15 +57,18 @@ constructor( @Application private val scope: CoroutineScope, private val context: Context, private val interactor: ScreenRecordChipInteractor, + private val shareToAppChipViewModel: ShareToAppChipViewModel, private val systemClock: SystemClock, private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper, + private val dialogTransitionAnimator: DialogTransitionAnimator, @StatusBarChipsLog private val logger: LogBuffer, ) : OngoingActivityChipViewModel { - override val chip: StateFlow<OngoingActivityChipModel> = + + private val internalChip = interactor.screenRecordState .map { state -> when (state) { - is ScreenRecordChipModel.DoingNothing -> OngoingActivityChipModel.Hidden + is ScreenRecordChipModel.DoingNothing -> OngoingActivityChipModel.Hidden() is ScreenRecordChipModel.Starting -> { OngoingActivityChipModel.Shown.Countdown( colors = ColorsModel.Red, @@ -80,6 +88,11 @@ constructor( startTimeMs = systemClock.elapsedRealtime(), createDialogLaunchOnClickListener( createDelegate(state.recordedTask), + dialogTransitionAnimator, + DialogCuj( + Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP, + tag = "Screen record", + ), logger, TAG, ), @@ -87,8 +100,13 @@ constructor( } } } - // See b/347726238. - .stateIn(scope, SharingStarted.Lazily, OngoingActivityChipModel.Hidden) + // 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) private fun createDelegate( recordedTask: ActivityManager.RunningTaskInfo? @@ -96,13 +114,15 @@ constructor( return EndScreenRecordingDialogDelegate( endMediaProjectionDialogHelper, context, - stopAction = this::stopRecording, + stopAction = this::stopRecordingFromDialog, recordedTask, ) } - private fun stopRecording() { - logger.log(TAG, LogLevel.INFO, {}, { "Stop recording requested" }) + private fun stopRecordingFromDialog() { + logger.log(TAG, LogLevel.INFO, {}, { "Stop recording requested from dialog" }) + chipTransitionHelper.onActivityStoppedFromDialog() + shareToAppChipViewModel.onRecordingStoppedFromDialog() interactor.stopRecording() } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegate.kt index 564f20e4b596..d10bd7705ce9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegate.kt @@ -44,9 +44,10 @@ class EndShareToAppDialogDelegate( // No custom on-click, because the dialog will automatically be dismissed when the // button is clicked anyway. setNegativeButton(R.string.close_dialog_button, /* onClick= */ null) - setPositiveButton(R.string.share_to_app_stop_dialog_button) { _, _ -> - stopAction.invoke() - } + setPositiveButton( + R.string.share_to_app_stop_dialog_button, + endMediaProjectionDialogHelper.wrapStopAction(stopAction), + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt index ddebd3a0e3c2..85973fca4326 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt @@ -18,6 +18,9 @@ package com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel import android.content.Context import androidx.annotation.DrawableRes +import com.android.internal.jank.Cuj +import com.android.systemui.animation.DialogCuj +import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.dagger.SysUISingleton @@ -32,6 +35,7 @@ import com.android.systemui.statusbar.chips.mediaprojection.ui.view.EndMediaProj import com.android.systemui.statusbar.chips.sharetoapp.ui.view.EndShareToAppDialogDelegate import com.android.systemui.statusbar.chips.ui.model.ColorsModel 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.time.SystemClock @@ -55,28 +59,49 @@ constructor( private val mediaProjectionChipInteractor: MediaProjectionChipInteractor, private val systemClock: SystemClock, private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper, + private val dialogTransitionAnimator: DialogTransitionAnimator, @StatusBarChipsLog private val logger: LogBuffer, ) : OngoingActivityChipViewModel { - override val chip: StateFlow<OngoingActivityChipModel> = + private val internalChip = mediaProjectionChipInteractor.projection .map { projectionModel -> when (projectionModel) { - is ProjectionChipModel.NotProjecting -> OngoingActivityChipModel.Hidden + is ProjectionChipModel.NotProjecting -> OngoingActivityChipModel.Hidden() is ProjectionChipModel.Projecting -> { if (projectionModel.type != ProjectionChipModel.Type.SHARE_TO_APP) { - OngoingActivityChipModel.Hidden + OngoingActivityChipModel.Hidden() } else { createShareToAppChip(projectionModel) } } } } - // See b/347726238. - .stateIn(scope, SharingStarted.Lazily, OngoingActivityChipModel.Hidden) + // 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) + + /** + * Notifies this class that the user just stopped a screen recording from the dialog that's + * shown when you tap the recording chip. + */ + fun onRecordingStoppedFromDialog() { + // When a screen recording is active, share-to-app is also active (screen recording is just + // a special case of share-to-app, where the specific app receiving the share is System UI). + // When a screen recording is stopped, we immediately hide the screen recording chip in + // [com.android.systemui.statusbar.chips.screenrecord.ui.viewmodel.ScreenRecordChipViewModel]. + // We *also* need to immediately hide the share-to-app chip so it doesn't briefly show. + // See b/350891338. + chipTransitionHelper.onActivityStoppedFromDialog() + } /** Stops the currently active projection. */ - private fun stopProjecting() { - logger.log(TAG, LogLevel.INFO, {}, { "Stop sharing requested" }) + private fun stopProjectingFromDialog() { + logger.log(TAG, LogLevel.INFO, {}, { "Stop sharing requested from dialog" }) + chipTransitionHelper.onActivityStoppedFromDialog() mediaProjectionChipInteractor.stopProjecting() } @@ -92,7 +117,16 @@ constructor( colors = ColorsModel.Red, // TODO(b/332662551): Maybe use a MediaProjection API to fetch this time. startTimeMs = systemClock.elapsedRealtime(), - createDialogLaunchOnClickListener(createShareToAppDialogDelegate(state), logger, TAG), + createDialogLaunchOnClickListener( + createShareToAppDialogDelegate(state), + dialogTransitionAnimator, + DialogCuj( + Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP, + tag = "Share to app", + ), + logger, + TAG, + ), ) } @@ -100,7 +134,7 @@ constructor( EndShareToAppDialogDelegate( endMediaProjectionDialogHelper, context, - stopAction = this::stopProjecting, + stopAction = this::stopProjectingFromDialog, state, ) 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 b2140f793ced..130b1170c9f1 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,6 +18,7 @@ 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 @@ -41,8 +42,11 @@ sealed interface ColorsModel { /** The chip should have a red background with white text. */ data object Red : ColorsModel { - override fun background(context: Context): ColorStateList = - ColorStateList.valueOf(context.getColor(R.color.GM2_red_600)) + 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) + } override fun text(context: Context) = context.getColor(android.R.color.white) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt index 40f86f924cd5..17cf60bf2dc5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt @@ -24,9 +24,15 @@ sealed class OngoingActivityChipModel { /** Condensed name representing the model, used for logs. */ abstract val logName: String - /** This chip shouldn't be shown. */ - data object Hidden : OngoingActivityChipModel() { - override val logName = "Hidden" + /** + * This chip shouldn't be shown. + * + * @property shouldAnimate true if the transition from [Shown] to [Hidden] should be animated, + * and false if that transition should *not* be animated (i.e. the chip view should + * immediately disappear). + */ + data class Hidden(val shouldAnimate: Boolean = true) : OngoingActivityChipModel() { + override val logName = "Hidden(anim=$shouldAnimate)" } /** This chip should be shown with the given information. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelper.kt new file mode 100644 index 000000000000..92e72c29519a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelper.kt @@ -0,0 +1,97 @@ +/* + * 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.statusbar.chips.ui.viewmodel + +import android.annotation.SuppressLint +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.transformLatest +import kotlinx.coroutines.launch + +/** + * A class that can help [OngoingActivityChipViewModel] instances with various transition states. + * + * For now, this class's only functionality is immediately hiding the chip if the user has tapped an + * activity chip and then clicked "Stop" on the resulting dialog. There's a bit of a delay between + * when the user clicks "Stop" and when the system services notify SysUI that the activity has + * indeed stopped. We don't want the chip to briefly show for a few frames during that delay, so + * this class helps us immediately hide the chip as soon as the user clicks "Stop" in the dialog. + * See b/353249803#comment4. + */ +@OptIn(ExperimentalCoroutinesApi::class) +class ChipTransitionHelper(@Application private val scope: CoroutineScope) { + /** A flow that emits each time the user has clicked "Stop" on the dialog. */ + @SuppressLint("SharedFlowCreation") + private val activityStoppedFromDialogEvent = MutableSharedFlow<Unit>() + + /** True if the user recently stopped the activity from the dialog. */ + private val wasActivityRecentlyStoppedFromDialog: Flow<Boolean> = + activityStoppedFromDialogEvent + .transformLatest { + // Give system services 500ms to stop the activity and notify SysUI. Once more than + // 500ms has elapsed, we should go back to using the current system service + // information as the source of truth. + emit(true) + delay(500) + emit(false) + } + // Use stateIn so that the flow created in [createChipFlow] is guaranteed to + // emit. (`combine`s require that all input flows have emitted.) + .stateIn(scope, SharingStarted.Lazily, false) + + /** + * Notifies this class that the user just clicked "Stop" on the stop dialog that's shown when + * the chip is tapped. + * + * Call this method in order to immediately hide the chip. + */ + fun onActivityStoppedFromDialog() { + // Because this event causes UI changes, make sure it's launched on the main thread scope. + scope.launch { activityStoppedFromDialogEvent.emit(Unit) } + } + + /** + * Creates a flow that will forcibly hide the chip if the user recently stopped the activity + * (see [onActivityStoppedFromDialog]). In general, this flow just uses value in [chip]. + */ + fun createChipFlow(chip: Flow<OngoingActivityChipModel>): StateFlow<OngoingActivityChipModel> { + return combine( + chip, + wasActivityRecentlyStoppedFromDialog, + ) { chipModel, activityRecentlyStopped -> + if (activityRecentlyStopped) { + // There's a bit of a delay between when the user stops an activity via + // SysUI and when the system services notify SysUI that the activity has + // indeed stopped. Prevent the chip from showing during this delay by + // immediately hiding it without any animation. + OngoingActivityChipModel.Hidden(shouldAnimate = false) + } else { + chipModel + } + } + .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden()) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModel.kt index ee010f7a818b..2fc366b7f078 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModel.kt @@ -17,10 +17,14 @@ package com.android.systemui.statusbar.chips.ui.viewmodel import android.view.View +import com.android.systemui.animation.DialogCuj +import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.LogLevel +import com.android.systemui.res.R import com.android.systemui.statusbar.chips.StatusBarChipsLog import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel +import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer import com.android.systemui.statusbar.phone.SystemUIDialog import kotlinx.coroutines.flow.StateFlow @@ -36,13 +40,19 @@ interface OngoingActivityChipViewModel { /** Creates a chip click listener that launches a dialog created by [dialogDelegate]. */ fun createDialogLaunchOnClickListener( dialogDelegate: SystemUIDialog.Delegate, + dialogTransitionAnimator: DialogTransitionAnimator, + cuj: DialogCuj, @StatusBarChipsLog logger: LogBuffer, tag: String, ): View.OnClickListener { - return View.OnClickListener { _ -> + return View.OnClickListener { view -> logger.log(tag, LogLevel.INFO, {}, { "Chip clicked" }) val dialog = dialogDelegate.createDialog() - dialog.show() + val launchableView = + view.requireViewById<ChipBackgroundContainer>( + R.id.ongoing_activity_chip_background + ) + dialogTransitionAnimator.showFromView(dialog, launchableView, cuj) } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt index 15c348ed2f67..b0d897def53f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt @@ -26,11 +26,14 @@ import com.android.systemui.statusbar.chips.casttootherdevice.ui.viewmodel.CastT import com.android.systemui.statusbar.chips.screenrecord.ui.viewmodel.ScreenRecordChipViewModel import com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel.ShareToAppChipViewModel import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel +import com.android.systemui.util.kotlin.pairwise import javax.inject.Inject import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn /** @@ -50,49 +53,132 @@ constructor( callChipViewModel: CallChipViewModel, @StatusBarChipsLog private val logger: LogBuffer, ) { + private enum class ChipType { + ScreenRecord, + ShareToApp, + CastToOtherDevice, + Call, + } + + /** Model that helps us internally track the various chip states from each of the types. */ + private sealed interface InternalChipModel { + /** + * Represents that we've internally decided to show the chip with type [type] with the given + * [model] information. + */ + data class Shown(val type: ChipType, val model: OngoingActivityChipModel.Shown) : + InternalChipModel + + /** + * Represents that all chip types would like to be hidden. Each value specifies *how* that + * chip type should get hidden. + */ + data class Hidden( + val screenRecord: OngoingActivityChipModel.Hidden, + val shareToApp: OngoingActivityChipModel.Hidden, + val castToOtherDevice: OngoingActivityChipModel.Hidden, + val call: OngoingActivityChipModel.Hidden, + ) : InternalChipModel + } + + private val internalChip: Flow<InternalChipModel> = + combine( + screenRecordChipViewModel.chip, + shareToAppChipViewModel.chip, + castToOtherDeviceChipViewModel.chip, + callChipViewModel.chip, + ) { screenRecord, shareToApp, castToOtherDevice, call -> + logger.log( + TAG, + LogLevel.INFO, + { + str1 = screenRecord.logName + str2 = shareToApp.logName + str3 = castToOtherDevice.logName + }, + { "Chips: ScreenRecord=$str1 > ShareToApp=$str2 > CastToOther=$str3..." }, + ) + logger.log(TAG, LogLevel.INFO, { str1 = call.logName }, { "... > Call=$str1" }) + // This `when` statement shows the priority order of the chips. + when { + // Screen recording also activates the media projection APIs, so whenever the + // screen recording chip is active, the media projection chip would also be + // active. We want the screen-recording-specific chip shown in this case, so we + // give the screen recording chip priority. See b/296461748. + screenRecord is OngoingActivityChipModel.Shown -> + InternalChipModel.Shown(ChipType.ScreenRecord, screenRecord) + shareToApp is OngoingActivityChipModel.Shown -> + InternalChipModel.Shown(ChipType.ShareToApp, shareToApp) + castToOtherDevice is OngoingActivityChipModel.Shown -> + InternalChipModel.Shown(ChipType.CastToOtherDevice, castToOtherDevice) + call is OngoingActivityChipModel.Shown -> + InternalChipModel.Shown(ChipType.Call, call) + else -> { + // We should only get here if all chip types are hidden + check(screenRecord is OngoingActivityChipModel.Hidden) + check(shareToApp is OngoingActivityChipModel.Hidden) + check(castToOtherDevice is OngoingActivityChipModel.Hidden) + check(call is OngoingActivityChipModel.Hidden) + InternalChipModel.Hidden( + screenRecord = screenRecord, + shareToApp = shareToApp, + castToOtherDevice = castToOtherDevice, + call = call, + ) + } + } + } + /** * A flow modeling the chip that should be shown in the status bar after accounting for possibly - * multiple ongoing activities. + * multiple ongoing activities and animation requirements. * * [com.android.systemui.statusbar.phone.fragment.CollapsedStatusBarFragment] is responsible for * actually displaying the chip. */ val chip: StateFlow<OngoingActivityChipModel> = - combine( - screenRecordChipViewModel.chip, - shareToAppChipViewModel.chip, - castToOtherDeviceChipViewModel.chip, - callChipViewModel.chip, - ) { screenRecord, shareToApp, castToOtherDevice, call -> - logger.log( - TAG, - LogLevel.INFO, - { - str1 = screenRecord.logName - str2 = shareToApp.logName - str3 = castToOtherDevice.logName - }, - { "Chips: ScreenRecord=$str1 > ShareToApp=$str2 > CastToOther=$str3..." }, - ) - logger.log(TAG, LogLevel.INFO, { str1 = call.logName }, { "... > Call=$str1" }) - // This `when` statement shows the priority order of the chips - when { - // Screen recording also activates the media projection APIs, so whenever the - // screen recording chip is active, the media projection chip would also be - // active. We want the screen-recording-specific chip shown in this case, so we - // give the screen recording chip priority. See b/296461748. - screenRecord is OngoingActivityChipModel.Shown -> screenRecord - shareToApp is OngoingActivityChipModel.Shown -> shareToApp - castToOtherDevice is OngoingActivityChipModel.Shown -> castToOtherDevice - else -> call + internalChip + .pairwise(initialValue = DEFAULT_INTERNAL_HIDDEN_MODEL) + .map { (old, new) -> + if (old is InternalChipModel.Shown && new is InternalChipModel.Hidden) { + // If we're transitioning from showing the chip to hiding the chip, different + // chips require different animation behaviors. For example, the screen share + // chips shouldn't animate if the user stopped the screen share from the dialog + // (see b/353249803#comment4), but the call chip should always animate. + // + // This `when` block makes sure that when we're transitioning from Shown to + // Hidden, we check what chip type was previously showing and we use that chip + // type's hide animation behavior. + when (old.type) { + ChipType.ScreenRecord -> new.screenRecord + ChipType.ShareToApp -> new.shareToApp + ChipType.CastToOtherDevice -> new.castToOtherDevice + ChipType.Call -> new.call + } + } else if (new is InternalChipModel.Shown) { + // If we have a chip to show, always show it. + new.model + } else { + // In the Hidden -> Hidden transition, it shouldn't matter which hidden model we + // choose because no animation should happen regardless. + OngoingActivityChipModel.Hidden() } } // Some of the chips could have timers in them and we don't want the start time // for those timers to get reset for any reason. So, as soon as any subscriber has - // requested the chip information, we need to maintain it forever. See b/347726238. - .stateIn(scope, SharingStarted.Lazily, OngoingActivityChipModel.Hidden) + // requested the chip information, we maintain it forever by using + // [SharingStarted.Lazily]. See b/347726238. + .stateIn(scope, SharingStarted.Lazily, OngoingActivityChipModel.Hidden()) companion object { private const val TAG = "ChipsViewModel" + + private val DEFAULT_INTERNAL_HIDDEN_MODEL = + InternalChipModel.Hidden( + screenRecord = OngoingActivityChipModel.Hidden(), + shareToApp = OngoingActivityChipModel.Hidden(), + castToOtherDevice = OngoingActivityChipModel.Hidden(), + call = OngoingActivityChipModel.Hidden(), + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt index 3dcaff3de35a..b342722ebb09 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt @@ -52,8 +52,11 @@ constructor(val proxy: DeviceConfigProxy, val context: Context) { } fun getNotificationBuckets(): IntArray { - if (PriorityPeopleSection.isEnabled || NotificationMinimalismPrototype.V2.isEnabled - || NotificationClassificationFlag.isEnabled) { + if ( + PriorityPeopleSection.isEnabled || + NotificationMinimalismPrototype.isEnabled || + NotificationClassificationFlag.isEnabled + ) { // We don't need this list to be adaptive, it can be the superset of all features. return PriorityBucket.getAllInOrder() } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java index e48c28d3f3ee..cb133ecadab2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java @@ -1005,6 +1005,16 @@ public final class NotificationEntry extends ListEntry implements NotificationRo mIsMarkedForUserTriggeredMovement = marked; } + private boolean mSeenInShade = false; + + public void setSeenInShade(boolean seen) { + mSeenInShade = seen; + } + + public boolean isSeenInShade() { + return mSeenInShade; + } + public void setIsHeadsUpEntry(boolean isHeadsUpEntry) { mIsHeadsUpEntry = isHeadsUpEntry; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt index 55c6790d4fb1..b1b2a653fde2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt @@ -16,62 +16,15 @@ package com.android.systemui.statusbar.notification.collection.coordinator -import android.app.NotificationManager -import android.os.UserHandle -import android.provider.Settings -import androidx.annotation.VisibleForTesting -import com.android.systemui.Dumpable -import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.dump.DumpManager -import com.android.systemui.keyguard.data.repository.KeyguardRepository -import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor -import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.plugins.statusbar.StatusBarStateController -import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.statusbar.StatusBarState -import com.android.systemui.statusbar.expansionChanges -import com.android.systemui.statusbar.notification.collection.GroupEntry -import com.android.systemui.statusbar.notification.collection.ListEntry import com.android.systemui.statusbar.notification.collection.NotifPipeline import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter -import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter -import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner -import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener import com.android.systemui.statusbar.notification.collection.provider.SectionHeaderVisibilityProvider -import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider -import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype -import com.android.systemui.statusbar.notification.stack.BUCKET_TOP_ONGOING -import com.android.systemui.statusbar.notification.stack.BUCKET_TOP_UNSEEN -import com.android.systemui.statusbar.policy.HeadsUpManager -import com.android.systemui.statusbar.policy.headsUpEvents -import com.android.systemui.util.asIndenting -import com.android.systemui.util.indentIfPossible -import com.android.systemui.util.settings.SecureSettings -import com.android.systemui.util.settings.SettingsProxyExt.observerFlow -import java.io.PrintWriter import javax.inject.Inject -import kotlin.time.Duration.Companion.seconds -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.conflate -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.onStart -import kotlinx.coroutines.launch -import kotlinx.coroutines.yield /** * Filters low priority and privacy-sensitive notifications from the lockscreen, and hides section @@ -82,24 +35,10 @@ import kotlinx.coroutines.yield class KeyguardCoordinator @Inject constructor( - @Background private val bgDispatcher: CoroutineDispatcher, - private val dumpManager: DumpManager, - private val headsUpManager: HeadsUpManager, private val keyguardNotificationVisibilityProvider: KeyguardNotificationVisibilityProvider, - private val keyguardRepository: KeyguardRepository, - private val keyguardTransitionInteractor: KeyguardTransitionInteractor, - private val logger: KeyguardCoordinatorLogger, - @Application private val scope: CoroutineScope, private val sectionHeaderVisibilityProvider: SectionHeaderVisibilityProvider, - private val secureSettings: SecureSettings, - private val seenNotificationsInteractor: SeenNotificationsInteractor, private val statusBarStateController: StatusBarStateController, -) : Coordinator, Dumpable { - - private val unseenNotifications = mutableSetOf<NotificationEntry>() - private val unseenEntryAdded = MutableSharedFlow<NotificationEntry>(extraBufferCapacity = 1) - private val unseenEntryRemoved = MutableSharedFlow<NotificationEntry>(extraBufferCapacity = 1) - private var unseenFilterEnabled = false +) : Coordinator { override fun attach(pipeline: NotifPipeline) { setupInvalidateNotifListCallbacks() @@ -107,385 +46,14 @@ constructor( pipeline.addFinalizeFilter(notifFilter) keyguardNotificationVisibilityProvider.addOnStateChangedListener(::invalidateListFromFilter) updateSectionHeadersVisibility() - attachUnseenFilter(pipeline) - } - - private fun attachUnseenFilter(pipeline: NotifPipeline) { - if (NotificationMinimalismPrototype.V2.isEnabled) { - pipeline.addPromoter(unseenNotifPromoter) - pipeline.addOnBeforeTransformGroupsListener(::pickOutTopUnseenNotifs) - } - pipeline.addFinalizeFilter(unseenNotifFilter) - pipeline.addCollectionListener(collectionListener) - scope.launch { trackUnseenFilterSettingChanges() } - dumpManager.registerDumpable(this) - } - - private suspend fun trackSeenNotifications() { - // Whether or not keyguard is visible (or occluded). - val isKeyguardPresent: Flow<Boolean> = - keyguardTransitionInteractor - .transitionValue( - scene = Scenes.Gone, - stateWithoutSceneContainer = KeyguardState.GONE, - ) - .map { it == 0f } - .distinctUntilChanged() - .onEach { trackingUnseen -> logger.logTrackingUnseen(trackingUnseen) } - - // Separately track seen notifications while the device is locked, applying once the device - // is unlocked. - val notificationsSeenWhileLocked = mutableSetOf<NotificationEntry>() - - // Use [collectLatest] to cancel any running jobs when [trackingUnseen] changes. - isKeyguardPresent.collectLatest { isKeyguardPresent: Boolean -> - if (isKeyguardPresent) { - // Keyguard is not gone, notifications need to be visible for a certain threshold - // before being marked as seen - trackSeenNotificationsWhileLocked(notificationsSeenWhileLocked) - } else { - // Mark all seen-while-locked notifications as seen for real. - if (notificationsSeenWhileLocked.isNotEmpty()) { - unseenNotifications.removeAll(notificationsSeenWhileLocked) - logger.logAllMarkedSeenOnUnlock( - seenCount = notificationsSeenWhileLocked.size, - remainingUnseenCount = unseenNotifications.size - ) - notificationsSeenWhileLocked.clear() - } - unseenNotifFilter.invalidateList("keyguard no longer showing") - // Keyguard is gone, notifications can be immediately marked as seen when they - // become visible. - trackSeenNotificationsWhileUnlocked() - } - } - } - - /** - * Keep [notificationsSeenWhileLocked] updated to represent which notifications have actually - * been "seen" while the device is on the keyguard. - */ - private suspend fun trackSeenNotificationsWhileLocked( - notificationsSeenWhileLocked: MutableSet<NotificationEntry>, - ) = coroutineScope { - // Remove removed notifications from the set - launch { - unseenEntryRemoved.collect { entry -> - if (notificationsSeenWhileLocked.remove(entry)) { - logger.logRemoveSeenOnLockscreen(entry) - } - } - } - // Use collectLatest so that the timeout delay is cancelled if the device enters doze, and - // is restarted when doze ends. - keyguardRepository.isDozing.collectLatest { isDozing -> - if (!isDozing) { - trackSeenNotificationsWhileLockedAndNotDozing(notificationsSeenWhileLocked) - } - } - } - - /** - * Keep [notificationsSeenWhileLocked] updated to represent which notifications have actually - * been "seen" while the device is on the keyguard and not dozing. Any new and existing unseen - * notifications are not marked as seen until they are visible for the [SEEN_TIMEOUT] duration. - */ - private suspend fun trackSeenNotificationsWhileLockedAndNotDozing( - notificationsSeenWhileLocked: MutableSet<NotificationEntry> - ) = coroutineScope { - // All child tracking jobs will be cancelled automatically when this is cancelled. - val trackingJobsByEntry = mutableMapOf<NotificationEntry, Job>() - - /** - * Wait for the user to spend enough time on the lock screen before removing notification - * from unseen set upon unlock. - */ - suspend fun trackSeenDurationThreshold(entry: NotificationEntry) { - if (notificationsSeenWhileLocked.remove(entry)) { - logger.logResetSeenOnLockscreen(entry) - } - delay(SEEN_TIMEOUT) - notificationsSeenWhileLocked.add(entry) - trackingJobsByEntry.remove(entry) - logger.logSeenOnLockscreen(entry) - } - - /** Stop any unseen tracking when a notification is removed. */ - suspend fun stopTrackingRemovedNotifs(): Nothing = - unseenEntryRemoved.collect { entry -> - trackingJobsByEntry.remove(entry)?.let { - it.cancel() - logger.logStopTrackingLockscreenSeenDuration(entry) - } - } - - /** Start tracking new notifications when they are posted. */ - suspend fun trackNewUnseenNotifs(): Nothing = coroutineScope { - unseenEntryAdded.collect { entry -> - logger.logTrackingLockscreenSeenDuration(entry) - // If this is an update, reset the tracking. - trackingJobsByEntry[entry]?.let { - it.cancel() - logger.logResetSeenOnLockscreen(entry) - } - trackingJobsByEntry[entry] = launch { trackSeenDurationThreshold(entry) } - } - } - - // Start tracking for all notifications that are currently unseen. - logger.logTrackingLockscreenSeenDuration(unseenNotifications) - unseenNotifications.forEach { entry -> - trackingJobsByEntry[entry] = launch { trackSeenDurationThreshold(entry) } - } - - launch { trackNewUnseenNotifs() } - launch { stopTrackingRemovedNotifs() } - } - - // Track "seen" notifications, marking them as such when either shade is expanded or the - // notification becomes heads up. - private suspend fun trackSeenNotificationsWhileUnlocked() { - coroutineScope { - launch { clearUnseenNotificationsWhenShadeIsExpanded() } - launch { markHeadsUpNotificationsAsSeen() } - } - } - - private suspend fun clearUnseenNotificationsWhenShadeIsExpanded() { - statusBarStateController.expansionChanges.collectLatest { isExpanded -> - // Give keyguard events time to propagate, in case this expansion is part of the - // keyguard transition and not the user expanding the shade - yield() - if (isExpanded) { - logger.logShadeExpanded() - unseenNotifications.clear() - } - } - } - - private suspend fun markHeadsUpNotificationsAsSeen() { - headsUpManager.allEntries - .filter { it.isRowPinned } - .forEach { unseenNotifications.remove(it) } - headsUpManager.headsUpEvents.collect { (entry, isHun) -> - if (isHun) { - logger.logUnseenHun(entry.key) - unseenNotifications.remove(entry) - } - } } - private fun unseenFeatureEnabled(): Flow<Boolean> { - if ( - NotificationMinimalismPrototype.V1.isEnabled || - NotificationMinimalismPrototype.V2.isEnabled - ) { - return flowOf(true) - } - return secureSettings - // emit whenever the setting has changed - .observerFlow( - UserHandle.USER_ALL, - Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, - ) - // perform a query immediately - .onStart { emit(Unit) } - // for each change, lookup the new value - .map { - secureSettings.getIntForUser( - name = Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, - def = 0, - userHandle = UserHandle.USER_CURRENT, - ) == 1 - } - // don't emit anything if nothing has changed - .distinctUntilChanged() - // perform lookups on the bg thread pool - .flowOn(bgDispatcher) - // only track the most recent emission, if events are happening faster than they can be - // consumed - .conflate() - } - - private suspend fun trackUnseenFilterSettingChanges() { - unseenFeatureEnabled().collectLatest { setting -> - // update local field and invalidate if necessary - if (setting != unseenFilterEnabled) { - unseenFilterEnabled = setting - unseenNotifFilter.invalidateList("unseen setting changed") - } - // if the setting is enabled, then start tracking and filtering unseen notifications - if (setting) { - trackSeenNotifications() - } - } - } - - private val collectionListener = - object : NotifCollectionListener { - override fun onEntryAdded(entry: NotificationEntry) { - if ( - keyguardRepository.isKeyguardShowing() || !statusBarStateController.isExpanded - ) { - logger.logUnseenAdded(entry.key) - unseenNotifications.add(entry) - unseenEntryAdded.tryEmit(entry) - } - } - - override fun onEntryUpdated(entry: NotificationEntry) { - if ( - keyguardRepository.isKeyguardShowing() || !statusBarStateController.isExpanded - ) { - logger.logUnseenUpdated(entry.key) - unseenNotifications.add(entry) - unseenEntryAdded.tryEmit(entry) - } - } - - override fun onEntryRemoved(entry: NotificationEntry, reason: Int) { - if (unseenNotifications.remove(entry)) { - logger.logUnseenRemoved(entry.key) - unseenEntryRemoved.tryEmit(entry) - } - } - } - - private fun pickOutTopUnseenNotifs(list: List<ListEntry>) { - if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) return - // Only ever elevate a top unseen notification on keyguard, not even locked shade - if (statusBarStateController.state != StatusBarState.KEYGUARD) { - seenNotificationsInteractor.setTopOngoingNotification(null) - seenNotificationsInteractor.setTopUnseenNotification(null) - return - } - // On keyguard pick the top-ranked unseen or ongoing notification to elevate - val nonSummaryEntries: Sequence<NotificationEntry> = - list - .asSequence() - .flatMap { - when (it) { - is NotificationEntry -> listOfNotNull(it) - is GroupEntry -> it.children - else -> error("unhandled type of $it") - } - } - .filter { it.importance >= NotificationManager.IMPORTANCE_DEFAULT } - seenNotificationsInteractor.setTopOngoingNotification( - nonSummaryEntries - .filter { ColorizedFgsCoordinator.isRichOngoing(it) } - .minByOrNull { it.ranking.rank } - ) - seenNotificationsInteractor.setTopUnseenNotification( - nonSummaryEntries - .filter { !ColorizedFgsCoordinator.isRichOngoing(it) && it in unseenNotifications } - .minByOrNull { it.ranking.rank } - ) - } - - @VisibleForTesting - internal val unseenNotifPromoter = - object : NotifPromoter("$TAG-unseen") { - override fun shouldPromoteToTopLevel(child: NotificationEntry): Boolean = - if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) false - else if (!NotificationMinimalismPrototype.V2.ungroupTopUnseen) false - else - seenNotificationsInteractor.isTopOngoingNotification(child) || - seenNotificationsInteractor.isTopUnseenNotification(child) - } - - val topOngoingSectioner = - object : NotifSectioner("TopOngoing", BUCKET_TOP_ONGOING) { - override fun isInSection(entry: ListEntry): Boolean { - if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) return false - return entry.anyEntry { notificationEntry -> - seenNotificationsInteractor.isTopOngoingNotification(notificationEntry) - } - } - } - - val topUnseenSectioner = - object : NotifSectioner("TopUnseen", BUCKET_TOP_UNSEEN) { - override fun isInSection(entry: ListEntry): Boolean { - if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) return false - return entry.anyEntry { notificationEntry -> - seenNotificationsInteractor.isTopUnseenNotification(notificationEntry) - } - } - } - - private fun ListEntry.anyEntry(predicate: (NotificationEntry?) -> Boolean) = - when { - predicate(representativeEntry) -> true - this !is GroupEntry -> false - else -> children.any(predicate) - } - - @VisibleForTesting - internal val unseenNotifFilter = - object : NotifFilter("$TAG-unseen") { - - var hasFilteredAnyNotifs = false - - /** - * Encapsulates a definition of "being on the keyguard". Note that these two definitions - * are wildly different: [StatusBarState.KEYGUARD] is when on the lock screen and does - * not include shade or occluded states, whereas [KeyguardRepository.isKeyguardShowing] - * is any state where the keyguard has not been dismissed, including locked shade and - * occluded lock screen. - * - * Returning false for locked shade and occluded states means that this filter will - * allow seen notifications to appear in the locked shade. - */ - private fun isOnKeyguard(): Boolean = - if (NotificationMinimalismPrototype.V2.isEnabled) { - false // disable this feature under this prototype - } else if ( - NotificationMinimalismPrototype.V1.isEnabled && - NotificationMinimalismPrototype.V1.showOnLockedShade - ) { - statusBarStateController.state == StatusBarState.KEYGUARD - } else { - keyguardRepository.isKeyguardShowing() - } - - override fun shouldFilterOut(entry: NotificationEntry, now: Long): Boolean = - when { - // Don't apply filter if the setting is disabled - !unseenFilterEnabled -> false - // Don't apply filter if the keyguard isn't currently showing - !isOnKeyguard() -> false - // Don't apply the filter if the notification is unseen - unseenNotifications.contains(entry) -> false - // Don't apply the filter to (non-promoted) group summaries - // - summary will be pruned if necessary, depending on if children are filtered - entry.parent?.summary == entry -> false - // Check that the entry satisfies certain characteristics that would bypass the - // filter - shouldIgnoreUnseenCheck(entry) -> false - else -> true - }.also { hasFiltered -> hasFilteredAnyNotifs = hasFilteredAnyNotifs || hasFiltered } - - override fun onCleanup() { - logger.logProviderHasFilteredOutSeenNotifs(hasFilteredAnyNotifs) - seenNotificationsInteractor.setHasFilteredOutSeenNotifications(hasFilteredAnyNotifs) - hasFilteredAnyNotifs = false - } - } - private val notifFilter: NotifFilter = object : NotifFilter(TAG) { override fun shouldFilterOut(entry: NotificationEntry, now: Long): Boolean = keyguardNotificationVisibilityProvider.shouldHideNotification(entry) } - private fun shouldIgnoreUnseenCheck(entry: NotificationEntry): Boolean = - when { - entry.isMediaNotification -> true - entry.sbn.isOngoing -> true - else -> false - } - // TODO(b/206118999): merge this class with SensitiveContentCoordinator which also depends on // these same updates private fun setupInvalidateNotifListCallbacks() {} @@ -502,22 +70,7 @@ constructor( sectionHeaderVisibilityProvider.sectionHeadersVisible = showSections } - override fun dump(pw: PrintWriter, args: Array<out String>) = - with(pw.asIndenting()) { - println( - "notificationListInteractor.hasFilteredOutSeenNotifications.value=" + - seenNotificationsInteractor.hasFilteredOutSeenNotifications.value - ) - println("unseen notifications:") - indentIfPossible { - for (notification in unseenNotifications) { - println(notification.key) - } - } - } - companion object { private const val TAG = "KeyguardCoordinator" - private val SEEN_TIMEOUT = 5.seconds } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt new file mode 100644 index 000000000000..a6605f652ff3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt @@ -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.systemui.statusbar.notification.collection.coordinator + +import android.annotation.SuppressLint +import android.app.NotificationManager +import android.os.UserHandle +import android.provider.Settings +import androidx.annotation.VisibleForTesting +import com.android.systemui.Dumpable +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dump.DumpManager +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.statusbar.StatusBarState +import com.android.systemui.statusbar.notification.collection.GroupEntry +import com.android.systemui.statusbar.notification.collection.ListEntry +import com.android.systemui.statusbar.notification.collection.NotifPipeline +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope +import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter +import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner +import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener +import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor +import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor +import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype +import com.android.systemui.statusbar.notification.stack.BUCKET_TOP_ONGOING +import com.android.systemui.statusbar.notification.stack.BUCKET_TOP_UNSEEN +import com.android.systemui.util.asIndenting +import com.android.systemui.util.printCollection +import com.android.systemui.util.settings.SecureSettings +import com.android.systemui.util.settings.SettingsProxyExt.observerFlow +import java.io.PrintWriter +import javax.inject.Inject +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.launch + +/** + * If the setting is enabled, this will track seen notifications and ensure that they only show in + * the shelf on the lockscreen. + * + * This class is a replacement of the [OriginalUnseenKeyguardCoordinator]. + */ +@CoordinatorScope +@SuppressLint("SharedFlowCreation") +class LockScreenMinimalismCoordinator +@Inject +constructor( + @Background private val bgDispatcher: CoroutineDispatcher, + private val dumpManager: DumpManager, + private val headsUpInteractor: HeadsUpNotificationInteractor, + private val logger: LockScreenMinimalismCoordinatorLogger, + @Application private val scope: CoroutineScope, + private val secureSettings: SecureSettings, + private val seenNotificationsInteractor: SeenNotificationsInteractor, + private val statusBarStateController: StatusBarStateController, + private val shadeInteractor: ShadeInteractor, +) : Coordinator, Dumpable { + + private val unseenNotifications = mutableSetOf<NotificationEntry>() + private var isShadeVisible = false + private var unseenFilterEnabled = false + + override fun attach(pipeline: NotifPipeline) { + if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) { + return + } + pipeline.addPromoter(unseenNotifPromoter) + pipeline.addOnBeforeTransformGroupsListener(::pickOutTopUnseenNotifs) + pipeline.addCollectionListener(collectionListener) + scope.launch { trackUnseenFilterSettingChanges() } + dumpManager.registerDumpable(this) + } + + private suspend fun trackSeenNotifications() { + coroutineScope { + launch { clearUnseenNotificationsWhenShadeIsExpanded() } + launch { markHeadsUpNotificationsAsSeen() } + } + } + + private suspend fun clearUnseenNotificationsWhenShadeIsExpanded() { + shadeInteractor.isShadeFullyExpanded.collectLatest { isExpanded -> + // Give keyguard events time to propagate, in case this expansion is part of the + // keyguard transition and not the user expanding the shade + delay(SHADE_VISIBLE_SEEN_TIMEOUT) + isShadeVisible = isExpanded + if (isExpanded) { + logger.logShadeVisible(unseenNotifications.size) + unseenNotifications.clear() + // no need to invalidateList; filtering is inactive while shade is open + } else { + logger.logShadeHidden() + } + } + } + + private suspend fun markHeadsUpNotificationsAsSeen() { + headsUpInteractor.topHeadsUpRowIfPinned + .map { it?.let { headsUpInteractor.notificationKey(it) } } + .collectLatest { key -> + if (key == null) { + logger.logTopHeadsUpRow(key = null, wasUnseenWhenPinned = false) + } else { + val wasUnseenWhenPinned = unseenNotifications.any { it.key == key } + logger.logTopHeadsUpRow(key, wasUnseenWhenPinned) + if (wasUnseenWhenPinned) { + delay(HEADS_UP_SEEN_TIMEOUT) + val wasUnseenAfterDelay = unseenNotifications.removeIf { it.key == key } + logger.logHunHasBeenSeen(key, wasUnseenAfterDelay) + // no need to invalidateList; nothing should change until after heads up + } + } + } + } + + private fun unseenFeatureEnabled(): Flow<Boolean> { + // TODO(b/330387368): create LOCK_SCREEN_NOTIFICATION_MINIMALISM setting to use here? + // Or should we actually just repurpose using the existing setting? + if (NotificationMinimalismPrototype.isEnabled) { + return flowOf(true) + } + return secureSettings + // emit whenever the setting has changed + .observerFlow( + UserHandle.USER_ALL, + Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, + ) + // perform a query immediately + .onStart { emit(Unit) } + // for each change, lookup the new value + .map { + secureSettings.getIntForUser( + name = Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, + def = 0, + userHandle = UserHandle.USER_CURRENT, + ) == 1 + } + // don't emit anything if nothing has changed + .distinctUntilChanged() + // perform lookups on the bg thread pool + .flowOn(bgDispatcher) + // only track the most recent emission, if events are happening faster than they can be + // consumed + .conflate() + } + + private suspend fun trackUnseenFilterSettingChanges() { + unseenFeatureEnabled().collectLatest { isSettingEnabled -> + // update local field and invalidate if necessary + if (isSettingEnabled != unseenFilterEnabled) { + unseenFilterEnabled = isSettingEnabled + unseenNotifPromoter.invalidateList("unseen setting changed") + } + // if the setting is enabled, then start tracking and filtering unseen notifications + logger.logTrackingUnseen(isSettingEnabled) + if (isSettingEnabled) { + trackSeenNotifications() + } + } + } + + private val collectionListener = + object : NotifCollectionListener { + override fun onEntryAdded(entry: NotificationEntry) { + if (!isShadeVisible) { + logger.logUnseenAdded(entry.key) + unseenNotifications.add(entry) + } + } + + override fun onEntryUpdated(entry: NotificationEntry) { + if (!isShadeVisible) { + logger.logUnseenUpdated(entry.key) + unseenNotifications.add(entry) + } + } + + override fun onEntryRemoved(entry: NotificationEntry, reason: Int) { + if (unseenNotifications.remove(entry)) { + logger.logUnseenRemoved(entry.key) + } + } + } + + private fun pickOutTopUnseenNotifs(list: List<ListEntry>) { + if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return + // Only ever elevate a top unseen notification on keyguard, not even locked shade + if (statusBarStateController.state != StatusBarState.KEYGUARD) { + seenNotificationsInteractor.setTopOngoingNotification(null) + seenNotificationsInteractor.setTopUnseenNotification(null) + return + } + // On keyguard pick the top-ranked unseen or ongoing notification to elevate + val nonSummaryEntries: Sequence<NotificationEntry> = + list + .asSequence() + .flatMap { + when (it) { + is NotificationEntry -> listOfNotNull(it) + is GroupEntry -> it.children + else -> error("unhandled type of $it") + } + } + .filter { it.importance >= NotificationManager.IMPORTANCE_DEFAULT } + seenNotificationsInteractor.setTopOngoingNotification( + nonSummaryEntries + .filter { ColorizedFgsCoordinator.isRichOngoing(it) } + .minByOrNull { it.ranking.rank } + ) + seenNotificationsInteractor.setTopUnseenNotification( + nonSummaryEntries + .filter { !ColorizedFgsCoordinator.isRichOngoing(it) && it in unseenNotifications } + .minByOrNull { it.ranking.rank } + ) + } + + @VisibleForTesting + val unseenNotifPromoter = + object : NotifPromoter(TAG) { + override fun shouldPromoteToTopLevel(child: NotificationEntry): Boolean = + when { + NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode() -> false + seenNotificationsInteractor.isTopOngoingNotification(child) -> true + !NotificationMinimalismPrototype.ungroupTopUnseen -> false + else -> seenNotificationsInteractor.isTopUnseenNotification(child) + } + } + + val topOngoingSectioner = + object : NotifSectioner("TopOngoing", BUCKET_TOP_ONGOING) { + override fun isInSection(entry: ListEntry): Boolean { + if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return false + return entry.anyEntry { notificationEntry -> + seenNotificationsInteractor.isTopOngoingNotification(notificationEntry) + } + } + } + + val topUnseenSectioner = + object : NotifSectioner("TopUnseen", BUCKET_TOP_UNSEEN) { + override fun isInSection(entry: ListEntry): Boolean { + if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return false + return entry.anyEntry { notificationEntry -> + seenNotificationsInteractor.isTopUnseenNotification(notificationEntry) + } + } + } + + private fun ListEntry.anyEntry(predicate: (NotificationEntry?) -> Boolean) = + when { + predicate(representativeEntry) -> true + this !is GroupEntry -> false + else -> children.any(predicate) + } + + override fun dump(pw: PrintWriter, args: Array<out String>) = + with(pw.asIndenting()) { + seenNotificationsInteractor.dump(this) + printCollection("unseen notifications", unseenNotifications) { println(it.key) } + } + + companion object { + private const val TAG = "LockScreenMinimalismCoordinator" + private val SHADE_VISIBLE_SEEN_TIMEOUT = 0.25.seconds + private val HEADS_UP_SEEN_TIMEOUT = 0.75.seconds + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLogger.kt new file mode 100644 index 000000000000..e44a77c30999 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLogger.kt @@ -0,0 +1,99 @@ +/* + * 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.statusbar.notification.collection.coordinator + +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.core.LogLevel +import com.android.systemui.log.dagger.UnseenNotificationLog +import javax.inject.Inject + +private const val TAG = "LockScreenMinimalismCoordinator" + +class LockScreenMinimalismCoordinatorLogger +@Inject +constructor( + @UnseenNotificationLog private val buffer: LogBuffer, +) { + + fun logTrackingUnseen(trackingUnseen: Boolean) = + buffer.log( + TAG, + LogLevel.DEBUG, + messageInitializer = { bool1 = trackingUnseen }, + messagePrinter = { "${if (bool1) "Start" else "Stop"} tracking unseen notifications." }, + ) + + fun logShadeVisible(numUnseen: Int) { + buffer.log( + TAG, + LogLevel.DEBUG, + messageInitializer = { int1 = numUnseen }, + messagePrinter = { "Shade expanded. Notifications marked as seen: $int1" } + ) + } + + fun logShadeHidden() { + buffer.log(TAG, LogLevel.DEBUG, "Shade no longer expanded.") + } + + fun logUnseenAdded(key: String) = + buffer.log( + TAG, + LogLevel.DEBUG, + messageInitializer = { str1 = key }, + messagePrinter = { "Unseen notif added: $str1" }, + ) + + fun logUnseenUpdated(key: String) = + buffer.log( + TAG, + LogLevel.DEBUG, + messageInitializer = { str1 = key }, + messagePrinter = { "Unseen notif updated: $str1" }, + ) + + fun logUnseenRemoved(key: String) = + buffer.log( + TAG, + LogLevel.DEBUG, + messageInitializer = { str1 = key }, + messagePrinter = { "Unseen notif removed: $str1" }, + ) + + fun logHunHasBeenSeen(key: String, wasUnseen: Boolean) = + buffer.log( + TAG, + LogLevel.DEBUG, + messageInitializer = { + str1 = key + bool1 = wasUnseen + }, + messagePrinter = { "Heads up notif has been seen: $str1 wasUnseen=$bool1" }, + ) + + fun logTopHeadsUpRow(key: String?, wasUnseenWhenPinned: Boolean) { + buffer.log( + TAG, + LogLevel.DEBUG, + messageInitializer = { + str1 = key + bool1 = wasUnseenWhenPinned + }, + messagePrinter = { "New notif is top heads up: $str1 wasUnseen=$bool1" }, + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt index e0389820aedf..73ce48b2324a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt @@ -17,7 +17,11 @@ package com.android.systemui.statusbar.notification.collection.coordinator import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED -import com.android.systemui.statusbar.notification.collection.* +import com.android.systemui.statusbar.notification.collection.NotifPipeline +import com.android.systemui.statusbar.notification.collection.NotificationClassificationFlag +import com.android.systemui.statusbar.notification.collection.PipelineDumpable +import com.android.systemui.statusbar.notification.collection.PipelineDumper +import com.android.systemui.statusbar.notification.collection.SortBySectionTimeFlag import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider @@ -42,6 +46,8 @@ constructor( hideLocallyDismissedNotifsCoordinator: HideLocallyDismissedNotifsCoordinator, hideNotifsForOtherUsersCoordinator: HideNotifsForOtherUsersCoordinator, keyguardCoordinator: KeyguardCoordinator, + unseenKeyguardCoordinator: OriginalUnseenKeyguardCoordinator, + lockScreenMinimalismCoordinator: LockScreenMinimalismCoordinator, rankingCoordinator: RankingCoordinator, colorizedFgsCoordinator: ColorizedFgsCoordinator, deviceProvisionedCoordinator: DeviceProvisionedCoordinator, @@ -82,6 +88,11 @@ constructor( mCoordinators.add(hideLocallyDismissedNotifsCoordinator) mCoordinators.add(hideNotifsForOtherUsersCoordinator) mCoordinators.add(keyguardCoordinator) + if (NotificationMinimalismPrototype.isEnabled) { + mCoordinators.add(lockScreenMinimalismCoordinator) + } else { + mCoordinators.add(unseenKeyguardCoordinator) + } mCoordinators.add(rankingCoordinator) mCoordinators.add(colorizedFgsCoordinator) mCoordinators.add(deviceProvisionedCoordinator) @@ -114,12 +125,12 @@ constructor( } // Manually add Ordered Sections - if (NotificationMinimalismPrototype.V2.isEnabled) { - mOrderedSections.add(keyguardCoordinator.topOngoingSectioner) // Top Ongoing + if (NotificationMinimalismPrototype.isEnabled) { + mOrderedSections.add(lockScreenMinimalismCoordinator.topOngoingSectioner) // Top Ongoing } mOrderedSections.add(headsUpCoordinator.sectioner) // HeadsUp - if (NotificationMinimalismPrototype.V2.isEnabled) { - mOrderedSections.add(keyguardCoordinator.topUnseenSectioner) // Top Unseen + if (NotificationMinimalismPrototype.isEnabled) { + mOrderedSections.add(lockScreenMinimalismCoordinator.topUnseenSectioner) // Top Unseen } mOrderedSections.add(colorizedFgsCoordinator.sectioner) // ForegroundService if (PriorityPeopleSection.isEnabled) { @@ -131,10 +142,10 @@ constructor( } mOrderedSections.add(rankingCoordinator.alertingSectioner) // Alerting if (NotificationClassificationFlag.isEnabled) { - mOrderedSections.add(bundleCoordinator.newsSectioner); - mOrderedSections.add(bundleCoordinator.socialSectioner); - mOrderedSections.add(bundleCoordinator.recsSectioner); - mOrderedSections.add(bundleCoordinator.promoSectioner); + mOrderedSections.add(bundleCoordinator.newsSectioner) + mOrderedSections.add(bundleCoordinator.socialSectioner) + mOrderedSections.add(bundleCoordinator.recsSectioner) + mOrderedSections.add(bundleCoordinator.promoSectioner) } mOrderedSections.add(rankingCoordinator.silentSectioner) // Silent mOrderedSections.add(rankingCoordinator.minimizedSectioner) // Minimized diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt new file mode 100644 index 000000000000..5b25b117c761 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt @@ -0,0 +1,389 @@ +/* + * 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.statusbar.notification.collection.coordinator + +import android.annotation.SuppressLint +import android.os.UserHandle +import android.provider.Settings +import androidx.annotation.VisibleForTesting +import com.android.systemui.Dumpable +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dump.DumpManager +import com.android.systemui.keyguard.data.repository.KeyguardRepository +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.statusbar.expansionChanges +import com.android.systemui.statusbar.notification.collection.NotifPipeline +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope +import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter +import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener +import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor +import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype +import com.android.systemui.statusbar.policy.HeadsUpManager +import com.android.systemui.statusbar.policy.headsUpEvents +import com.android.systemui.util.asIndenting +import com.android.systemui.util.indentIfPossible +import com.android.systemui.util.settings.SecureSettings +import com.android.systemui.util.settings.SettingsProxyExt.observerFlow +import java.io.PrintWriter +import javax.inject.Inject +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.launch +import kotlinx.coroutines.yield + +/** + * If the setting is enabled, this will track and hide seen notifications on the lockscreen. + * + * This is the "original" unseen keyguard coordinator because this is the logic originally developed + * for large screen devices where showing "seen" notifications on the lock screen was distracting. + * Moreover, this file was created during a project that will replace this logic, so the + * [LockScreenMinimalismCoordinator] is the expected replacement of this file. + */ +@CoordinatorScope +@SuppressLint("SharedFlowCreation") +class OriginalUnseenKeyguardCoordinator +@Inject +constructor( + @Background private val bgDispatcher: CoroutineDispatcher, + private val dumpManager: DumpManager, + private val headsUpManager: HeadsUpManager, + private val keyguardRepository: KeyguardRepository, + private val keyguardTransitionInteractor: KeyguardTransitionInteractor, + private val logger: KeyguardCoordinatorLogger, + @Application private val scope: CoroutineScope, + private val secureSettings: SecureSettings, + private val seenNotificationsInteractor: SeenNotificationsInteractor, + private val statusBarStateController: StatusBarStateController, +) : Coordinator, Dumpable { + + private val unseenNotifications = mutableSetOf<NotificationEntry>() + private val unseenEntryAdded = MutableSharedFlow<NotificationEntry>(extraBufferCapacity = 1) + private val unseenEntryRemoved = MutableSharedFlow<NotificationEntry>(extraBufferCapacity = 1) + private var unseenFilterEnabled = false + + override fun attach(pipeline: NotifPipeline) { + NotificationMinimalismPrototype.assertInLegacyMode() + pipeline.addFinalizeFilter(unseenNotifFilter) + pipeline.addCollectionListener(collectionListener) + scope.launch { trackUnseenFilterSettingChanges() } + dumpManager.registerDumpable(this) + } + + private suspend fun trackSeenNotifications() { + // Whether or not keyguard is visible (or occluded). + @Suppress("DEPRECATION") + val isKeyguardPresentFlow: Flow<Boolean> = + keyguardTransitionInteractor + .transitionValue( + scene = Scenes.Gone, + stateWithoutSceneContainer = KeyguardState.GONE, + ) + .map { it == 0f } + .distinctUntilChanged() + .onEach { trackingUnseen -> logger.logTrackingUnseen(trackingUnseen) } + + // Separately track seen notifications while the device is locked, applying once the device + // is unlocked. + val notificationsSeenWhileLocked = mutableSetOf<NotificationEntry>() + + // Use [collectLatest] to cancel any running jobs when [trackingUnseen] changes. + isKeyguardPresentFlow.collectLatest { isKeyguardPresent: Boolean -> + if (isKeyguardPresent) { + // Keyguard is not gone, notifications need to be visible for a certain threshold + // before being marked as seen + trackSeenNotificationsWhileLocked(notificationsSeenWhileLocked) + } else { + // Mark all seen-while-locked notifications as seen for real. + if (notificationsSeenWhileLocked.isNotEmpty()) { + unseenNotifications.removeAll(notificationsSeenWhileLocked) + logger.logAllMarkedSeenOnUnlock( + seenCount = notificationsSeenWhileLocked.size, + remainingUnseenCount = unseenNotifications.size + ) + notificationsSeenWhileLocked.clear() + } + unseenNotifFilter.invalidateList("keyguard no longer showing") + // Keyguard is gone, notifications can be immediately marked as seen when they + // become visible. + trackSeenNotificationsWhileUnlocked() + } + } + } + + /** + * Keep [notificationsSeenWhileLocked] updated to represent which notifications have actually + * been "seen" while the device is on the keyguard. + */ + private suspend fun trackSeenNotificationsWhileLocked( + notificationsSeenWhileLocked: MutableSet<NotificationEntry>, + ) = coroutineScope { + // Remove removed notifications from the set + launch { + unseenEntryRemoved.collect { entry -> + if (notificationsSeenWhileLocked.remove(entry)) { + logger.logRemoveSeenOnLockscreen(entry) + } + } + } + // Use collectLatest so that the timeout delay is cancelled if the device enters doze, and + // is restarted when doze ends. + keyguardRepository.isDozing.collectLatest { isDozing -> + if (!isDozing) { + trackSeenNotificationsWhileLockedAndNotDozing(notificationsSeenWhileLocked) + } + } + } + + /** + * Keep [notificationsSeenWhileLocked] updated to represent which notifications have actually + * been "seen" while the device is on the keyguard and not dozing. Any new and existing unseen + * notifications are not marked as seen until they are visible for the [SEEN_TIMEOUT] duration. + */ + private suspend fun trackSeenNotificationsWhileLockedAndNotDozing( + notificationsSeenWhileLocked: MutableSet<NotificationEntry> + ) = coroutineScope { + // All child tracking jobs will be cancelled automatically when this is cancelled. + val trackingJobsByEntry = mutableMapOf<NotificationEntry, Job>() + + /** + * Wait for the user to spend enough time on the lock screen before removing notification + * from unseen set upon unlock. + */ + suspend fun trackSeenDurationThreshold(entry: NotificationEntry) { + if (notificationsSeenWhileLocked.remove(entry)) { + logger.logResetSeenOnLockscreen(entry) + } + delay(SEEN_TIMEOUT) + notificationsSeenWhileLocked.add(entry) + trackingJobsByEntry.remove(entry) + logger.logSeenOnLockscreen(entry) + } + + /** Stop any unseen tracking when a notification is removed. */ + suspend fun stopTrackingRemovedNotifs(): Nothing = + unseenEntryRemoved.collect { entry -> + trackingJobsByEntry.remove(entry)?.let { + it.cancel() + logger.logStopTrackingLockscreenSeenDuration(entry) + } + } + + /** Start tracking new notifications when they are posted. */ + suspend fun trackNewUnseenNotifs(): Nothing = coroutineScope { + unseenEntryAdded.collect { entry -> + logger.logTrackingLockscreenSeenDuration(entry) + // If this is an update, reset the tracking. + trackingJobsByEntry[entry]?.let { + it.cancel() + logger.logResetSeenOnLockscreen(entry) + } + trackingJobsByEntry[entry] = launch { trackSeenDurationThreshold(entry) } + } + } + + // Start tracking for all notifications that are currently unseen. + logger.logTrackingLockscreenSeenDuration(unseenNotifications) + unseenNotifications.forEach { entry -> + trackingJobsByEntry[entry] = launch { trackSeenDurationThreshold(entry) } + } + + launch { trackNewUnseenNotifs() } + launch { stopTrackingRemovedNotifs() } + } + + // Track "seen" notifications, marking them as such when either shade is expanded or the + // notification becomes heads up. + private suspend fun trackSeenNotificationsWhileUnlocked() { + coroutineScope { + launch { clearUnseenNotificationsWhenShadeIsExpanded() } + launch { markHeadsUpNotificationsAsSeen() } + } + } + + private suspend fun clearUnseenNotificationsWhenShadeIsExpanded() { + statusBarStateController.expansionChanges.collectLatest { isExpanded -> + // Give keyguard events time to propagate, in case this expansion is part of the + // keyguard transition and not the user expanding the shade + yield() + if (isExpanded) { + logger.logShadeExpanded() + unseenNotifications.clear() + } + } + } + + private suspend fun markHeadsUpNotificationsAsSeen() { + headsUpManager.allEntries + .filter { it.isRowPinned } + .forEach { unseenNotifications.remove(it) } + headsUpManager.headsUpEvents.collect { (entry, isHun) -> + if (isHun) { + logger.logUnseenHun(entry.key) + unseenNotifications.remove(entry) + } + } + } + + private fun unseenFeatureEnabled(): Flow<Boolean> { + if (NotificationMinimalismPrototype.isEnabled) { + // TODO(b/330387368): should this really just be turned off? If so, hide the setting. + return flowOf(false) + } + return secureSettings + // emit whenever the setting has changed + .observerFlow( + UserHandle.USER_ALL, + Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, + ) + // perform a query immediately + .onStart { emit(Unit) } + // for each change, lookup the new value + .map { + secureSettings.getIntForUser( + name = Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, + def = 0, + userHandle = UserHandle.USER_CURRENT, + ) == 1 + } + // don't emit anything if nothing has changed + .distinctUntilChanged() + // perform lookups on the bg thread pool + .flowOn(bgDispatcher) + // only track the most recent emission, if events are happening faster than they can be + // consumed + .conflate() + } + + private suspend fun trackUnseenFilterSettingChanges() { + unseenFeatureEnabled().collectLatest { setting -> + // update local field and invalidate if necessary + if (setting != unseenFilterEnabled) { + unseenFilterEnabled = setting + unseenNotifFilter.invalidateList("unseen setting changed") + } + // if the setting is enabled, then start tracking and filtering unseen notifications + if (setting) { + trackSeenNotifications() + } + } + } + + private val collectionListener = + object : NotifCollectionListener { + override fun onEntryAdded(entry: NotificationEntry) { + if ( + keyguardRepository.isKeyguardShowing() || !statusBarStateController.isExpanded + ) { + logger.logUnseenAdded(entry.key) + unseenNotifications.add(entry) + unseenEntryAdded.tryEmit(entry) + } + } + + override fun onEntryUpdated(entry: NotificationEntry) { + if ( + keyguardRepository.isKeyguardShowing() || !statusBarStateController.isExpanded + ) { + logger.logUnseenUpdated(entry.key) + unseenNotifications.add(entry) + unseenEntryAdded.tryEmit(entry) + } + } + + override fun onEntryRemoved(entry: NotificationEntry, reason: Int) { + if (unseenNotifications.remove(entry)) { + logger.logUnseenRemoved(entry.key) + unseenEntryRemoved.tryEmit(entry) + } + } + } + + @VisibleForTesting + val unseenNotifFilter = + object : NotifFilter(TAG) { + + var hasFilteredAnyNotifs = false + + override fun shouldFilterOut(entry: NotificationEntry, now: Long): Boolean = + when { + // Don't apply filter if the setting is disabled + !unseenFilterEnabled -> false + // Don't apply filter if the keyguard isn't currently showing + !keyguardRepository.isKeyguardShowing() -> false + // Don't apply the filter if the notification is unseen + unseenNotifications.contains(entry) -> false + // Don't apply the filter to (non-promoted) group summaries + // - summary will be pruned if necessary, depending on if children are filtered + entry.parent?.summary == entry -> false + // Check that the entry satisfies certain characteristics that would bypass the + // filter + shouldIgnoreUnseenCheck(entry) -> false + else -> true + }.also { hasFiltered -> hasFilteredAnyNotifs = hasFilteredAnyNotifs || hasFiltered } + + override fun onCleanup() { + logger.logProviderHasFilteredOutSeenNotifs(hasFilteredAnyNotifs) + seenNotificationsInteractor.setHasFilteredOutSeenNotifications(hasFilteredAnyNotifs) + hasFilteredAnyNotifs = false + } + } + + private fun shouldIgnoreUnseenCheck(entry: NotificationEntry): Boolean = + when { + entry.isMediaNotification -> true + entry.sbn.isOngoing -> true + else -> false + } + + override fun dump(pw: PrintWriter, args: Array<out String>) = + with(pw.asIndenting()) { + println( + "notificationListInteractor.hasFilteredOutSeenNotifications.value=" + + seenNotificationsInteractor.hasFilteredOutSeenNotifications.value + ) + println("unseen notifications:") + indentIfPossible { + for (notification in unseenNotifications) { + println(notification.key) + } + } + } + + companion object { + private const val TAG = "OriginalUnseenKeyguardCoordinator" + private val SEEN_TIMEOUT = 5.seconds + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java index caa6c17ac3d2..696298e82db9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java @@ -18,8 +18,6 @@ package com.android.systemui.statusbar.notification.collection.coordinator; import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_ASLEEP; -import android.util.Log; - import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; @@ -29,7 +27,10 @@ import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dump.DumpManager; import com.android.systemui.keyguard.WakefulnessLifecycle; +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; +import com.android.systemui.keyguard.shared.model.KeyguardState; import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.scene.shared.flag.SceneContainerFlag; import com.android.systemui.shade.domain.interactor.ShadeAnimationInteractor; import com.android.systemui.statusbar.notification.VisibilityLocationProvider; import com.android.systemui.statusbar.notification.collection.GroupEntry; @@ -41,7 +42,6 @@ import com.android.systemui.statusbar.notification.collection.provider.VisualSta import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor; import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype; import com.android.systemui.statusbar.policy.HeadsUpManager; -import com.android.systemui.util.Compile; import com.android.systemui.util.concurrency.DelayableExecutor; import com.android.systemui.util.kotlin.JavaAdapter; @@ -61,8 +61,6 @@ import javax.inject.Inject; // TODO(b/204468557): Move to @CoordinatorScope @SysUISingleton public class VisualStabilityCoordinator implements Coordinator, Dumpable { - public static final String TAG = "VisualStability"; - public static final boolean DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.VERBOSE); private final DelayableExecutor mDelayableExecutor; private final HeadsUpManager mHeadsUpManager; private final SeenNotificationsInteractor mSeenNotificationsInteractor; @@ -73,6 +71,8 @@ public class VisualStabilityCoordinator implements Coordinator, Dumpable { private final VisualStabilityProvider mVisualStabilityProvider; private final WakefulnessLifecycle mWakefulnessLifecycle; private final CommunalInteractor mCommunalInteractor; + private final KeyguardTransitionInteractor mKeyguardTransitionInteractor; + private final VisualStabilityCoordinatorLogger mLogger; private boolean mSleepy = true; private boolean mFullyDozed; @@ -81,6 +81,7 @@ public class VisualStabilityCoordinator implements Coordinator, Dumpable { private boolean mNotifPanelCollapsing; private boolean mNotifPanelLaunchingActivity; private boolean mCommunalShowing = false; + private boolean mLockscreenShowing = false; private boolean mPipelineRunAllowed; private boolean mReorderingAllowed; @@ -109,7 +110,9 @@ public class VisualStabilityCoordinator implements Coordinator, Dumpable { VisibilityLocationProvider visibilityLocationProvider, VisualStabilityProvider visualStabilityProvider, WakefulnessLifecycle wakefulnessLifecycle, - CommunalInteractor communalInteractor) { + CommunalInteractor communalInteractor, + KeyguardTransitionInteractor keyguardTransitionInteractor, + VisualStabilityCoordinatorLogger logger) { mHeadsUpManager = headsUpManager; mShadeAnimationInteractor = shadeAnimationInteractor; mJavaAdapter = javaAdapter; @@ -120,6 +123,8 @@ public class VisualStabilityCoordinator implements Coordinator, Dumpable { mStatusBarStateController = statusBarStateController; mDelayableExecutor = delayableExecutor; mCommunalInteractor = communalInteractor; + mKeyguardTransitionInteractor = keyguardTransitionInteractor; + mLogger = logger; dumpManager.registerDumpable(this); } @@ -138,6 +143,9 @@ public class VisualStabilityCoordinator implements Coordinator, Dumpable { this::onLaunchingActivityChanged); mJavaAdapter.alwaysCollectFlow(mCommunalInteractor.isIdleOnCommunal(), this::onCommunalShowingChanged); + mJavaAdapter.alwaysCollectFlow(mKeyguardTransitionInteractor.transitionValue( + KeyguardState.LOCKSCREEN), + this::onLockscreenKeyguardStateTransitionValueChanged); pipeline.setVisualStabilityManager(mNotifStabilityManager); } @@ -150,8 +158,9 @@ public class VisualStabilityCoordinator implements Coordinator, Dumpable { if (entry == null) { return false; } - boolean isTopUnseen = NotificationMinimalismPrototype.V2.isEnabled() - && mSeenNotificationsInteractor.isTopUnseenNotification(entry); + boolean isTopUnseen = NotificationMinimalismPrototype.isEnabled() + && (mSeenNotificationsInteractor.isTopUnseenNotification(entry) + || mSeenNotificationsInteractor.isTopOngoingNotification(entry)); if (isTopUnseen || mHeadsUpManager.isHeadsUpEntry(entry.getKey())) { return !mVisibilityLocationProvider.isInVisibleLocation(entry); } @@ -220,12 +229,12 @@ public class VisualStabilityCoordinator implements Coordinator, Dumpable { boolean wasReorderingAllowed = mReorderingAllowed; mPipelineRunAllowed = !isPanelCollapsingOrLaunchingActivity(); mReorderingAllowed = isReorderingAllowed(); - if (DEBUG && (wasPipelineRunAllowed != mPipelineRunAllowed - || wasReorderingAllowed != mReorderingAllowed)) { - Log.d(TAG, "Stability allowances changed:" - + " pipelineRunAllowed " + wasPipelineRunAllowed + "->" + mPipelineRunAllowed - + " reorderingAllowed " + wasReorderingAllowed + "->" + mReorderingAllowed - + " when setting " + field + "=" + value); + if (wasPipelineRunAllowed != mPipelineRunAllowed + || wasReorderingAllowed != mReorderingAllowed) { + mLogger.logAllowancesChanged( + wasPipelineRunAllowed, mPipelineRunAllowed, + wasReorderingAllowed, mReorderingAllowed, + field, value); } if (mPipelineRunAllowed && mIsSuppressingPipelineRun) { mNotifStabilityManager.invalidateList("pipeline run suppression ended"); @@ -250,7 +259,9 @@ public class VisualStabilityCoordinator implements Coordinator, Dumpable { } private boolean isReorderingAllowed() { - return ((mFullyDozed && mSleepy) || !mPanelExpanded || mCommunalShowing) && !mPulsing; + final boolean sleepyAndDozed = mFullyDozed && mSleepy; + final boolean stackShowing = mPanelExpanded || mLockscreenShowing; + return (sleepyAndDozed || !stackShowing || mCommunalShowing) && !mPulsing; } /** @@ -363,4 +374,14 @@ public class VisualStabilityCoordinator implements Coordinator, Dumpable { mCommunalShowing = isShowing; updateAllowedStates("communalShowing", isShowing); } + + private void onLockscreenKeyguardStateTransitionValueChanged(float value) { + if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) { + return; + } + + final boolean isShowing = value > 0.0f; + mLockscreenShowing = isShowing; + updateAllowedStates("lockscreenShowing", isShowing); + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorLogger.kt new file mode 100644 index 000000000000..fe23e4e12db4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorLogger.kt @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.collection.coordinator + +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.core.LogLevel +import com.android.systemui.log.dagger.VisualStabilityLog +import javax.inject.Inject + +private const val TAG = "VisualStability" + +class VisualStabilityCoordinatorLogger +@Inject +constructor(@VisualStabilityLog private val buffer: LogBuffer) { + fun logAllowancesChanged( + wasRunAllowed: Boolean, + isRunAllowed: Boolean, + wasReorderingAllowed: Boolean, + isReorderingAllowed: Boolean, + field: String, + value: Boolean + ) { + buffer.log( + TAG, + LogLevel.DEBUG, + { + bool1 = wasRunAllowed + bool2 = isRunAllowed + bool3 = wasReorderingAllowed + bool4 = isReorderingAllowed + str1 = field + str2 = value.toString() + }, + { + "stability allowances changed:" + + " pipelineRunAllowed $bool1->$bool2" + + " reorderingAllowed $bool3->$bool4" + + " when setting $str1=$str2" + } + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/VisualStabilityProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/VisualStabilityProvider.kt index 5adf31b75fa7..0c7ba15baa92 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/VisualStabilityProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/VisualStabilityProvider.kt @@ -13,12 +13,18 @@ class VisualStabilityProvider @Inject constructor() { /** The subset of active listeners which are temporary (will be removed after called) */ private val temporaryListeners = ArraySet<OnReorderingAllowedListener>() + private val banListeners = ListenerSet<OnReorderingBannedListener>() + var isReorderingAllowed = true set(value) { if (field != value) { field = value if (value) { notifyReorderingAllowed() + } else { + banListeners.forEach { listener -> + listener.onReorderingBanned() + } } } } @@ -38,6 +44,10 @@ class VisualStabilityProvider @Inject constructor() { allListeners.addIfAbsent(listener) } + fun addPersistentReorderingBannedListener(listener: OnReorderingBannedListener) { + banListeners.addIfAbsent(listener) + } + /** Add a listener which will be removed when it is called. */ fun addTemporaryReorderingAllowedListener(listener: OnReorderingAllowedListener) { // Only add to the temporary set if it was added to the global set @@ -57,3 +67,7 @@ class VisualStabilityProvider @Inject constructor() { fun interface OnReorderingAllowedListener { fun onReorderingAllowed() } + +fun interface OnReorderingBannedListener { + fun onReorderingBanned() +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt index bf44b9f3cf78..24b75d49ed16 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt @@ -30,6 +30,7 @@ import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map @@ -44,8 +45,17 @@ constructor( private val shadeInteractor: ShadeInteractor, ) { + /** The top-ranked heads up row, regardless of pinned state */ val topHeadsUpRow: Flow<HeadsUpRowKey?> = headsUpRepository.topHeadsUpRow + /** The top-ranked heads up row, if that row is pinned */ + val topHeadsUpRowIfPinned: Flow<HeadsUpRowKey?> = + headsUpRepository.topHeadsUpRow + .flatMapLatest { repository -> + repository?.isPinned?.map { pinned -> repository.takeIf { pinned } } ?: flowOf(null) + } + .distinctUntilChanged() + /** Set of currently pinned top-level heads up rows to be displayed. */ val pinnedHeadsUpRows: Flow<Set<HeadsUpRowKey>> by lazy { if (NotificationsHeadsUpRefactor.isUnexpectedlyInLegacyMode()) { @@ -89,10 +99,10 @@ constructor( flowOf(false) } else { combine(hasPinnedRows, headsUpRepository.isHeadsUpAnimatingAway) { - hasPinnedRows, - animatingAway -> - hasPinnedRows || animatingAway - } + hasPinnedRows, + animatingAway -> + hasPinnedRows || animatingAway + } } } @@ -127,6 +137,9 @@ constructor( fun elementKeyFor(key: HeadsUpRowKey) = (key as HeadsUpRowRepository).elementKey + /** Returns the Notification Key (the standard string) of this row. */ + fun notificationKey(key: HeadsUpRowKey): String = (key as HeadsUpRowRepository).key + fun setHeadsUpAnimatingAway(animatingAway: Boolean) { headsUpRepository.setHeadsUpAnimatingAway(animatingAway) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt index 85c66bd6f25a..948a3c2f65b0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt @@ -16,10 +16,12 @@ package com.android.systemui.statusbar.notification.domain.interactor +import android.util.IndentingPrintWriter import com.android.systemui.dagger.SysUISingleton import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype +import com.android.systemui.util.printSection import javax.inject.Inject import kotlinx.coroutines.flow.StateFlow @@ -41,24 +43,42 @@ constructor( /** Set the entry that is identified as the top ongoing notification. */ fun setTopOngoingNotification(entry: NotificationEntry?) { - if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) return + if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return notificationListRepository.topOngoingNotificationKey.value = entry?.key } /** Determine if the given notification is the top ongoing notification. */ fun isTopOngoingNotification(entry: NotificationEntry?): Boolean = - if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) false + if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) false else entry != null && notificationListRepository.topOngoingNotificationKey.value == entry.key /** Set the entry that is identified as the top unseen notification. */ fun setTopUnseenNotification(entry: NotificationEntry?) { - if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) return + if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return notificationListRepository.topUnseenNotificationKey.value = entry?.key } /** Determine if the given notification is the top unseen notification. */ fun isTopUnseenNotification(entry: NotificationEntry?): Boolean = - if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) false + if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) false else entry != null && notificationListRepository.topUnseenNotificationKey.value == entry.key + + fun dump(pw: IndentingPrintWriter) = + with(pw) { + printSection("SeenNotificationsInteractor") { + print( + "hasFilteredOutSeenNotifications", + notificationListRepository.hasFilteredOutSeenNotifications.value + ) + print( + "topOngoingNotificationKey", + notificationListRepository.topOngoingNotificationKey.value + ) + print( + "topUnseenNotificationKey", + notificationListRepository.topUnseenNotificationKey.value + ) + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt index b8af3698fb63..fe86375d628e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt @@ -122,12 +122,15 @@ class RichOngoingNotificationContentExtractorImpl @Inject constructor() : val timeRemaining = parseTimeDelta(remaining) TimerContentModel( icon = icon, - name = total, + // TODO: b/352142761 - define and use a string resource rather than " Timer". + // (The UX isn't final so using " Timer" for now). + name = total.replace("Σ", "") + " Timer", state = TimerContentModel.TimerState.Paused( timeRemaining = timeRemaining, - resumeIntent = notification.findActionWithName("Resume"), - resetIntent = notification.findActionWithName("Reset"), + resumeIntent = notification.findStartIntent(), + addMinuteAction = notification.findAddMinuteAction(), + resetAction = notification.findResetAction(), ) ) } @@ -136,12 +139,15 @@ class RichOngoingNotificationContentExtractorImpl @Inject constructor() : val finishTime = parseCurrentTime(current) + parseTimeDelta(remaining).toMillis() TimerContentModel( icon = icon, - name = total, + // TODO: b/352142761 - define and use a string resource rather than " Timer". + // (The UX isn't final so using " Timer" for now). + name = total.replace("Σ", "") + " Timer", state = TimerContentModel.TimerState.Running( finishTime = finishTime, - pauseIntent = notification.findActionWithName("Pause"), - addOneMinuteIntent = notification.findActionWithName("Add 1 min"), + pauseIntent = notification.findPauseIntent(), + addMinuteAction = notification.findAddMinuteAction(), + resetAction = notification.findResetAction(), ) ) } @@ -149,8 +155,34 @@ class RichOngoingNotificationContentExtractorImpl @Inject constructor() : } } - private fun Notification.findActionWithName(name: String): PendingIntent? { - return actions.firstOrNull { name == it.title?.toString() }?.actionIntent + private fun Notification.findPauseIntent(): PendingIntent? { + return actions + .firstOrNull { it.actionIntent.intent?.action?.endsWith(".PAUSE_TIMER") == true } + ?.actionIntent + } + + private fun Notification.findStartIntent(): PendingIntent? { + return actions + .firstOrNull { it.actionIntent.intent?.action?.endsWith(".START_TIMER") == true } + ?.actionIntent + } + + // TODO: b/352142761 - switch to system attributes for label and icon. + // - We probably want a consistent look for the Reset button. (Double check with UX.) + // - Using the custom assets now since I couldn't an existing "Reset" icon. + private fun Notification.findResetAction(): Notification.Action? { + return actions.firstOrNull { + it.actionIntent.intent?.action?.endsWith(".RESET_TIMER") == true + } + } + + // TODO: b/352142761 - check with UX on whether this should be required. + // - Alternative is to allow for optional actions in addition to main and reset. + // - For optional actions, we should take the custom label and icon. + private fun Notification.findAddMinuteAction(): Notification.Action? { + return actions.firstOrNull { + it.actionIntent.intent?.action?.endsWith(".ADD_MINUTE_TIMER") == true + } } private fun parseCurrentTime(current: String): Long { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/RichOngoingClock.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/RichOngoingClock.kt index 558470175e8d..33b256456ca3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/RichOngoingClock.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/RichOngoingClock.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.notification.row.shared +import android.app.Notification import android.app.PendingIntent import java.time.Duration @@ -32,6 +33,9 @@ data class TimerContentModel( ) : RichOngoingContentModel { /** The state (paused or running) of the timer, and relevant time */ sealed interface TimerState { + val addMinuteAction: Notification.Action? + val resetAction: Notification.Action? + /** * Indicates a running timer * @@ -41,7 +45,8 @@ data class TimerContentModel( data class Running( val finishTime: Long, val pauseIntent: PendingIntent?, - val addOneMinuteIntent: PendingIntent?, + override val addMinuteAction: Notification.Action?, + override val resetAction: Notification.Action?, ) : TimerState /** @@ -53,7 +58,8 @@ data class TimerContentModel( data class Paused( val timeRemaining: Duration, val resumeIntent: PendingIntent?, - val resetIntent: PendingIntent?, + override val addMinuteAction: Notification.Action?, + override val resetAction: Notification.Action?, ) : TimerState } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerButtonView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerButtonView.kt index 0d83aced6d07..8c951877544c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerButtonView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerButtonView.kt @@ -18,8 +18,9 @@ package com.android.systemui.statusbar.notification.row.ui.view import android.annotation.DrawableRes import android.content.Context +import android.graphics.BlendMode import android.util.AttributeSet -import android.widget.Button +import com.android.internal.widget.EmphasizedNotificationButton class TimerButtonView @JvmOverloads @@ -28,14 +29,19 @@ constructor( attrs: AttributeSet? = null, defStyleAttr: Int = 0, defStyleRes: Int = 0, -) : Button(context, attrs, defStyleAttr, defStyleRes) { +) : EmphasizedNotificationButton(context, attrs, defStyleAttr, defStyleRes) { private val Int.dp: Int get() = (this * context.resources.displayMetrics.density).toInt() fun setIcon(@DrawableRes icon: Int) { val drawable = context.getDrawable(icon) + + drawable?.mutate() + drawable?.setTintList(textColors) + drawable?.setTintBlendMode(BlendMode.SRC_IN) drawable?.setBounds(0, 0, 24.dp, 24.dp) + setCompoundDrawablesRelative(drawable, null, null, null) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerView.kt index 2e164d60431d..d481b50101c1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerView.kt @@ -17,7 +17,7 @@ package com.android.systemui.statusbar.notification.row.ui.view import android.content.Context -import android.graphics.drawable.Drawable +import android.graphics.drawable.Icon import android.os.SystemClock import android.util.AttributeSet import android.widget.Chronometer @@ -48,6 +48,9 @@ constructor( lateinit var altButton: TimerButtonView private set + lateinit var resetButton: TimerButtonView + private set + override fun onFinishInflate() { super.onFinishInflate() icon = requireViewById(R.id.icon) @@ -56,13 +59,14 @@ constructor( pausedTimeRemaining = requireViewById(R.id.pausedTimeRemaining) mainButton = requireViewById(R.id.mainButton) altButton = requireViewById(R.id.altButton) + resetButton = requireViewById(R.id.resetButton) } /** the resources configuration has changed such that the view needs to be reinflated */ fun isReinflateNeeded(): Boolean = configTracker.hasUnhandledConfigChange() - fun setIcon(iconDrawable: Drawable?) { - this.icon.setImageDrawable(iconDrawable) + fun setIcon(icon: Icon?) { + this.icon.setImageIcon(icon) } fun setLabel(label: String) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/TimerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/TimerViewBinder.kt index c9ff58961582..042d1bcfb2ee 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/TimerViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/TimerViewBinder.kt @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.notification.row.ui.viewbinder +import android.content.res.ColorStateList +import android.graphics.drawable.Icon import android.view.View import androidx.core.view.isGone import androidx.lifecycle.lifecycleScope @@ -46,12 +48,43 @@ object TimerViewBinder { launch { viewModel.countdownTime.collect { view.setCountdownTime(it) } } launch { viewModel.mainButtonModel.collect { bind(view.mainButton, it) } } launch { viewModel.altButtonModel.collect { bind(view.altButton, it) } } + launch { viewModel.resetButtonModel.collect { bind(view.resetButton, it) } } } fun bind(buttonView: TimerButtonView, model: TimerViewModel.ButtonViewModel?) { if (model != null) { - buttonView.setIcon(model.iconRes) - buttonView.setText(model.labelRes) + buttonView.setButtonBackground( + ColorStateList.valueOf( + buttonView.context.getColor(com.android.internal.R.color.system_accent2_100) + ) + ) + buttonView.setTextColor( + buttonView.context.getColor( + com.android.internal.R.color.notification_primary_text_color_light + ) + ) + + when (model) { + is TimerViewModel.ButtonViewModel.WithSystemAttrs -> { + buttonView.setIcon(model.iconRes) + buttonView.setText(model.labelRes) + } + is TimerViewModel.ButtonViewModel.WithCustomAttrs -> { + // TODO: b/352142761 - is there a better way to deal with TYPE_RESOURCE icons + // with empty resPackage? RemoteViews handles this by using a different + // `contextForResources` for inflation. + val icon = + if (model.icon.type == Icon.TYPE_RESOURCE && model.icon.resPackage == "") + Icon.createWithResource( + "com.google.android.deskclock", + model.icon.resId + ) + else model.icon + buttonView.setImageIcon(icon) + buttonView.text = model.label + } + } + buttonView.setOnClickListener( model.pendingIntent?.let { pendingIntent -> View.OnClickListener { pendingIntent.send() } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModel.kt index a85c87f288d3..768a093e0b65 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModel.kt @@ -19,7 +19,7 @@ package com.android.systemui.statusbar.notification.row.ui.viewmodel import android.annotation.DrawableRes import android.annotation.StringRes import android.app.PendingIntent -import android.graphics.drawable.Drawable +import android.graphics.drawable.Icon import com.android.systemui.dump.DumpManager import com.android.systemui.statusbar.notification.row.domain.interactor.NotificationRowInteractor import com.android.systemui.statusbar.notification.row.shared.RichOngoingNotificationFlag @@ -44,7 +44,7 @@ constructor( private val state: Flow<TimerState> = rowInteractor.timerContentModel.mapNotNull { it.state } - val icon: Flow<Drawable?> = rowInteractor.timerContentModel.mapNotNull { it.icon.drawable } + val icon: Flow<Icon?> = rowInteractor.timerContentModel.mapNotNull { it.icon.icon } val label: Flow<String> = rowInteractor.timerContentModel.mapNotNull { it.name } @@ -57,13 +57,13 @@ constructor( state.map { when (it) { is TimerState.Paused -> - ButtonViewModel( + ButtonViewModel.WithSystemAttrs( it.resumeIntent, com.android.systemui.res.R.string.controls_media_resume, // "Resume", com.android.systemui.res.R.drawable.ic_media_play ) is TimerState.Running -> - ButtonViewModel( + ButtonViewModel.WithSystemAttrs( it.pauseIntent, com.android.systemui.res.R.string.controls_media_button_pause, // "Pause", com.android.systemui.res.R.drawable.ic_media_pause @@ -73,31 +73,41 @@ constructor( val altButtonModel: Flow<ButtonViewModel?> = state.map { - when (it) { - is TimerState.Paused -> - it.resetIntent?.let { resetIntent -> - ButtonViewModel( - resetIntent, - com.android.systemui.res.R.string.reset, // "Reset", - com.android.systemui.res.R.drawable.ic_close_white_rounded - ) - } - is TimerState.Running -> - it.addOneMinuteIntent?.let { addOneMinuteIntent -> - ButtonViewModel( - addOneMinuteIntent, - com.android.systemui.res.R.string.add, // "Add 1 minute", - com.android.systemui.res.R.drawable.ic_add - ) - } + it.addMinuteAction?.let { action -> + ButtonViewModel.WithCustomAttrs( + action.actionIntent, + action.title, // "1:00", + action.getIcon() + ) + } + } + + val resetButtonModel: Flow<ButtonViewModel?> = + state.map { + it.resetAction?.let { action -> + ButtonViewModel.WithCustomAttrs( + action.actionIntent, + action.title, // "Reset", + action.getIcon() + ) } } - data class ButtonViewModel( - val pendingIntent: PendingIntent?, - @StringRes val labelRes: Int, - @DrawableRes val iconRes: Int, - ) + sealed interface ButtonViewModel { + val pendingIntent: PendingIntent? + + data class WithSystemAttrs( + override val pendingIntent: PendingIntent?, + @StringRes val labelRes: Int, + @DrawableRes val iconRes: Int, + ) : ButtonViewModel + + data class WithCustomAttrs( + override val pendingIntent: PendingIntent?, + val label: CharSequence, + val icon: Icon, + ) : ButtonViewModel + } } private fun Duration.format(): String { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt index bf37036ee018..06f3db504aaf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt @@ -24,102 +24,43 @@ import com.android.systemui.flags.RefactorFlagUtils /** Helper for reading or using the minimalism prototype flag state. */ @Suppress("NOTHING_TO_INLINE") object NotificationMinimalismPrototype { - - val version: Int by lazy { - SystemProperties.getInt("persist.notification_minimalism_prototype.version", 2) - } - - object V1 { - /** The aconfig flag name */ - const val FLAG_NAME = Flags.FLAG_NOTIFICATION_MINIMALISM_PROTOTYPE - - /** A token used for dependency declaration */ - val token: FlagToken - get() = FlagToken(FLAG_NAME, isEnabled) - - /** Is the heads-up cycling animation enabled */ - @JvmStatic - inline val isEnabled - get() = Flags.notificationMinimalismPrototype() && version == 1 - - /** - * the prototype will now show seen notifications on the locked shade by default, but this - * property read allows that to be quickly disabled for testing - */ - val showOnLockedShade: Boolean - get() = - if (isUnexpectedlyInLegacyMode()) false - else - SystemProperties.getBoolean( - "persist.notification_minimalism_prototype.show_on_locked_shade", - true - ) - - /** gets the configurable max number of notifications */ - val maxNotifs: Int - get() = - if (isUnexpectedlyInLegacyMode()) -1 - else - SystemProperties.getInt( - "persist.notification_minimalism_prototype.lock_screen_max_notifs", - 1 - ) - - /** - * Called to ensure code is only run when the flag is enabled. This protects users from the - * unintended behaviors caused by accidentally running new logic, while also crashing on an - * eng build to ensure that the refactor author catches issues in testing. - */ - @JvmStatic - inline fun isUnexpectedlyInLegacyMode() = - RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME) - - /** - * Called to ensure code is only run when the flag is disabled. This will throw an exception - * if the flag is enabled to ensure that the refactor author catches issues in testing. - */ - @JvmStatic - inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME) - } - object V2 { - const val FLAG_NAME = Flags.FLAG_NOTIFICATION_MINIMALISM_PROTOTYPE - - /** A token used for dependency declaration */ - val token: FlagToken - get() = FlagToken(FLAG_NAME, isEnabled) - - /** Is the heads-up cycling animation enabled */ - @JvmStatic - inline val isEnabled - get() = Flags.notificationMinimalismPrototype() && version == 2 - - /** - * The prototype will (by default) use a promoter to ensure that the top unseen notification - * is not grouped, but this property read allows that behavior to be disabled. - */ - val ungroupTopUnseen: Boolean - get() = - if (isUnexpectedlyInLegacyMode()) false - else - SystemProperties.getBoolean( - "persist.notification_minimalism_prototype.ungroup_top_unseen", - true - ) - - /** - * Called to ensure code is only run when the flag is enabled. This protects users from the - * unintended behaviors caused by accidentally running new logic, while also crashing on an - * eng build to ensure that the refactor author catches issues in testing. - */ - @JvmStatic - inline fun isUnexpectedlyInLegacyMode() = - RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME) - - /** - * Called to ensure code is only run when the flag is disabled. This will throw an exception - * if the flag is enabled to ensure that the refactor author catches issues in testing. - */ - @JvmStatic - inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME) - } + const val FLAG_NAME = Flags.FLAG_NOTIFICATION_MINIMALISM_PROTOTYPE + + /** A token used for dependency declaration */ + val token: FlagToken + get() = FlagToken(FLAG_NAME, isEnabled) + + /** Is the heads-up cycling animation enabled */ + @JvmStatic + inline val isEnabled + get() = Flags.notificationMinimalismPrototype() + + /** + * The prototype will (by default) use a promoter to ensure that the top unseen notification is + * not grouped, but this property read allows that behavior to be disabled. + */ + val ungroupTopUnseen: Boolean + get() = + if (isUnexpectedlyInLegacyMode()) false + else + SystemProperties.getBoolean( + "persist.notification_minimalism_prototype.ungroup_top_unseen", + false + ) + + /** + * Called to ensure code is only run when the flag is enabled. This protects users from the + * unintended behaviors caused by accidentally running new logic, while also crashing on an eng + * build to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun isUnexpectedlyInLegacyMode() = + RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME) + + /** + * Called to ensure code is only run when the flag is disabled. This will throw an exception if + * the flag is enabled to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index cec1ef3f1e7b..4a447b7439b8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -4862,14 +4862,20 @@ public class NotificationStackScrollLayout * @param isHeadsUp true for appear, false for disappear animations */ public void generateHeadsUpAnimation(ExpandableNotificationRow row, boolean isHeadsUp) { - final boolean add = mAnimationsEnabled && (isHeadsUp || mHeadsUpGoingAwayAnimationsAllowed); + final boolean closedAndSeenInShade = !mIsExpanded && row.getEntry() != null + && row.getEntry().isSeenInShade(); + final boolean addAnimation = mAnimationsEnabled && !closedAndSeenInShade && + (isHeadsUp || mHeadsUpGoingAwayAnimationsAllowed); if (SPEW) { Log.v(TAG, "generateHeadsUpAnimation:" - + " willAdd=" + add - + " isHeadsUp=" + isHeadsUp - + " row=" + row.getEntry().getKey()); - } - if (add) { + + " addAnimation=" + addAnimation + + (row.getEntry() == null ? " entry NULL " + : " isSeenInShade=" + row.getEntry().isSeenInShade() + + " row=" + row.getEntry().getKey()) + + " mIsExpanded=" + mIsExpanded + + " isHeadsUp=" + isHeadsUp); + } + if (addAnimation) { // If we're hiding a HUN we just started showing THIS FRAME, then remove that event, // and do not add the disappear event either. if (!isHeadsUp && mHeadsUpChangeAnimations.remove(new Pair<>(row, true))) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt index 391bc43a784c..06222fdb2761 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt @@ -74,7 +74,7 @@ constructor( /** Whether we allow keyguard to show less important notifications above the shelf. */ private val limitLockScreenToOneImportant - get() = NotificationMinimalismPrototype.V2.isEnabled + get() = NotificationMinimalismPrototype.isEnabled /** Minimum space between two notifications, see [calculateGapAndDividerHeight]. */ private var dividerHeight by notNull<Float>() @@ -405,16 +405,8 @@ constructor( fun updateResources() { maxKeyguardNotifications = - infiniteIfNegative( - if (NotificationMinimalismPrototype.V1.isEnabled) { - NotificationMinimalismPrototype.V1.maxNotifs - } else { - resources.getInteger(R.integer.keyguard_max_notification_count) - } - ) - maxNotificationsExcludesMedia = - NotificationMinimalismPrototype.V1.isEnabled || - NotificationMinimalismPrototype.V2.isEnabled + infiniteIfNegative(resources.getInteger(R.integer.keyguard_max_notification_count)) + maxNotificationsExcludesMedia = NotificationMinimalismPrototype.isEnabled dividerHeight = max(1f, resources.getDimensionPixelSize(R.dimen.notification_divider_height).toFloat()) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt index 97266c57ba09..86c7c6bd1e86 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt @@ -24,6 +24,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.ActivityStarter.OnDismissAction +import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.statusbar.SysuiStatusBarStateController import com.android.systemui.util.concurrency.DelayableExecutor import dagger.Lazy @@ -36,10 +37,16 @@ class ActivityStarterImpl constructor( private val statusBarStateController: SysuiStatusBarStateController, @Main private val mainExecutor: DelayableExecutor, + activityStarterInternal: Lazy<ActivityStarterInternalImpl>, legacyActivityStarter: Lazy<LegacyActivityStarterInternalImpl> ) : ActivityStarter { - private val activityStarterInternal: ActivityStarterInternal = legacyActivityStarter.get() + private val activityStarterInternal: ActivityStarterInternal = + if (SceneContainerFlag.isEnabled) { + activityStarterInternal.get() + } else { + legacyActivityStarter.get() + } override fun startPendingIntentDismissingKeyguard(intent: PendingIntent) { activityStarterInternal.startPendingIntentDismissingKeyguard( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternalImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternalImpl.kt index ae98e1d60589..107bf1edb74f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternalImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternalImpl.kt @@ -16,23 +16,93 @@ package com.android.systemui.statusbar.phone +import android.app.ActivityManager +import android.app.ActivityOptions +import android.app.ActivityTaskManager import android.app.PendingIntent +import android.app.TaskStackBuilder +import android.content.Context import android.content.Intent +import android.content.res.Resources import android.os.Bundle +import android.os.RemoteException import android.os.UserHandle +import android.provider.Settings +import android.util.Log +import android.view.RemoteAnimationAdapter import android.view.View +import android.view.WindowManager +import com.android.systemui.ActivityIntentHelper +import com.android.systemui.Flags import com.android.systemui.animation.ActivityTransitionAnimator +import com.android.systemui.animation.DelegateTransitionAnimatorController +import com.android.systemui.assist.AssistManager +import com.android.systemui.camera.CameraIntents +import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor +import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.DisplayId +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor +import com.android.systemui.keyguard.KeyguardViewMediator +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.res.R +import com.android.systemui.scene.shared.flag.SceneContainerFlag +import com.android.systemui.shade.ShadeController +import com.android.systemui.shade.domain.interactor.ShadeAnimationInteractor +import com.android.systemui.statusbar.CommandQueue +import com.android.systemui.statusbar.NotificationLockscreenUserManager +import com.android.systemui.statusbar.NotificationShadeWindowController +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow +import com.android.systemui.statusbar.policy.domain.interactor.DeviceProvisioningInteractor +import com.android.systemui.statusbar.window.StatusBarWindowController +import com.android.systemui.user.domain.interactor.SelectedUserInteractor +import com.android.systemui.util.concurrency.DelayableExecutor +import com.android.systemui.util.kotlin.getOrNull +import dagger.Lazy +import java.util.Optional import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi /** - * Encapsulates the activity logic for activity starter when flexiglass is enabled. + * Encapsulates the activity logic for activity starter when the SceneContainerFlag is enabled. * * TODO: b/308819693 */ +@ExperimentalCoroutinesApi @SysUISingleton -class ActivityStarterInternalImpl @Inject constructor() : ActivityStarterInternal { +class ActivityStarterInternalImpl +@Inject +constructor( + private val statusBarKeyguardViewManagerLazy: Lazy<StatusBarKeyguardViewManager>, + private val keyguardInteractor: KeyguardInteractor, + private val centralSurfacesOptLazy: Lazy<Optional<CentralSurfaces>>, + private val context: Context, + @Main private val resources: Resources, + private val selectedUserInteractor: SelectedUserInteractor, + private val deviceEntryInteractor: DeviceEntryInteractor, + private val activityTransitionAnimator: ActivityTransitionAnimator, + @DisplayId private val displayId: Int, + private val deviceProvisioningInteractor: DeviceProvisioningInteractor, + private val activityIntentHelper: ActivityIntentHelper, + private val keyguardTransitionInteractor: KeyguardTransitionInteractor, + private val assistManagerLazy: Lazy<AssistManager>, + @Main private val mainExecutor: DelayableExecutor, + private val shadeControllerLazy: Lazy<ShadeController>, + private val communalSceneInteractor: CommunalSceneInteractor, + private val statusBarWindowController: StatusBarWindowController, + private val keyguardViewMediatorLazy: Lazy<KeyguardViewMediator>, + private val shadeAnimationInteractor: ShadeAnimationInteractor, + private val notifShadeWindowControllerLazy: Lazy<NotificationShadeWindowController>, + private val commandQueue: CommandQueue, + private val lockScreenUserManager: NotificationLockscreenUserManager, +) : ActivityStarterInternal { + private val centralSurfaces: CentralSurfaces? + get() = centralSurfacesOptLazy.get().getOrNull() + override fun startPendingIntentDismissingKeyguard( intent: PendingIntent, dismissShade: Boolean, @@ -45,7 +115,119 @@ class ActivityStarterInternalImpl @Inject constructor() : ActivityStarterInterna extraOptions: Bundle?, customMessage: String?, ) { - TODO("Not yet implemented b/308819693") + if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return + val animationController = + if (associatedView is ExpandableNotificationRow) { + centralSurfaces?.getAnimatorControllerFromNotification(associatedView) + } else animationController + + val willLaunchResolverActivity = + intent.isActivity && + activityIntentHelper.wouldPendingLaunchResolverActivity( + intent, + lockScreenUserManager.currentUserId, + ) + + val actuallyShowOverLockscreen = + showOverLockscreen && + intent.isActivity && + (skipLockscreenChecks || + activityIntentHelper.wouldPendingShowOverLockscreen( + intent, + lockScreenUserManager.currentUserId + )) + + val animate = + !willLaunchResolverActivity && + animationController != null && + shouldAnimateLaunch(intent.isActivity, actuallyShowOverLockscreen) + + // We wrap animationCallback with a StatusBarLaunchAnimatorController so + // that the shade is collapsed after the animation (or when it is cancelled, + // aborted, etc). + val statusBarController = + wrapAnimationControllerForShadeOrStatusBar( + animationController = animationController, + dismissShade = dismissShade, + isLaunchForActivity = intent.isActivity, + ) + val controller = + if (actuallyShowOverLockscreen) { + wrapAnimationControllerForLockscreen(dismissShade, statusBarController) + } else { + statusBarController + } + + // If we animate, don't collapse the shade and defer the keyguard dismiss (in case we + // run the animation on the keyguard). The animation will take care of (instantly) + // collapsing the shade and hiding the keyguard once it is done. + val collapse = dismissShade && !animate + val runnable = Runnable { + try { + activityTransitionAnimator.startPendingIntentWithAnimation( + controller, + animate, + intent.creatorPackage, + actuallyShowOverLockscreen, + object : ActivityTransitionAnimator.PendingIntentStarter { + override fun startPendingIntent( + animationAdapter: RemoteAnimationAdapter? + ): Int { + val options = + ActivityOptions( + CentralSurfaces.getActivityOptions(displayId, animationAdapter) + .apply { extraOptions?.let { putAll(it) } } + ) + // TODO b/221255671: restrict this to only be set for notifications + options.isEligibleForLegacyPermissionPrompt = true + options.setPendingIntentBackgroundActivityStartMode( + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED + ) + return intent.sendAndReturnResult( + context, + 0, + fillInIntent, + null, + null, + null, + options.toBundle() + ) + } + }, + ) + } catch (e: PendingIntent.CanceledException) { + // the stack trace isn't very helpful here. + // Just log the exception message. + Log.w(TAG, "Sending intent failed: $e") + if (!collapse) { + // executeRunnableDismissingKeyguard did not collapse for us already. + shadeControllerLazy.get().collapseOnMainThread() + } + // TODO: Dismiss Keyguard. + } + if (intent.isActivity) { + assistManagerLazy.get().hideAssist() + // This activity could have started while the device is dreaming, in which case + // the dream would occlude the activity. In order to show the newly started + // activity, we wake from the dream. + centralSurfaces?.awakenDreams() + } + intentSentUiThreadCallback?.let { mainExecutor.execute(it) } + } + + if (!actuallyShowOverLockscreen) { + mainExecutor.execute { + executeRunnableDismissingKeyguard( + runnable = runnable, + afterKeyguardGone = willLaunchResolverActivity, + dismissShade = collapse, + willAnimateOnKeyguard = animate, + customMessage = customMessage, + ) + } + } else { + mainExecutor.execute(runnable) + } } override fun startActivityDismissingKeyguard( @@ -59,7 +241,116 @@ class ActivityStarterInternalImpl @Inject constructor() : ActivityStarterInterna disallowEnterPictureInPictureWhileLaunching: Boolean, userHandle: UserHandle? ) { - TODO("Not yet implemented b/308819693") + if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return + val userHandle: UserHandle = userHandle ?: getActivityUserHandle(intent) + + if (onlyProvisioned && !deviceProvisioningInteractor.isDeviceProvisioned()) return + + val willLaunchResolverActivity: Boolean = + activityIntentHelper.wouldLaunchResolverActivity( + intent, + selectedUserInteractor.getSelectedUserId(), + ) + + val animate = + animationController != null && + !willLaunchResolverActivity && + shouldAnimateLaunch(isActivityIntent = true) + val animController = + wrapAnimationControllerForShadeOrStatusBar( + animationController = animationController, + dismissShade = dismissShade, + isLaunchForActivity = true, + ) + + // If we animate, we will dismiss the shade only once the animation is done. This is + // taken care of by the StatusBarLaunchAnimationController. + val dismissShadeDirectly = dismissShade && animController == null + + val runnable = Runnable { + assistManagerLazy.get().hideAssist() + intent.flags = + if (intent.flags and Intent.FLAG_ACTIVITY_REORDER_TO_FRONT != 0) { + Intent.FLAG_ACTIVITY_NEW_TASK + } else { + Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + } + intent.addFlags(flags) + val result = intArrayOf(ActivityManager.START_CANCELED) + activityTransitionAnimator.startIntentWithAnimation( + animController, + animate, + intent.getPackage() + ) { adapter: RemoteAnimationAdapter? -> + val options = + ActivityOptions(CentralSurfaces.getActivityOptions(displayId, adapter)) + + // We know that the intent of the caller is to dismiss the keyguard and + // this runnable is called right after the keyguard is solved, so we tell + // WM that we should dismiss it to avoid flickers when opening an activity + // that can also be shown over the keyguard. + options.setDismissKeyguardIfInsecure() + options.setDisallowEnterPictureInPictureWhileLaunching( + disallowEnterPictureInPictureWhileLaunching + ) + if (CameraIntents.isInsecureCameraIntent(intent)) { + // Normally an activity will set it's requested rotation + // animation on its window. However when launching an activity + // causes the orientation to change this is too late. In these cases + // the default animation is used. This doesn't look good for + // the camera (as it rotates the camera contents out of sync + // with physical reality). So, we ask the WindowManager to + // force the cross fade animation if an orientation change + // happens to occur during the launch. + options.rotationAnimationHint = + WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS + } + if (Settings.Panel.ACTION_VOLUME == intent.action) { + // Settings Panel is implemented as activity(not a dialog), so + // underlying app is paused and may enter picture-in-picture mode + // as a result. + // So we need to disable picture-in-picture mode here + // if it is volume panel. + options.setDisallowEnterPictureInPictureWhileLaunching(true) + } + try { + result[0] = + ActivityTaskManager.getService() + .startActivityAsUser( + null, + context.basePackageName, + context.attributionTag, + intent, + intent.resolveTypeIfNeeded(context.contentResolver), + null, + null, + 0, + Intent.FLAG_ACTIVITY_NEW_TASK, + null, + options.toBundle(), + userHandle.identifier, + ) + } catch (e: RemoteException) { + Log.w(TAG, "Unable to start activity", e) + } + result[0] + } + callback?.onActivityStarted(result[0]) + } + val cancelRunnable = Runnable { + callback?.onActivityStarted(ActivityManager.START_CANCELED) + } + // Do not deferKeyguard when occluded because, when keyguard is occluded, + // we do not launch the activity until keyguard is done. + executeRunnableDismissingKeyguard( + runnable, + cancelRunnable, + dismissShadeDirectly, + willLaunchResolverActivity, + deferred = !isKeyguardOccluded(), + animate, + customMessage, + ) } override fun startActivity( @@ -69,7 +360,64 @@ class ActivityStarterInternalImpl @Inject constructor() : ActivityStarterInterna showOverLockscreenWhenLocked: Boolean, userHandle: UserHandle? ) { - TODO("Not yet implemented b/308819693") + if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return + val userHandle = userHandle ?: getActivityUserHandle(intent) + // Make sure that we dismiss the keyguard if it is directly dismissible or when we don't + // want to show the activity above it. + if (deviceEntryInteractor.isUnlocked.value || !showOverLockscreenWhenLocked) { + startActivityDismissingKeyguard( + intent = intent, + onlyProvisioned = false, + dismissShade = dismissShade, + disallowEnterPictureInPictureWhileLaunching = false, + callback = null, + flags = 0, + animationController = animationController, + userHandle = userHandle, + ) + return + } + + val animate = + animationController != null && + shouldAnimateLaunch( + isActivityIntent = true, + showOverLockscreen = showOverLockscreenWhenLocked + ) + + var controller: ActivityTransitionAnimator.Controller? = null + if (animate) { + // Wrap the animation controller to dismiss the shade and set + // mIsLaunchingActivityOverLockscreen during the animation. + val delegate = + wrapAnimationControllerForShadeOrStatusBar( + animationController = animationController, + dismissShade = dismissShade, + isLaunchForActivity = true, + ) + controller = wrapAnimationControllerForLockscreen(dismissShade, delegate) + } else if (dismissShade) { + // The animation will take care of dismissing the shade at the end of the animation. + // If we don't animate, collapse it directly. + shadeControllerLazy.get().cancelExpansionAndCollapseShade() + } + + // We should exit the dream to prevent the activity from starting below the + // dream. + if (keyguardInteractor.isDreaming.value) { + centralSurfaces?.awakenDreams() + } + + activityTransitionAnimator.startIntentWithAnimation( + controller, + animate, + intent.getPackage(), + showOverLockscreenWhenLocked + ) { adapter: RemoteAnimationAdapter? -> + TaskStackBuilder.create(context) + .addNextIntent(intent) + .startActivities(CentralSurfaces.getActivityOptions(displayId, adapter), userHandle) + } } override fun dismissKeyguardThenExecute( @@ -78,7 +426,23 @@ class ActivityStarterInternalImpl @Inject constructor() : ActivityStarterInterna afterKeyguardGone: Boolean, customMessage: String? ) { - TODO("Not yet implemented b/308819693") + if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return + Log.i(TAG, "Invoking dismissKeyguardThenExecute, afterKeyguardGone: $afterKeyguardGone") + + // TODO b/308819693: startWakeAndUnlock animation when pulsing + + if (isKeyguardShowing()) { + statusBarKeyguardViewManagerLazy + .get() + .dismissWithAction(action, cancel, afterKeyguardGone, customMessage) + } else { + // If the keyguard isn't showing but the device is dreaming, we should exit the + // dream. + if (keyguardInteractor.isDreaming.value) { + centralSurfaces?.awakenDreams() + } + action.onDismiss() + } } override fun executeRunnableDismissingKeyguard( @@ -90,10 +454,195 @@ class ActivityStarterInternalImpl @Inject constructor() : ActivityStarterInterna willAnimateOnKeyguard: Boolean, customMessage: String? ) { - TODO("Not yet implemented b/308819693") + if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return + val onDismissAction: ActivityStarter.OnDismissAction = + object : ActivityStarter.OnDismissAction { + override fun onDismiss(): Boolean { + if (runnable != null) { + if (isKeyguardOccluded()) { + statusBarKeyguardViewManagerLazy + .get() + .addAfterKeyguardGoneRunnable(runnable) + } else { + mainExecutor.execute(runnable) + } + } + if (dismissShade) { + shadeControllerLazy.get().collapseShadeForActivityStart() + } + if (Flags.communalHub()) { + communalSceneInteractor.changeSceneForActivityStartOnDismissKeyguard() + } + return deferred + } + + override fun willRunAnimationOnKeyguard(): Boolean { + if (Flags.communalHub() && communalSceneInteractor.isIdleOnCommunal.value) { + // Override to false when launching activity over the hub that requires auth + return false + } + return willAnimateOnKeyguard + } + } + dismissKeyguardThenExecute( + onDismissAction, + cancelAction, + afterKeyguardGone, + customMessage, + ) } override fun shouldAnimateLaunch(isActivityIntent: Boolean): Boolean { - TODO("Not yet implemented b/308819693") + return shouldAnimateLaunch(isActivityIntent, false) + } + + /** + * Whether we should animate an activity launch. + * + * Note: This method must be called *before* dismissing the keyguard. + */ + private fun shouldAnimateLaunch( + isActivityIntent: Boolean, + showOverLockscreen: Boolean, + ): Boolean { + // TODO(b/294418322): always support launch animations when occluded. + val ignoreOcclusion = showOverLockscreen && Flags.mediaLockscreenLaunchAnimation() + if (isKeyguardOccluded() && !ignoreOcclusion) { + return false + } + + // Always animate if we are not showing the keyguard or if we animate over the lockscreen + // (without unlocking it). + if (showOverLockscreen || !isKeyguardShowing()) { + return true + } + + // We don't animate non-activity launches as they can break the animation. + // TODO(b/184121838): Support non activity launches on the lockscreen. + return isActivityIntent + } + + /** Retrieves the current user handle to start the Activity. */ + private fun getActivityUserHandle(intent: Intent): UserHandle { + val packages: Array<String> = resources.getStringArray(R.array.system_ui_packages) + for (pkg in packages) { + val componentName = intent.component ?: break + if (pkg == componentName.packageName) { + return UserHandle(UserHandle.myUserId()) + } + } + return UserHandle(selectedUserInteractor.getSelectedUserId()) + } + + private fun isKeyguardShowing(): Boolean { + return !deviceEntryInteractor.isDeviceEntered.value + } + + private fun isKeyguardOccluded(): Boolean { + return keyguardTransitionInteractor.getCurrentState() == KeyguardState.OCCLUDED + } + + /** + * Return a [ActivityTransitionAnimator.Controller] wrapping `animationController` so that: + * - if it launches in the notification shade window and `dismissShade` is true, then the shade + * will be instantly dismissed at the end of the animation. + * - if it launches in status bar window, it will make the status bar window match the device + * size during the animation (that way, the animation won't be clipped by the status bar + * size). + * + * @param animationController the controller that is wrapped and will drive the main animation. + * @param dismissShade whether the notification shade will be dismissed at the end of the + * animation. This is ignored if `animationController` is not animating in the shade window. + * @param isLaunchForActivity whether the launch is for an activity. + */ + private fun wrapAnimationControllerForShadeOrStatusBar( + animationController: ActivityTransitionAnimator.Controller?, + dismissShade: Boolean, + isLaunchForActivity: Boolean, + ): ActivityTransitionAnimator.Controller? { + if (animationController == null) { + return null + } + val rootView = animationController.transitionContainer.rootView + val controllerFromStatusBar: Optional<ActivityTransitionAnimator.Controller> = + statusBarWindowController.wrapAnimationControllerIfInStatusBar( + rootView, + animationController + ) + if (controllerFromStatusBar.isPresent) { + return controllerFromStatusBar.get() + } + + centralSurfaces?.let { + // If the view is not in the status bar, then we are animating a view in the shade. + // We have to make sure that we collapse it when the animation ends or is cancelled. + if (dismissShade) { + return StatusBarTransitionAnimatorController( + animationController, + shadeAnimationInteractor, + shadeControllerLazy.get(), + notifShadeWindowControllerLazy.get(), + commandQueue, + displayId, + isLaunchForActivity + ) + } + } + + return animationController + } + + /** + * Wraps an animation controller so that if an activity would be launched on top of the + * lockscreen, the correct flags are set for it to be occluded. + */ + private fun wrapAnimationControllerForLockscreen( + dismissShade: Boolean, + animationController: ActivityTransitionAnimator.Controller? + ): ActivityTransitionAnimator.Controller? { + return animationController?.let { + object : DelegateTransitionAnimatorController(it) { + override fun onIntentStarted(willAnimate: Boolean) { + delegate.onIntentStarted(willAnimate) + if (willAnimate) { + centralSurfaces?.setIsLaunchingActivityOverLockscreen(true, dismissShade) + } + } + + override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) { + super.onTransitionAnimationStart(isExpandingFullyAbove) + if (Flags.communalHub()) { + communalSceneInteractor.snapToScene( + CommunalScenes.Blank, + ActivityTransitionAnimator.TIMINGS.totalDuration + ) + } + } + + override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) { + // Set mIsLaunchingActivityOverLockscreen to false before actually + // finishing the animation so that we can assume that + // mIsLaunchingActivityOverLockscreen being true means that we will + // collapse the shade (or at least run the post collapse runnables) + // later on. + centralSurfaces?.setIsLaunchingActivityOverLockscreen(false, false) + delegate.onTransitionAnimationEnd(isExpandingFullyAbove) + } + + override fun onTransitionAnimationCancelled(newKeyguardOccludedState: Boolean?) { + // Set mIsLaunchingActivityOverLockscreen to false before actually + // finishing the animation so that we can assume that + // mIsLaunchingActivityOverLockscreen being true means that we will + // collapse the shade (or at least run the // post collapse + // runnables) later on. + centralSurfaces?.setIsLaunchingActivityOverLockscreen(false, false) + delegate.onTransitionAnimationCancelled(newKeyguardOccludedState) + } + } + } + } + + companion object { + private const val TAG = "ActivityStarterInternalImpl" } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java index a2e44dffb767..8577d48b6679 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java @@ -39,6 +39,7 @@ import com.android.systemui.shade.domain.interactor.ShadeInteractor; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener; +import com.android.systemui.statusbar.notification.collection.provider.OnReorderingBannedListener; import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider; import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager; import com.android.systemui.statusbar.notification.data.repository.HeadsUpRepository; @@ -86,7 +87,7 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements private final List<OnHeadsUpPhoneListenerChange> mHeadsUpPhoneListeners = new ArrayList<>(); private final VisualStabilityProvider mVisualStabilityProvider; - private final AvalancheController mAvalancheController; + private AvalancheController mAvalancheController; // TODO(b/328393698) move the topHeadsUpRow logic to an interactor private final MutableStateFlow<HeadsUpRowRepository> mTopHeadsUpRow = @@ -173,6 +174,9 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements }); javaAdapter.alwaysCollectFlow(shadeInteractor.isAnyExpanded(), this::onShadeOrQsExpanded); + mVisualStabilityProvider.addPersistentReorderingBannedListener(mOnReorderingBannedListener); + mVisualStabilityProvider.addPersistentReorderingAllowedListener( + mOnReorderingAllowedListener); } public void setAnimationStateHandler(AnimationStateHandler handler) { @@ -379,6 +383,7 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements private final OnReorderingAllowedListener mOnReorderingAllowedListener = () -> { mAnimationStateHandler.setHeadsUpGoingAwayAnimationsAllowed(false); + mAvalancheController.setEnableAtRuntime(true); for (NotificationEntry entry : mEntriesToRemoveWhenReorderingAllowed) { if (isHeadsUpEntry(entry.getKey())) { // Maybe the heads-up was removed already @@ -389,6 +394,22 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements mAnimationStateHandler.setHeadsUpGoingAwayAnimationsAllowed(true); }; + private final OnReorderingBannedListener mOnReorderingBannedListener = () -> { + if (mAvalancheController != null) { + // In open shade the first HUN is pinned, and visual stability logic prevents us from + // unpinning this first HUN as long as the shade remains open. AvalancheController only + // shows the next HUN when the currently showing HUN is unpinned, so we must disable + // throttling here so that the incoming HUN stream is not forever paused. This is reset + // when reorder becomes allowed. + mAvalancheController.setEnableAtRuntime(false); + + // Note that we cannot do the above when + // 1) The remove runnable runs because its delay means it may not run before shade close + // 2) Reordering is allowed again (when shade closes) because the HUN appear animation + // will have started by then + } + }; + /////////////////////////////////////////////////////////////////////////////////////////////// // HeadsUpManager utility (protected) methods overrides: @@ -561,18 +582,26 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements } @Override + protected void setEntry(@androidx.annotation.NonNull NotificationEntry entry, + @androidx.annotation.Nullable Runnable removeRunnable) { + super.setEntry(entry, removeRunnable); + + if (!mVisualStabilityProvider.isReorderingAllowed() + // We don't want to allow reordering while pulsing, but headsup need to + // time out anyway + && !entry.showingPulsing()) { + mEntriesToRemoveWhenReorderingAllowed.add(entry); + entry.setSeenInShade(true); + } + } + + @Override protected Runnable createRemoveRunnable(NotificationEntry entry) { - return () -> { - if (!mVisualStabilityProvider.isReorderingAllowed() - // We don't want to allow reordering while pulsing, but headsup need to - // time out anyway - && !entry.showingPulsing()) { - mEntriesToRemoveWhenReorderingAllowed.add(entry); - mVisualStabilityProvider.addTemporaryReorderingAllowedListener( - mOnReorderingAllowedListener); - } else if (mTrackingHeadsUp) { + return () -> { + if (mTrackingHeadsUp) { mEntriesToRemoveAfterExpand.add(entry); - } else { + } else if (mVisualStabilityProvider.isReorderingAllowed() + || entry.showingPulsing()) { removeEntry(entry.getKey(), "createRemoveRunnable"); } }; @@ -585,9 +614,6 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements if (mEntriesToRemoveAfterExpand.contains(mEntry)) { mEntriesToRemoveAfterExpand.remove(mEntry); } - if (mEntriesToRemoveWhenReorderingAllowed.contains(mEntry)) { - mEntriesToRemoveWhenReorderingAllowed.remove(mEntry); - } } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java index aced0be4cc46..0320a7ae103b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java @@ -528,9 +528,10 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue } @Override - public void onOngoingActivityStatusChanged(boolean hasOngoingActivity) { + public void onOngoingActivityStatusChanged( + boolean hasOngoingActivity, boolean shouldAnimate) { mHasOngoingActivity = hasOngoingActivity; - updateStatusBarVisibilities(/* animate= */ true); + updateStatusBarVisibilities(shouldAnimate); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt index ae1898bc479c..16bd7f830c66 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt @@ -110,7 +110,7 @@ class CollapsedStatusBarViewBinderImpl @Inject constructor() : CollapsedStatusBa chipView.setOnClickListener(chipModel.onClickListener) // Accessibility - setChipAccessibility(chipModel, chipView) + setChipAccessibility(chipModel, chipView, chipBackgroundView) // Colors val textColor = chipModel.colors.text(chipContext) @@ -122,7 +122,8 @@ class CollapsedStatusBarViewBinderImpl @Inject constructor() : CollapsedStatusBa // Notify listeners listener.onOngoingActivityStatusChanged( - hasOngoingActivity = true + hasOngoingActivity = true, + shouldAnimate = true, ) } is OngoingActivityChipModel.Hidden -> { @@ -130,7 +131,8 @@ class CollapsedStatusBarViewBinderImpl @Inject constructor() : CollapsedStatusBa // b/192243808 and [Chronometer.start]. chipTimeView.stop() listener.onOngoingActivityStatusChanged( - hasOngoingActivity = false + hasOngoingActivity = false, + shouldAnimate = chipModel.shouldAnimate, ) } } @@ -211,7 +213,11 @@ class CollapsedStatusBarViewBinderImpl @Inject constructor() : CollapsedStatusBa this.setPaddingRelative(/* start= */ 0, paddingTop, paddingEnd, paddingBottom) } - private fun setChipAccessibility(chipModel: OngoingActivityChipModel.Shown, chipView: View) { + private fun setChipAccessibility( + chipModel: OngoingActivityChipModel.Shown, + chipView: View, + chipBackgroundView: View, + ) { when (chipModel) { is OngoingActivityChipModel.Shown.Countdown -> { // Set as assertive so talkback will announce the countdown @@ -222,6 +228,16 @@ class CollapsedStatusBarViewBinderImpl @Inject constructor() : CollapsedStatusBa chipView.accessibilityLiveRegion = View.ACCESSIBILITY_LIVE_REGION_NONE } } + // Clickable chips need to be a minimum size for accessibility purposes, but let + // non-clickable chips be smaller. + if (chipModel.onClickListener != null) { + chipBackgroundView.minimumWidth = + chipBackgroundView.context.resources.getDimensionPixelSize( + R.dimen.min_clickable_item_size + ) + } else { + chipBackgroundView.minimumWidth = 0 + } } private fun animateLightsOutView(view: View, visible: Boolean) { @@ -266,8 +282,13 @@ interface StatusBarVisibilityChangeListener { /** Called when a transition from lockscreen to dream has started. */ fun onTransitionFromLockscreenToDreamStarted() - /** Called when the status of the ongoing activity chip (active or not active) has changed. */ - fun onOngoingActivityStatusChanged(hasOngoingActivity: Boolean) + /** + * Called when the status of the ongoing activity chip (active or not active) has changed. + * + * @param shouldAnimate true if the chip should animate in/out, and false if the chip should + * immediately appear/disappear. + */ + fun onOngoingActivityStatusChanged(hasOngoingActivity: Boolean, shouldAnimate: Boolean) /** * Called when the scene state has changed such that the home status bar is newly allowed or no diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt index 40799583a7b9..a88c6d707da4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt @@ -37,13 +37,27 @@ import javax.inject.Inject @SysUISingleton class AvalancheController @Inject -constructor(dumpManager: DumpManager, - private val uiEventLogger: UiEventLogger, - @Background private val bgHandler: Handler +constructor( + dumpManager: DumpManager, + private val uiEventLogger: UiEventLogger, + @Background private val bgHandler: Handler ) : Dumpable { private val tag = "AvalancheController" private val debug = Compile.IS_DEBUG && Log.isLoggable(tag, Log.DEBUG) + var enableAtRuntime = true + set(value) { + if (!value) { + // Waiting HUNs in AvalancheController are shown in the HUN section in open shade. + // Clear them so we don't show them again when the shade closes and reordering is + // allowed again. + logDroppedHunsInBackground(getWaitingKeys().size) + clearNext() + } + if (field != value) { + field = value + } + } // HUN showing right now, in the floating state where full shade is hidden, on launcher or AOD @VisibleForTesting var headsUpEntryShowing: HeadsUpEntry? = null @@ -90,13 +104,17 @@ constructor(dumpManager: DumpManager, return getKey(headsUpEntryShowing) } + fun isEnabled(): Boolean { + return NotificationThrottleHun.isEnabled && enableAtRuntime + } + /** Run or delay Runnable for given HeadsUpEntry */ fun update(entry: HeadsUpEntry?, runnable: Runnable?, label: String) { if (runnable == null) { log { "Runnable is NULL, stop update." } return } - if (!NotificationThrottleHun.isEnabled) { + if (!isEnabled()) { runnable.run() return } @@ -156,7 +174,7 @@ constructor(dumpManager: DumpManager, log { "Runnable is NULL, stop delete." } return } - if (!NotificationThrottleHun.isEnabled) { + if (!isEnabled()) { runnable.run() return } @@ -185,7 +203,8 @@ constructor(dumpManager: DumpManager, showNext() runnable.run() } else { - log { "$fn => removing untracked ${getKey(entry)}" } + log { "$fn => run runnable for untracked shown ${getKey(entry)}" } + runnable.run() } logState("after $fn") } @@ -197,7 +216,7 @@ constructor(dumpManager: DumpManager, * BaseHeadsUpManager.HeadsUpEntry.calculateFinishTime to shorten display duration. */ fun getDurationMs(entry: HeadsUpEntry, autoDismissMs: Int): Int { - if (!NotificationThrottleHun.isEnabled) { + if (!isEnabled()) { // Use default duration, like we did before AvalancheController existed return autoDismissMs } @@ -246,7 +265,7 @@ constructor(dumpManager: DumpManager, /** Return true if entry is waiting to show. */ fun isWaiting(key: String): Boolean { - if (!NotificationThrottleHun.isEnabled) { + if (!isEnabled()) { return false } for (entry in nextMap.keys) { @@ -259,7 +278,7 @@ constructor(dumpManager: DumpManager, /** Return list of keys for huns waiting */ fun getWaitingKeys(): MutableList<String> { - if (!NotificationThrottleHun.isEnabled) { + if (!isEnabled()) { return mutableListOf() } val keyList = mutableListOf<String>() @@ -270,7 +289,7 @@ constructor(dumpManager: DumpManager, } fun getWaitingEntry(key: String): HeadsUpEntry? { - if (!NotificationThrottleHun.isEnabled) { + if (!isEnabled()) { return null } for (headsUpEntry in nextMap.keys) { @@ -282,7 +301,7 @@ constructor(dumpManager: DumpManager, } fun getWaitingEntryList(): List<HeadsUpEntry> { - if (!NotificationThrottleHun.isEnabled) { + if (!isEnabled()) { return mutableListOf() } return nextMap.keys.toList() @@ -340,13 +359,15 @@ constructor(dumpManager: DumpManager, showNow(headsUpEntryShowing!!, headsUpEntryShowingRunnableList) } - fun logDroppedHunsInBackground(numDropped: Int) { - bgHandler.post(Runnable { - // Do this in the background to avoid missing frames when closing the shade - for (n in 1..numDropped) { - uiEventLogger.log(ThrottleEvent.AVALANCHE_THROTTLING_HUN_DROPPED) + private fun logDroppedHunsInBackground(numDropped: Int) { + bgHandler.post( + Runnable { + // Do this in the background to avoid missing frames when closing the shade + for (n in 1..numDropped) { + uiEventLogger.log(ThrottleEvent.AVALANCHE_THROTTLING_HUN_DROPPED) + } } - }) + ) } fun clearNext() { @@ -367,7 +388,8 @@ constructor(dumpManager: DumpManager, "\nPREVIOUS: [$previousHunKey]" + "\nNEXT LIST: $nextListStr" + "\nNEXT MAP: $nextMapStr" + - "\nDROPPED: $dropSetStr" + "\nDROPPED: $dropSetStr" + + "\nENABLED: $enableAtRuntime" } private fun logState(reason: String) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java index 220e729625af..a0eb989a57bb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java @@ -756,7 +756,7 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { setEntry(entry, createRemoveRunnable(entry)); } - private void setEntry(@NonNull final NotificationEntry entry, + protected void setEntry(@NonNull final NotificationEntry entry, @Nullable Runnable removeRunnable) { mEntry = entry; mRemoveRunnable = removeRunnable; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/data/repository/DeviceProvisioningRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/data/repository/DeviceProvisioningRepository.kt index 483855409b91..07bbca74e12e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/data/repository/DeviceProvisioningRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/data/repository/DeviceProvisioningRepository.kt @@ -31,6 +31,13 @@ interface DeviceProvisioningRepository { * @see android.provider.Settings.Global.DEVICE_PROVISIONED */ val isDeviceProvisioned: Flow<Boolean> + + /** + * Whether this device has been provisioned. + * + * @see android.provider.Settings.Global.DEVICE_PROVISIONED + */ + fun isDeviceProvisioned(): Boolean } @Module @@ -48,11 +55,15 @@ constructor( val listener = object : DeviceProvisionedController.DeviceProvisionedListener { override fun onDeviceProvisionedChanged() { - trySend(deviceProvisionedController.isDeviceProvisioned) + trySend(isDeviceProvisioned()) } } deviceProvisionedController.addCallback(listener) - trySend(deviceProvisionedController.isDeviceProvisioned) + trySend(isDeviceProvisioned()) awaitClose { deviceProvisionedController.removeCallback(listener) } } + + override fun isDeviceProvisioned(): Boolean { + return deviceProvisionedController.isDeviceProvisioned + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/DeviceProvisioningInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/DeviceProvisioningInteractor.kt index 66ed092fcce1..ace4ce0673a4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/DeviceProvisioningInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/DeviceProvisioningInteractor.kt @@ -26,7 +26,7 @@ import kotlinx.coroutines.flow.Flow class DeviceProvisioningInteractor @Inject constructor( - repository: DeviceProvisioningRepository, + private val repository: DeviceProvisioningRepository, ) { /** * Whether this device has been provisioned. @@ -34,4 +34,8 @@ constructor( * @see android.provider.Settings.Global.DEVICE_PROVISIONED */ val isDeviceProvisioned: Flow<Boolean> = repository.isDeviceProvisioned + + fun isDeviceProvisioned(): Boolean { + return repository.isDeviceProvisioned() + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt index e4d06681d439..7a521a6ba28f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt @@ -16,8 +16,14 @@ package com.android.systemui.statusbar.policy.domain.interactor +import android.content.Context import android.provider.Settings +import androidx.concurrent.futures.await import com.android.settingslib.notification.data.repository.ZenModeRepository +import com.android.settingslib.notification.modes.ZenIconLoader +import com.android.settingslib.notification.modes.ZenMode +import com.android.systemui.common.shared.model.Icon +import java.time.Duration import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine @@ -28,7 +34,9 @@ import kotlinx.coroutines.flow.map * An interactor that performs business logic related to the status and configuration of Zen Mode * (or Do Not Disturb/DND Mode). */ -class ZenModeInteractor @Inject constructor(repository: ZenModeRepository) { +class ZenModeInteractor @Inject constructor(private val repository: ZenModeRepository) { + private val iconLoader: ZenIconLoader = ZenIconLoader.getInstance() + val isZenModeEnabled: Flow<Boolean> = repository.globalZenMode .map { @@ -52,4 +60,18 @@ class ZenModeInteractor @Inject constructor(repository: ZenModeRepository) { } } .distinctUntilChanged() + + val modes: Flow<List<ZenMode>> = repository.modes + + suspend fun getModeIcon(mode: ZenMode, context: Context): Icon { + return Icon.Loaded(mode.getIcon(context, iconLoader).await(), contentDescription = null) + } + + fun activateMode(zenMode: ZenMode, duration: Duration? = null) { + repository.activateMode(zenMode, duration) + } + + fun deactivateMode(zenMode: ZenMode) { + repository.deactivateMode(zenMode) + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt new file mode 100644 index 000000000000..2b094d6b4922 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.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.statusbar.policy.ui.dialog + +import android.content.Intent +import android.provider.Settings +import androidx.compose.material3.Text +import androidx.compose.ui.res.stringResource +import com.android.compose.PlatformButton +import com.android.compose.PlatformOutlinedButton +import com.android.systemui.animation.DialogTransitionAnimator +import com.android.systemui.dialog.ui.composable.AlertDialogContent +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.res.R +import com.android.systemui.statusbar.phone.SystemUIDialog +import com.android.systemui.statusbar.phone.SystemUIDialogFactory +import com.android.systemui.statusbar.phone.create +import com.android.systemui.statusbar.policy.ui.dialog.composable.ModeTileGrid +import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.ModesDialogViewModel +import javax.inject.Inject + +class ModesDialogDelegate +@Inject +constructor( + private val sysuiDialogFactory: SystemUIDialogFactory, + private val dialogTransitionAnimator: DialogTransitionAnimator, + private val activityStarter: ActivityStarter, + private val viewModel: ModesDialogViewModel, +) : SystemUIDialog.Delegate { + override fun createDialog(): SystemUIDialog { + return sysuiDialogFactory.create { dialog -> + AlertDialogContent( + title = { Text(stringResource(R.string.zen_modes_dialog_title)) }, + content = { ModeTileGrid(viewModel) }, + neutralButton = { + PlatformOutlinedButton( + onClick = { + val animationController = + dialogTransitionAnimator.createActivityTransitionController( + dialog.getButton(SystemUIDialog.BUTTON_NEUTRAL) + ) + if (animationController == null) { + // The controller will take care of dismissing for us after the + // animation, but let's make sure we dismiss the dialog if we don't + // animate it. + dialog.dismiss() + } + activityStarter.startActivity( + ZEN_MODE_SETTINGS_INTENT, + true /* dismissShade */, + animationController + ) + } + ) { + Text(stringResource(R.string.zen_modes_dialog_settings)) + } + }, + positiveButton = { + PlatformButton(onClick = { dialog.dismiss() }) { + Text(stringResource(R.string.zen_modes_dialog_done)) + } + }, + ) + } + } + + companion object { + private val ZEN_MODE_SETTINGS_INTENT = Intent(Settings.ACTION_ZEN_MODE_SETTINGS) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt new file mode 100644 index 000000000000..91bfdff1095e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.policy.ui.dialog.composable + +import androidx.compose.foundation.basicMarquee +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.android.systemui.common.ui.compose.Icon +import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.ModeTileViewModel + +@Composable +fun ModeTile(viewModel: ModeTileViewModel) { + val tileColor = + if (viewModel.enabled) MaterialTheme.colorScheme.primary + else MaterialTheme.colorScheme.surfaceVariant + val contentColor = + if (viewModel.enabled) MaterialTheme.colorScheme.onPrimary + else MaterialTheme.colorScheme.onSurfaceVariant + + CompositionLocalProvider(LocalContentColor provides contentColor) { + Surface( + color = tileColor, + shape = RoundedCornerShape(16.dp), + modifier = + Modifier.combinedClickable( + onClick = viewModel.onClick, + onLongClick = viewModel.onLongClick + ), + ) { + Row( + modifier = Modifier.padding(20.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = + Arrangement.spacedBy( + space = 10.dp, + alignment = Alignment.Start, + ), + ) { + Icon(icon = viewModel.icon, modifier = Modifier.size(24.dp)) + Column { + Text( + viewModel.text, + fontWeight = FontWeight.W500, + modifier = Modifier.tileMarquee() + ) + Text( + viewModel.subtext, + fontWeight = FontWeight.W400, + modifier = Modifier.tileMarquee() + ) + } + } + } + } +} + +private fun Modifier.tileMarquee(): Modifier { + return this.basicMarquee( + iterations = 1, + initialDelayMillis = 200, + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTileGrid.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTileGrid.kt new file mode 100644 index 000000000000..73d361f69eac --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTileGrid.kt @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.policy.ui.dialog.composable + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.ModesDialogViewModel + +@Composable +fun ModeTileGrid(viewModel: ModesDialogViewModel) { + val tiles by viewModel.tiles.collectAsStateWithLifecycle(initialValue = emptyList()) + + // TODO(b/346519570): Handle what happens when we have more than a few modes. + LazyVerticalGrid( + columns = GridCells.Fixed(2), + modifier = Modifier.padding(8.dp).fillMaxWidth().heightIn(max = 300.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + items( + tiles.size, + key = { index -> tiles[index].id }, + ) { index -> + ModeTile(viewModel = tiles[index]) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModeTileViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModeTileViewModel.kt new file mode 100644 index 000000000000..5bd26ccc965f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModeTileViewModel.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.policy.ui.dialog.viewmodel + +import com.android.systemui.common.shared.model.Icon + +/** + * Viewmodel for a tile representing a single priority ("zen") mode, for use within the modes + * dialog. Not to be confused with ModesTile, which is the Quick Settings tile that opens the + * dialog. + */ +data class ModeTileViewModel( + val id: String, + val icon: Icon, + val text: String, + val subtext: String, + val enabled: Boolean, + val contentDescription: String, + val onClick: () -> Unit, + val onLongClick: () -> Unit, +) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt new file mode 100644 index 000000000000..e84c8b61ff54 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt @@ -0,0 +1,87 @@ +/* + * 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.statusbar.policy.ui.dialog.viewmodel + +import android.content.Context +import com.android.settingslib.notification.modes.ZenMode +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.res.R +import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map + +/** + * Viewmodel for the priority ("zen") modes dialog that can be opened from quick settings. It allows + * the user to quickly toggle modes. + */ +@SysUISingleton +class ModesDialogViewModel +@Inject +constructor( + val context: Context, + zenModeInteractor: ZenModeInteractor, + @Background val bgDispatcher: CoroutineDispatcher, +) { + // Modes that should be displayed in the dialog + // TODO(b/346519570): Include modes that have not been set up yet. + private val visibleModes: Flow<List<ZenMode>> = + zenModeInteractor.modes.map { + it.filter { mode -> + mode.rule.isEnabled && (mode.isActive || mode.rule.isManualInvocationAllowed) + } + } + + val tiles: Flow<List<ModeTileViewModel>> = + visibleModes + .map { modesList -> + modesList.map { mode -> + ModeTileViewModel( + id = mode.id, + icon = zenModeInteractor.getModeIcon(mode, context), + text = mode.rule.name, + subtext = getTileSubtext(mode), + enabled = mode.isActive, + // TODO(b/346519570): This should be some combination of the above, e.g. + // "ON: Do Not Disturb, Until Mon 08:09"; see DndTile. + contentDescription = "", + onClick = { + if (mode.isActive) { + zenModeInteractor.deactivateMode(mode) + } else { + // TODO(b/346519570): Handle duration for DND mode. + zenModeInteractor.activateMode(mode) + } + }, + onLongClick = { + // TODO(b/346519570): Open settings page for mode. + } + ) + } + } + .flowOn(bgDispatcher) + + private fun getTileSubtext(mode: ZenMode): String { + // TODO(b/346519570): Use ZenModeConfig.getDescription for manual DND + val on = context.resources.getString(R.string.zen_mode_on) + val off = context.resources.getString(R.string.zen_mode_off) + return mode.rule.triggerDescription ?: if (mode.isActive) on else off + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java index c45f98e5f4f5..066bfc5c588d 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java @@ -18,6 +18,8 @@ package com.android.systemui.volume; import static android.media.AudioManager.RINGER_MODE_NORMAL; +import static com.android.settingslib.flags.Flags.volumeDialogAudioSharingFix; + import android.app.ActivityManager; import android.app.KeyguardManager; import android.app.NotificationManager; @@ -59,6 +61,8 @@ import android.view.accessibility.AccessibilityManager; import android.view.accessibility.CaptioningManager; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import androidx.lifecycle.Observer; import com.android.internal.annotations.GuardedBy; @@ -76,6 +80,8 @@ import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.util.RingerModeLiveData; import com.android.systemui.util.RingerModeTracker; import com.android.systemui.util.concurrency.ThreadFactory; +import com.android.systemui.util.kotlin.JavaAdapter; +import com.android.systemui.volume.domain.interactor.AudioSharingInteractor; import dalvik.annotation.optimization.NeverCompile; @@ -102,7 +108,13 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); private static final int TOUCH_FEEDBACK_TIMEOUT_MS = 1000; - private static final int DYNAMIC_STREAM_START_INDEX = 100; + // We only need one dynamic stream for broadcast because at most two headsets are allowed + // to join local broadcast in current stage. + // It is safe to use 99 as the broadcast stream now. There are only 10+ default audio + // streams defined in AudioSystem for now and audio team is in the middle of restructure, + // no new default stream is preferred. + @VisibleForTesting static final int DYNAMIC_STREAM_BROADCAST = 99; + private static final int DYNAMIC_STREAM_REMOTE_START_INDEX = 100; private static final AudioAttributes SONIFICIATION_VIBRATION_ATTRIBUTES = new AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) @@ -145,6 +157,8 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa private final State mState = new State(); protected final MediaSessionsCallbacks mMediaSessionsCallbacksW; private final VibratorHelper mVibrator; + private final AudioSharingInteractor mAudioSharingInteractor; + private final JavaAdapter mJavaAdapter; private final boolean mHasVibrator; private boolean mShowA11yStream; private boolean mShowVolumeDialog; @@ -188,7 +202,9 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa KeyguardManager keyguardManager, ActivityManager activityManager, UserTracker userTracker, - DumpManager dumpManager + DumpManager dumpManager, + AudioSharingInteractor audioSharingInteractor, + JavaAdapter javaAdapter ) { mContext = context.getApplicationContext(); mPackageManager = packageManager; @@ -200,6 +216,8 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa mRouter2Manager = MediaRouter2Manager.getInstance(mContext); mMediaSessionsCallbacksW = new MediaSessionsCallbacks(mContext); mMediaSessions = createMediaSessions(mContext, mWorkerLooper, mMediaSessionsCallbacksW); + mAudioSharingInteractor = audioSharingInteractor; + mJavaAdapter = javaAdapter; mAudio = audioManager; mNoMan = notificationManager; mObserver = new SettingObserver(mWorker); @@ -272,6 +290,12 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa } catch (SecurityException e) { Log.w(TAG, "No access to media sessions", e); } + if (volumeDialogAudioSharingFix()) { + Slog.d(TAG, "Start collect volume changes in audio sharing"); + mJavaAdapter.alwaysCollectFlow( + mAudioSharingInteractor.getVolume(), + this::handleAudioSharingStreamVolumeChanges); + } } public void setVolumePolicy(VolumePolicy policy) { @@ -545,7 +569,13 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa mState.activeStream = activeStream; Events.writeEvent(Events.EVENT_ACTIVE_STREAM_CHANGED, activeStream); if (D.BUG) Log.d(TAG, "updateActiveStreamW " + activeStream); - final int s = activeStream < DYNAMIC_STREAM_START_INDEX ? activeStream : -1; + final int s = + activeStream + < (volumeDialogAudioSharingFix() + ? DYNAMIC_STREAM_BROADCAST + : DYNAMIC_STREAM_REMOTE_START_INDEX) + ? activeStream + : -1; if (D.BUG) Log.d(TAG, "forceVolumeControlStream " + s); mAudio.forceVolumeControlStream(s); return true; @@ -726,7 +756,12 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa private void onSetStreamVolumeW(int stream, int level) { if (D.BUG) Log.d(TAG, "onSetStreamVolume " + stream + " level=" + level); - if (stream >= DYNAMIC_STREAM_START_INDEX) { + if (volumeDialogAudioSharingFix() && stream == DYNAMIC_STREAM_BROADCAST) { + Slog.d(TAG, "onSetStreamVolumeW set broadcast stream level = " + level); + mAudioSharingInteractor.setStreamVolume(level); + return; + } + if (stream >= DYNAMIC_STREAM_REMOTE_START_INDEX) { mMediaSessionsCallbacksW.setStreamVolume(stream, level); return; } @@ -758,6 +793,40 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa DndTile.setVisible(mContext, true); } + void handleAudioSharingStreamVolumeChanges(@Nullable Integer volume) { + if (volume == null) { + if (mState.states.contains(DYNAMIC_STREAM_BROADCAST)) { + mState.states.remove(DYNAMIC_STREAM_BROADCAST); + Slog.d(TAG, "Remove audio sharing stream"); + mCallbacks.onStateChanged(mState); + } + } else { + if (mState.states.contains(DYNAMIC_STREAM_BROADCAST)) { + StreamState ss = mState.states.get(DYNAMIC_STREAM_BROADCAST); + if (ss.level != volume) { + ss.level = volume; + Slog.d(TAG, "updateState, audio sharing stream volume = " + volume); + mCallbacks.onStateChanged(mState); + } + } else { + StreamState ss = streamStateW(DYNAMIC_STREAM_BROADCAST); + ss.dynamic = true; + ss.levelMin = mAudioSharingInteractor.getVolumeMin(); + ss.levelMax = mAudioSharingInteractor.getVolumeMax(); + if (ss.level != volume) { + ss.level = volume; + } + String label = mContext.getString(R.string.audio_sharing_description); + if (!Objects.equals(ss.remoteLabel, label)) { + ss.name = -1; + ss.remoteLabel = label; + } + Slog.d(TAG, "updateState, new audio sharing stream volume = " + volume); + mCallbacks.onStateChanged(mState); + } + } + } + private final class VC extends IVolumeController.Stub { private final String TAG = VolumeDialogControllerImpl.TAG + ".VC"; @@ -1256,7 +1325,7 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa protected final class MediaSessionsCallbacks implements MediaSessions.Callbacks { private final HashMap<Token, Integer> mRemoteStreams = new HashMap<>(); - private int mNextStream = DYNAMIC_STREAM_START_INDEX; + private int mNextStream = DYNAMIC_STREAM_REMOTE_START_INDEX; private final boolean mVolumeAdjustmentForRemoteGroupSessions; public MediaSessionsCallbacks(Context context) { diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java index 6b02e1ada491..0770d8926389 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java @@ -34,6 +34,7 @@ import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; import static com.android.internal.jank.InteractionJankMonitor.CUJ_VOLUME_CONTROL; import static com.android.internal.jank.InteractionJankMonitor.Configuration.Builder; +import static com.android.settingslib.flags.Flags.volumeDialogAudioSharingFix; import static com.android.systemui.Flags.hapticVolumeSlider; import static com.android.systemui.volume.Events.DISMISS_REASON_POSTURE_CHANGED; import static com.android.systemui.volume.Events.DISMISS_REASON_SETTINGS_CLICKED; @@ -1678,6 +1679,14 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, return true; } + // Always show the stream for audio sharing if it exists. + if (volumeDialogAudioSharingFix() + && row.ss != null + && mContext.getString(R.string.audio_sharing_description) + .equals(row.ss.remoteLabel)) { + return true; + } + if (row.defaultStream) { return activeRow.stream == STREAM_RING || activeRow.stream == STREAM_ALARM @@ -1880,10 +1889,25 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, if (!ss.dynamic) continue; mDynamic.put(stream, true); if (findRow(stream) == null) { - addRow(stream, - com.android.settingslib.R.drawable.ic_volume_remote, - com.android.settingslib.R.drawable.ic_volume_remote_mute, - true, false, true); + if (volumeDialogAudioSharingFix() + && mContext.getString(R.string.audio_sharing_description) + .equals(ss.remoteLabel)) { + addRow( + stream, + R.drawable.ic_volume_media, + R.drawable.ic_volume_media_mute, + true, + false, + true); + } else { + addRow( + stream, + com.android.settingslib.R.drawable.ic_volume_remote, + com.android.settingslib.R.drawable.ic_volume_remote_mute, + true, + false, + true); + } } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioSharingEmptyImplModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioSharingEmptyImplModule.kt index 29040923a2cc..cf80263b4d14 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioSharingEmptyImplModule.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioSharingEmptyImplModule.kt @@ -16,20 +16,15 @@ package com.android.systemui.volume.dagger -import com.android.systemui.dagger.SysUISingleton import com.android.systemui.volume.domain.interactor.AudioSharingInteractor import com.android.systemui.volume.domain.interactor.AudioSharingInteractorEmptyImpl +import dagger.Binds import dagger.Module -import dagger.Provides /** Dagger module for empty audio sharing impl for unnecessary volume overlay */ @Module interface AudioSharingEmptyImplModule { - companion object { - @Provides - @SysUISingleton - fun provideAudioSharingInteractor(): AudioSharingInteractor = - AudioSharingInteractorEmptyImpl() - } + @Binds + fun bindsAudioSharingInteractor(impl: AudioSharingInteractorEmptyImpl): AudioSharingInteractor } diff --git a/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioSharingInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioSharingInteractor.kt index 4d29788edf68..aba3015a6b7d 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioSharingInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioSharingInteractor.kt @@ -77,7 +77,7 @@ constructor( } @SysUISingleton -class AudioSharingInteractorEmptyImpl : AudioSharingInteractor { +class AudioSharingInteractorEmptyImpl @Inject constructor() : AudioSharingInteractor { override val volume: Flow<Int?> = emptyFlow() override val volumeMin: Int = EMPTY_VOLUME override val volumeMax: Int = EMPTY_VOLUME diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java index ec9b5cfbdeb2..a71435168aca 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java @@ -263,6 +263,11 @@ public final class WMShell implements public void onFinishedWakingUp() { splitScreen.onFinishedWakingUp(); } + + @Override + public void onStartedGoingToSleep() { + splitScreen.onStartedGoingToSleep(); + } }); mCommandQueue.addCallback(new CommandQueue.Callbacks() { @Override diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt index 79933ee3b018..a94ef36bda29 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt @@ -16,6 +16,7 @@ package com.android.keyguard import android.content.BroadcastReceiver +import android.platform.test.annotations.DisableFlags import android.view.View import android.view.ViewTreeObserver import android.widget.FrameLayout @@ -263,9 +264,9 @@ class ClockEventControllerTest : SysuiTestCase() { } @Test + @DisableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) fun keyguardCallback_visibilityChanged_clockDozeCalled() = runBlocking(IMMEDIATE) { - mSetFlagsRule.disableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) val captor = argumentCaptor<KeyguardUpdateMonitorCallback>() verify(keyguardUpdateMonitor).registerCallback(capture(captor)) diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java index 1d7816848cd0..892375d002c1 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java @@ -29,6 +29,7 @@ import static org.mockito.Mockito.when; import android.database.ContentObserver; import android.os.UserHandle; +import android.platform.test.annotations.DisableFlags; import android.provider.Settings; import android.view.View; @@ -48,11 +49,10 @@ import org.mockito.verification.VerificationMode; @SmallTest @RunWith(AndroidJUnit4.class) +@DisableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public class KeyguardClockSwitchControllerTest extends KeyguardClockSwitchControllerBaseTest { @Test public void testInit_viewAlreadyAttached() { - mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT); - mController.init(); verifyAttachment(times(1)); @@ -60,8 +60,6 @@ public class KeyguardClockSwitchControllerTest extends KeyguardClockSwitchContro @Test public void testInit_viewNotYetAttached() { - mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT); - ArgumentCaptor<View.OnAttachStateChangeListener> listenerArgumentCaptor = ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class); @@ -78,16 +76,12 @@ public class KeyguardClockSwitchControllerTest extends KeyguardClockSwitchContro @Test public void testInitSubControllers() { - mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT); - mController.init(); verify(mKeyguardSliceViewController).init(); } @Test public void testInit_viewDetached() { - mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT); - ArgumentCaptor<View.OnAttachStateChangeListener> listenerArgumentCaptor = ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class); mController.init(); @@ -101,8 +95,6 @@ public class KeyguardClockSwitchControllerTest extends KeyguardClockSwitchContro @Test public void testPluginPassesStatusBarState() { - mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT); - ArgumentCaptor<ClockRegistry.ClockChangeListener> listenerArgumentCaptor = ArgumentCaptor.forClass(ClockRegistry.ClockChangeListener.class); @@ -116,8 +108,6 @@ public class KeyguardClockSwitchControllerTest extends KeyguardClockSwitchContro @Test public void testSmartspaceEnabledRemovesKeyguardStatusArea() { - mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT); - when(mSmartspaceController.isEnabled()).thenReturn(true); mController.init(); @@ -126,8 +116,6 @@ public class KeyguardClockSwitchControllerTest extends KeyguardClockSwitchContro @Test public void onLocaleListChangedRebuildsSmartspaceView() { - mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT); - when(mSmartspaceController.isEnabled()).thenReturn(true); mController.init(); @@ -138,8 +126,6 @@ public class KeyguardClockSwitchControllerTest extends KeyguardClockSwitchContro @Test public void onLocaleListChanged_rebuildsSmartspaceViews_whenDecouplingEnabled() { - mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT); - when(mSmartspaceController.isEnabled()).thenReturn(true); when(mSmartspaceController.isDateWeatherDecoupled()).thenReturn(true); mController.init(); @@ -153,8 +139,6 @@ public class KeyguardClockSwitchControllerTest extends KeyguardClockSwitchContro @Test public void testSmartspaceDisabledShowsKeyguardStatusArea() { - mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT); - when(mSmartspaceController.isEnabled()).thenReturn(false); mController.init(); @@ -163,8 +147,6 @@ public class KeyguardClockSwitchControllerTest extends KeyguardClockSwitchContro @Test public void testRefresh() { - mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT); - mController.refresh(); verify(mSmartspaceController).requestSmartspaceUpdate(); @@ -172,8 +154,6 @@ public class KeyguardClockSwitchControllerTest extends KeyguardClockSwitchContro @Test public void testChangeToDoubleLineClockSetsSmallClock() { - mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT); - when(mSecureSettings.getIntForUser(Settings.Secure.LOCKSCREEN_USE_DOUBLE_LINE_CLOCK, 1, UserHandle.USER_CURRENT)) .thenReturn(0); @@ -197,15 +177,11 @@ public class KeyguardClockSwitchControllerTest extends KeyguardClockSwitchContro @Test public void testGetClock_ForwardsToClock() { - mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT); - assertEquals(mClockController, mController.getClock()); } @Test public void testGetLargeClockBottom_returnsExpectedValue() { - mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT); - when(mLargeClockFrame.getVisibility()).thenReturn(View.VISIBLE); when(mLargeClockFrame.getHeight()).thenReturn(100); when(mSmallClockFrame.getHeight()).thenReturn(50); @@ -218,8 +194,6 @@ public class KeyguardClockSwitchControllerTest extends KeyguardClockSwitchContro @Test public void testGetSmallLargeClockBottom_returnsExpectedValue() { - mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT); - when(mLargeClockFrame.getVisibility()).thenReturn(View.GONE); when(mLargeClockFrame.getHeight()).thenReturn(100); when(mSmallClockFrame.getHeight()).thenReturn(50); @@ -232,16 +206,12 @@ public class KeyguardClockSwitchControllerTest extends KeyguardClockSwitchContro @Test public void testGetClockBottom_nullClock_returnsZero() { - mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT); - when(mClockEventController.getClock()).thenReturn(null); assertEquals(0, mController.getClockBottom(10)); } @Test public void testChangeLockscreenWeatherEnabledSetsWeatherViewVisible() { - mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT); - when(mSmartspaceController.isWeatherEnabled()).thenReturn(true); ArgumentCaptor<ContentObserver> observerCaptor = ArgumentCaptor.forClass(ContentObserver.class); @@ -260,8 +230,6 @@ public class KeyguardClockSwitchControllerTest extends KeyguardClockSwitchContro @Test public void testChangeClockDateWeatherEnabled_SetsDateWeatherViewVisibility() { - mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT); - ArgumentCaptor<ClockRegistry.ClockChangeListener> listenerArgumentCaptor = ArgumentCaptor.forClass(ClockRegistry.ClockChangeListener.class); when(mSmartspaceController.isEnabled()).thenReturn(true); @@ -284,15 +252,11 @@ public class KeyguardClockSwitchControllerTest extends KeyguardClockSwitchContro @Test public void testGetClock_nullClock_returnsNull() { - mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT); - when(mClockEventController.getClock()).thenReturn(null); assertNull(mController.getClock()); } private void verifyAttachment(VerificationMode times) { - mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT); - verify(mClockRegistry, times).registerClockChangeListener( any(ClockRegistry.ClockChangeListener.class)); verify(mClockEventController, times).registerListeners(mView); @@ -300,8 +264,6 @@ public class KeyguardClockSwitchControllerTest extends KeyguardClockSwitchContro @Test public void testSplitShadeEnabledSetToSmartspaceController() { - mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT); - mController.setSplitShadeEnabled(true); verify(mSmartspaceController, times(1)).setSplitShadeEnabled(true); verify(mSmartspaceController, times(0)).setSplitShadeEnabled(false); @@ -309,8 +271,6 @@ public class KeyguardClockSwitchControllerTest extends KeyguardClockSwitchContro @Test public void testSplitShadeDisabledSetToSmartspaceController() { - mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT); - mController.setSplitShadeEnabled(false); verify(mSmartspaceController, times(1)).setSplitShadeEnabled(false); verify(mSmartspaceController, times(0)).setSplitShadeEnabled(true); diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java index 83443bee744e..0bf9d12a09d5 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java @@ -30,6 +30,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import android.content.Context; +import android.platform.test.annotations.DisableFlags; import android.testing.TestableLooper.RunWithLooper; import android.util.AttributeSet; import android.view.LayoutInflater; @@ -60,6 +61,7 @@ import org.mockito.MockitoAnnotations; // the main thread before acquiring a wake lock. This class is constructed when // the keyguard_clock_switch layout is inflated. @RunWithLooper(setAsMainLooper = true) +@DisableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public class KeyguardClockSwitchTest extends SysuiTestCase { @Mock ViewGroup mMockKeyguardSliceView; @@ -81,8 +83,6 @@ public class KeyguardClockSwitchTest extends SysuiTestCase { @Before public void setUp() { - mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT); - MockitoAnnotations.initMocks(this); when(mMockKeyguardSliceView.getContext()).thenReturn(mContext); when(mMockKeyguardSliceView.findViewById(R.id.keyguard_status_area)) diff --git a/packages/SystemUI/tests/src/com/android/keyguard/LegacyLockIconViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/keyguard/LegacyLockIconViewControllerBaseTest.java index b09357f853b9..c51aa04fc7b6 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/LegacyLockIconViewControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/LegacyLockIconViewControllerBaseTest.java @@ -151,6 +151,7 @@ public class LegacyLockIconViewControllerBaseTest extends SysuiTestCase { if (!SceneContainerFlag.isEnabled()) { mSetFlagsRule.disableFlags(Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR); + //TODO move this to use @DisableFlags annotation if needed mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java b/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java index 639b53bfb95b..5600b87280ad 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java @@ -710,6 +710,35 @@ public class TouchMonitorTest extends SysuiTestCase { environment.verifyLifecycleObserversUnregistered(); } + @Test + public void testLastSessionPop_createsNewInputSession() { + final TouchHandler touchHandler = createTouchHandler(); + + final TouchHandler.TouchSession.Callback callback = + Mockito.mock(TouchHandler.TouchSession.Callback.class); + + final Environment environment = new Environment(Stream.of(touchHandler) + .collect(Collectors.toCollection(HashSet::new)), mKosmos); + + final InputEvent initialEvent = Mockito.mock(InputEvent.class); + environment.publishInputEvent(initialEvent); + + final TouchHandler.TouchSession session = captureSession(touchHandler); + session.registerCallback(callback); + + // Clear invocations on input session and factory. + clearInvocations(environment.mInputFactory); + clearInvocations(environment.mInputSession); + + // Pop only active touch session. + session.pop(); + environment.executeAll(); + + // Verify that input session disposed and new session requested from factory. + verify(environment.mInputSession).dispose(); + verify(environment.mInputFactory).create(any(), any(), any(), anyBoolean()); + } + private GestureDetector.OnGestureListener registerGestureListener(TouchHandler handler) { final GestureDetector.OnGestureListener gestureListener = Mockito.mock( GestureDetector.OnGestureListener.class); diff --git a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java index d940fc938c4b..7a4bbfe9a580 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java @@ -36,7 +36,6 @@ import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; import com.android.systemui.flags.FakeFeatureFlags; -import com.android.systemui.res.R; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.phone.StatusBarLocation; import com.android.systemui.statusbar.policy.BatteryController; @@ -77,9 +76,6 @@ public class BatteryMeterViewControllerTest extends SysuiTestCase { when(mBatteryMeterView.getContext()).thenReturn(mContext); when(mBatteryMeterView.getResources()).thenReturn(mContext.getResources()); - - mContext.getOrCreateTestableResources().addOverride( - R.bool.flag_battery_shield_icon, false); } @Test @@ -136,26 +132,6 @@ public class BatteryMeterViewControllerTest extends SysuiTestCase { verify(mTunerService, never()).addTunable(any(), any()); } - @Test - public void shieldFlagDisabled_viewNotified() { - mContext.getOrCreateTestableResources().addOverride( - R.bool.flag_battery_shield_icon, false); - - initController(); - - verify(mBatteryMeterView).setDisplayShieldEnabled(false); - } - - @Test - public void shieldFlagEnabled_viewNotified() { - mContext.getOrCreateTestableResources().addOverride( - R.bool.flag_battery_shield_icon, true); - - initController(); - - verify(mBatteryMeterView).setDisplayShieldEnabled(true); - } - private void initController() { mController = new BatteryMeterViewController( mBatteryMeterView, diff --git a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt index 2bd0976f30dc..2aa33a176ac9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt @@ -22,9 +22,9 @@ import android.widget.ImageView import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.settingslib.flags.Flags.FLAG_NEW_STATUS_BAR_ICONS -import com.android.systemui.res.R import com.android.systemui.SysuiTestCase import com.android.systemui.battery.BatteryMeterView.BatteryEstimateFetcher +import com.android.systemui.res.R import com.android.systemui.statusbar.policy.BatteryController.EstimateFetchCompletion import com.google.common.truth.Truth.assertThat import org.junit.Before @@ -67,9 +67,8 @@ class BatteryMeterViewTest : SysuiTestCase() { fun contentDescription_unknown() { mBatteryMeterView.onBatteryUnknownStateChanged(true) - assertThat(mBatteryMeterView.contentDescription).isEqualTo( - context.getString(R.string.accessibility_battery_unknown) - ) + assertThat(mBatteryMeterView.contentDescription) + .isEqualTo(context.getString(R.string.accessibility_battery_unknown)) } @Test @@ -80,11 +79,10 @@ class BatteryMeterViewTest : SysuiTestCase() { mBatteryMeterView.updatePercentText() - assertThat(mBatteryMeterView.contentDescription).isEqualTo( - context.getString( - R.string.accessibility_battery_level_with_estimate, 15, ESTIMATE - ) - ) + assertThat(mBatteryMeterView.contentDescription) + .isEqualTo( + context.getString(R.string.accessibility_battery_level_with_estimate, 15, ESTIMATE) + ) } @Test @@ -96,13 +94,14 @@ class BatteryMeterViewTest : SysuiTestCase() { mBatteryMeterView.updatePercentText() - assertThat(mBatteryMeterView.contentDescription).isEqualTo( + assertThat(mBatteryMeterView.contentDescription) + .isEqualTo( context.getString( - R.string.accessibility_battery_level_charging_paused_with_estimate, - 17, - ESTIMATE, + R.string.accessibility_battery_level_charging_paused_with_estimate, + 17, + ESTIMATE, ) - ) + ) } @Test @@ -110,27 +109,24 @@ class BatteryMeterViewTest : SysuiTestCase() { mBatteryMeterView.onBatteryLevelChanged(90, false) mBatteryMeterView.onIsBatteryDefenderChanged(true) - assertThat(mBatteryMeterView.contentDescription).isEqualTo( - context.getString(R.string.accessibility_battery_level_charging_paused, 90) - ) + assertThat(mBatteryMeterView.contentDescription) + .isEqualTo(context.getString(R.string.accessibility_battery_level_charging_paused, 90)) } @Test fun contentDescription_charging() { mBatteryMeterView.onBatteryLevelChanged(45, true) - assertThat(mBatteryMeterView.contentDescription).isEqualTo( - context.getString(R.string.accessibility_battery_level_charging, 45) - ) + assertThat(mBatteryMeterView.contentDescription) + .isEqualTo(context.getString(R.string.accessibility_battery_level_charging, 45)) } @Test fun contentDescription_notCharging() { mBatteryMeterView.onBatteryLevelChanged(45, false) - assertThat(mBatteryMeterView.contentDescription).isEqualTo( - context.getString(R.string.accessibility_battery_level, 45) - ) + assertThat(mBatteryMeterView.contentDescription) + .isEqualTo(context.getString(R.string.accessibility_battery_level, 45)) } @Test @@ -138,9 +134,8 @@ class BatteryMeterViewTest : SysuiTestCase() { mBatteryMeterView.onBatteryLevelChanged(45, true) mBatteryMeterView.onIsIncompatibleChargingChanged(true) - assertThat(mBatteryMeterView.contentDescription).isEqualTo( - context.getString(R.string.accessibility_battery_level, 45) - ) + assertThat(mBatteryMeterView.contentDescription) + .isEqualTo(context.getString(R.string.accessibility_battery_level, 45)) } @Test @@ -152,19 +147,17 @@ class BatteryMeterViewTest : SysuiTestCase() { mBatteryMeterView.updatePercentText() - assertThat(mBatteryMeterView.contentDescription).isEqualTo( - context.getString( - R.string.accessibility_battery_level_with_estimate, 15, ESTIMATE - ) - ) + assertThat(mBatteryMeterView.contentDescription) + .isEqualTo( + context.getString(R.string.accessibility_battery_level_with_estimate, 15, ESTIMATE) + ) // Update the show mode from estimate to percent mBatteryMeterView.setPercentShowMode(BatteryMeterView.MODE_ON) assertThat(mBatteryMeterView.batteryPercentViewText).isEqualTo("15%") - assertThat(mBatteryMeterView.contentDescription).isEqualTo( - context.getString(R.string.accessibility_battery_level, 15) - ) + assertThat(mBatteryMeterView.contentDescription) + .isEqualTo(context.getString(R.string.accessibility_battery_level, 15)) } @Test @@ -176,19 +169,17 @@ class BatteryMeterViewTest : SysuiTestCase() { mBatteryMeterView.updatePercentText() - assertThat(mBatteryMeterView.contentDescription).isEqualTo( - context.getString( - R.string.accessibility_battery_level_with_estimate, 15, ESTIMATE + assertThat(mBatteryMeterView.contentDescription) + .isEqualTo( + context.getString(R.string.accessibility_battery_level_with_estimate, 15, ESTIMATE) ) - ) // Update the show mode from estimate to percent mBatteryMeterView.setPercentShowMode(BatteryMeterView.MODE_ON) assertThat(mBatteryMeterView.batteryPercentView).isNull() - assertThat(mBatteryMeterView.contentDescription).isEqualTo( - context.getString(R.string.accessibility_battery_level, 15) - ) + assertThat(mBatteryMeterView.contentDescription) + .isEqualTo(context.getString(R.string.accessibility_battery_level, 15)) assertThat(mBatteryMeterView.unifiedBatteryState.showPercent).isTrue() } @@ -225,49 +216,47 @@ class BatteryMeterViewTest : SysuiTestCase() { // BatteryDefender mBatteryMeterView.onBatteryLevelChanged(90, false) mBatteryMeterView.onIsBatteryDefenderChanged(true) - assertThat(mBatteryMeterView.contentDescription).isEqualTo( - context.getString(R.string.accessibility_battery_level_charging_paused, 90) - ) + assertThat(mBatteryMeterView.contentDescription) + .isEqualTo(context.getString(R.string.accessibility_battery_level_charging_paused, 90)) // BatteryDefender & estimate mBatteryMeterView.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE) mBatteryMeterView.setBatteryEstimateFetcher(Fetcher()) mBatteryMeterView.updatePercentText() - assertThat(mBatteryMeterView.contentDescription).isEqualTo( + assertThat(mBatteryMeterView.contentDescription) + .isEqualTo( context.getString( - R.string.accessibility_battery_level_charging_paused_with_estimate, - 90, - ESTIMATE, + R.string.accessibility_battery_level_charging_paused_with_estimate, + 90, + ESTIMATE, ) - ) + ) // Just estimate mBatteryMeterView.onIsBatteryDefenderChanged(false) - assertThat(mBatteryMeterView.contentDescription).isEqualTo( + assertThat(mBatteryMeterView.contentDescription) + .isEqualTo( context.getString( - R.string.accessibility_battery_level_with_estimate, - 90, - ESTIMATE, + R.string.accessibility_battery_level_with_estimate, + 90, + ESTIMATE, ) - ) + ) // Just percent mBatteryMeterView.setPercentShowMode(BatteryMeterView.MODE_ON) - assertThat(mBatteryMeterView.contentDescription).isEqualTo( - context.getString(R.string.accessibility_battery_level, 90) - ) + assertThat(mBatteryMeterView.contentDescription) + .isEqualTo(context.getString(R.string.accessibility_battery_level, 90)) // Charging mBatteryMeterView.onBatteryLevelChanged(90, true) - assertThat(mBatteryMeterView.contentDescription).isEqualTo( - context.getString(R.string.accessibility_battery_level_charging, 90) - ) + assertThat(mBatteryMeterView.contentDescription) + .isEqualTo(context.getString(R.string.accessibility_battery_level_charging, 90)) } @Test @DisableFlags(FLAG_NEW_STATUS_BAR_ICONS) fun isBatteryDefenderChanged_true_drawableGetsTrue_flagOff() { - mBatteryMeterView.setDisplayShieldEnabled(true) val drawable = getBatteryDrawable() mBatteryMeterView.onIsBatteryDefenderChanged(true) @@ -278,8 +267,6 @@ class BatteryMeterViewTest : SysuiTestCase() { @Test @EnableFlags(FLAG_NEW_STATUS_BAR_ICONS) fun isBatteryDefenderChanged_true_drawableGetsTrue_flagOn() { - mBatteryMeterView.setDisplayShieldEnabled(true) - mBatteryMeterView.onIsBatteryDefenderChanged(true) assertThat(mBatteryMeterView.unifiedBatteryState.attribution).isNotNull() @@ -288,7 +275,6 @@ class BatteryMeterViewTest : SysuiTestCase() { @Test @DisableFlags(FLAG_NEW_STATUS_BAR_ICONS) fun isBatteryDefenderChanged_false_drawableGetsFalse_flagOff() { - mBatteryMeterView.setDisplayShieldEnabled(true) val drawable = getBatteryDrawable() // Start as true @@ -303,8 +289,6 @@ class BatteryMeterViewTest : SysuiTestCase() { @Test @EnableFlags(FLAG_NEW_STATUS_BAR_ICONS) fun isBatteryDefenderChanged_false_drawableGetsFalse_flagOn() { - mBatteryMeterView.setDisplayShieldEnabled(true) - // Start as true mBatteryMeterView.onIsBatteryDefenderChanged(true) @@ -316,27 +300,6 @@ class BatteryMeterViewTest : SysuiTestCase() { @Test @DisableFlags(FLAG_NEW_STATUS_BAR_ICONS) - fun isBatteryDefenderChanged_true_featureflagOff_drawableGetsFalse_flagOff() { - mBatteryMeterView.setDisplayShieldEnabled(false) - val drawable = getBatteryDrawable() - - mBatteryMeterView.onIsBatteryDefenderChanged(true) - - assertThat(drawable.displayShield).isFalse() - } - - @Test - @EnableFlags(FLAG_NEW_STATUS_BAR_ICONS) - fun isBatteryDefenderChanged_true_featureflagOff_drawableGetsFalse_flagOn() { - mBatteryMeterView.setDisplayShieldEnabled(false) - - mBatteryMeterView.onIsBatteryDefenderChanged(true) - - assertThat(mBatteryMeterView.unifiedBatteryState.attribution).isNull() - } - - @Test - @DisableFlags(FLAG_NEW_STATUS_BAR_ICONS) fun isIncompatibleChargingChanged_true_drawableGetsChargingFalse_flagOff() { mBatteryMeterView.onBatteryLevelChanged(45, true) val drawable = getBatteryDrawable() @@ -381,8 +344,8 @@ class BatteryMeterViewTest : SysuiTestCase() { } private fun getBatteryDrawable(): AccessorizedBatteryDrawable { - return (mBatteryMeterView.getChildAt(0) as ImageView) - .drawable as AccessorizedBatteryDrawable + return (mBatteryMeterView.getChildAt(0) as ImageView).drawable + as AccessorizedBatteryDrawable } private class Fetcher : BatteryEstimateFetcher { diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt index 6dc4b10a57da..bbff5392f59c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt @@ -18,6 +18,10 @@ package com.android.systemui.biometrics import android.graphics.Point import android.hardware.biometrics.BiometricSourceType +import android.hardware.biometrics.ComponentInfoInternal +import android.hardware.biometrics.SensorLocationInternal +import android.hardware.biometrics.SensorProperties +import android.hardware.fingerprint.FingerprintSensorProperties import android.hardware.fingerprint.FingerprintSensorPropertiesInternal import android.testing.TestableLooper.RunWithLooper import android.util.DisplayMetrics @@ -43,6 +47,7 @@ import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.leak.RotationUtils import com.android.systemui.util.mockito.any +import javax.inject.Provider import kotlinx.coroutines.ExperimentalCoroutinesApi import org.junit.After import org.junit.Assert.assertFalse @@ -62,8 +67,6 @@ import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations import org.mockito.MockitoSession import org.mockito.quality.Strictness -import javax.inject.Provider - @ExperimentalCoroutinesApi @SmallTest @@ -79,35 +82,28 @@ class AuthRippleControllerTest : SysuiTestCase() { @Mock private lateinit var authController: AuthController @Mock private lateinit var authRippleInteractor: AuthRippleInteractor @Mock private lateinit var keyguardStateController: KeyguardStateController - @Mock - private lateinit var wakefulnessLifecycle: WakefulnessLifecycle - @Mock - private lateinit var notificationShadeWindowController: NotificationShadeWindowController - @Mock - private lateinit var biometricUnlockController: BiometricUnlockController - @Mock - private lateinit var udfpsControllerProvider: Provider<UdfpsController> - @Mock - private lateinit var udfpsController: UdfpsController - @Mock - private lateinit var statusBarStateController: StatusBarStateController - @Mock - private lateinit var lightRevealScrim: LightRevealScrim - @Mock - private lateinit var fpSensorProp: FingerprintSensorPropertiesInternal + @Mock private lateinit var wakefulnessLifecycle: WakefulnessLifecycle + @Mock private lateinit var notificationShadeWindowController: NotificationShadeWindowController + @Mock private lateinit var biometricUnlockController: BiometricUnlockController + @Mock private lateinit var udfpsControllerProvider: Provider<UdfpsController> + @Mock private lateinit var udfpsController: UdfpsController + @Mock private lateinit var statusBarStateController: StatusBarStateController + @Mock private lateinit var lightRevealScrim: LightRevealScrim + @Mock private lateinit var fpSensorProp: FingerprintSensorPropertiesInternal private val facePropertyRepository = FakeFacePropertyRepository() private val displayMetrics = DisplayMetrics() @Captor private lateinit var biometricUnlockListener: - ArgumentCaptor<BiometricUnlockController.BiometricUnlockEventsListener> + ArgumentCaptor<BiometricUnlockController.BiometricUnlockEventsListener> @Before fun setUp() { mSetFlagsRule.disableFlags(Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR) MockitoAnnotations.initMocks(this) - staticMockSession = mockitoSession() + staticMockSession = + mockitoSession() .mockStatic(RotationUtils::class.java) .strictness(Strictness.LENIENT) .startMocking() @@ -116,25 +112,26 @@ class AuthRippleControllerTest : SysuiTestCase() { `when`(authController.udfpsProps).thenReturn(listOf(fpSensorProp)) `when`(udfpsControllerProvider.get()).thenReturn(udfpsController) - controller = AuthRippleController( - context, - authController, - configurationController, - keyguardUpdateMonitor, - keyguardStateController, - wakefulnessLifecycle, - commandRegistry, - notificationShadeWindowController, - udfpsControllerProvider, - statusBarStateController, - displayMetrics, - KeyguardLogger(logcatLogBuffer(AuthRippleController.TAG)), - biometricUnlockController, - lightRevealScrim, - authRippleInteractor, - facePropertyRepository, - rippleView, - ) + controller = + AuthRippleController( + context, + authController, + configurationController, + keyguardUpdateMonitor, + keyguardStateController, + wakefulnessLifecycle, + commandRegistry, + notificationShadeWindowController, + udfpsControllerProvider, + statusBarStateController, + displayMetrics, + KeyguardLogger(logcatLogBuffer(AuthRippleController.TAG)), + biometricUnlockController, + lightRevealScrim, + authRippleInteractor, + facePropertyRepository, + rippleView, + ) controller.init() } @@ -150,13 +147,18 @@ class AuthRippleControllerTest : SysuiTestCase() { `when`(authController.fingerprintSensorLocation).thenReturn(fpsLocation) controller.onViewAttached() `when`(keyguardStateController.isShowing).thenReturn(true) - `when`(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed( - eq(BiometricSourceType.FINGERPRINT))).thenReturn(true) + `when`( + keyguardUpdateMonitor.isUnlockingWithBiometricAllowed( + eq(BiometricSourceType.FINGERPRINT) + ) + ) + .thenReturn(true) // WHEN fingerprint authenticated verify(biometricUnlockController).addListener(biometricUnlockListener.capture()) - biometricUnlockListener.value - .onBiometricUnlockedWithKeyguardDismissal(BiometricSourceType.FINGERPRINT) + biometricUnlockListener.value.onBiometricUnlockedWithKeyguardDismissal( + BiometricSourceType.FINGERPRINT + ) // THEN update sensor location and show ripple verify(rippleView).setFingerprintSensorLocation(fpsLocation, 0f) @@ -169,8 +171,12 @@ class AuthRippleControllerTest : SysuiTestCase() { val fpsLocation = Point(5, 5) `when`(authController.udfpsLocation).thenReturn(fpsLocation) controller.onViewAttached() - `when`(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed( - eq(BiometricSourceType.FINGERPRINT))).thenReturn(true) + `when`( + keyguardUpdateMonitor.isUnlockingWithBiometricAllowed( + eq(BiometricSourceType.FINGERPRINT) + ) + ) + .thenReturn(true) // WHEN keyguard is NOT showing & fingerprint authenticated `when`(keyguardStateController.isShowing).thenReturn(false) @@ -179,7 +185,8 @@ class AuthRippleControllerTest : SysuiTestCase() { captor.value.onBiometricAuthenticated( 0 /* userId */, BiometricSourceType.FINGERPRINT /* type */, - false /* isStrongBiometric */) + false /* isStrongBiometric */ + ) // THEN no ripple verify(rippleView, never()).startUnlockedRipple(any()) @@ -194,14 +201,19 @@ class AuthRippleControllerTest : SysuiTestCase() { `when`(keyguardStateController.isShowing).thenReturn(true) // WHEN unlocking with fingerprint is NOT allowed & fingerprint authenticated - `when`(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed( - eq(BiometricSourceType.FINGERPRINT))).thenReturn(false) + `when`( + keyguardUpdateMonitor.isUnlockingWithBiometricAllowed( + eq(BiometricSourceType.FINGERPRINT) + ) + ) + .thenReturn(false) val captor = ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback::class.java) verify(keyguardUpdateMonitor).registerCallback(captor.capture()) captor.value.onBiometricAuthenticated( 0 /* userId */, BiometricSourceType.FINGERPRINT /* type */, - false /* isStrongBiometric */) + false /* isStrongBiometric */ + ) // THEN no ripple verify(rippleView, never()).startUnlockedRipple(any()) @@ -218,7 +230,8 @@ class AuthRippleControllerTest : SysuiTestCase() { captor.value.onBiometricAuthenticated( 0 /* userId */, BiometricSourceType.FACE /* type */, - false /* isStrongBiometric */) + false /* isStrongBiometric */ + ) verify(rippleView, never()).startUnlockedRipple(any()) } @@ -233,18 +246,17 @@ class AuthRippleControllerTest : SysuiTestCase() { captor.value.onBiometricAuthenticated( 0 /* userId */, BiometricSourceType.FINGERPRINT /* type */, - false /* isStrongBiometric */) + false /* isStrongBiometric */ + ) verify(rippleView, never()).startUnlockedRipple(any()) } @Test fun registersAndDeregisters() { controller.onViewAttached() - val captor = ArgumentCaptor - .forClass(KeyguardStateController.Callback::class.java) + val captor = ArgumentCaptor.forClass(KeyguardStateController.Callback::class.java) verify(keyguardStateController).addCallback(captor.capture()) - val captor2 = ArgumentCaptor - .forClass(WakefulnessLifecycle.Observer::class.java) + val captor2 = ArgumentCaptor.forClass(WakefulnessLifecycle.Observer::class.java) verify(wakefulnessLifecycle).addObserver(captor2.capture()) controller.onViewDetached() verify(keyguardStateController).removeCallback(any()) @@ -259,17 +271,25 @@ class AuthRippleControllerTest : SysuiTestCase() { `when`(authController.fingerprintSensorLocation).thenReturn(fpsLocation) controller.onViewAttached() `when`(keyguardStateController.isShowing).thenReturn(true) - `when`(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed( - BiometricSourceType.FINGERPRINT)).thenReturn(true) + `when`( + keyguardUpdateMonitor.isUnlockingWithBiometricAllowed( + BiometricSourceType.FINGERPRINT + ) + ) + .thenReturn(true) `when`(biometricUnlockController.isWakeAndUnlock).thenReturn(true) controller.showUnlockRipple(BiometricSourceType.FINGERPRINT) - assertTrue("reveal didn't start on keyguardFadingAway", - controller.startLightRevealScrimOnKeyguardFadingAway) + assertTrue( + "reveal didn't start on keyguardFadingAway", + controller.startLightRevealScrimOnKeyguardFadingAway + ) `when`(keyguardStateController.isKeyguardFadingAway).thenReturn(true) controller.onKeyguardFadingAwayChanged() - assertFalse("reveal triggers multiple times", - controller.startLightRevealScrimOnKeyguardFadingAway) + assertFalse( + "reveal triggers multiple times", + controller.startLightRevealScrimOnKeyguardFadingAway + ) } @Test @@ -282,23 +302,27 @@ class AuthRippleControllerTest : SysuiTestCase() { `when`(keyguardStateController.isShowing).thenReturn(true) `when`(biometricUnlockController.isWakeAndUnlock).thenReturn(true) `when`(authController.isUdfpsFingerDown).thenReturn(true) - `when`(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed( - eq(BiometricSourceType.FACE))).thenReturn(true) + `when`(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(eq(BiometricSourceType.FACE))) + .thenReturn(true) controller.showUnlockRipple(BiometricSourceType.FACE) - assertTrue("reveal didn't start on keyguardFadingAway", - controller.startLightRevealScrimOnKeyguardFadingAway) + assertTrue( + "reveal didn't start on keyguardFadingAway", + controller.startLightRevealScrimOnKeyguardFadingAway + ) `when`(keyguardStateController.isKeyguardFadingAway).thenReturn(true) controller.onKeyguardFadingAwayChanged() - assertFalse("reveal triggers multiple times", - controller.startLightRevealScrimOnKeyguardFadingAway) + assertFalse( + "reveal triggers multiple times", + controller.startLightRevealScrimOnKeyguardFadingAway + ) } @Test fun testUpdateRippleColor() { controller.onViewAttached() - val captor = ArgumentCaptor - .forClass(ConfigurationController.ConfigurationListener::class.java) + val captor = + ArgumentCaptor.forClass(ConfigurationController.ConfigurationListener::class.java) verify(configurationController).addCallback(captor.capture()) reset(rippleView) @@ -333,6 +357,40 @@ class AuthRippleControllerTest : SysuiTestCase() { } @Test + fun testUltrasonicUdfps_onFingerDown_runningForDeviceEntry_doNotShowDwellRipple() { + // GIVEN UDFPS is ultrasonic + `when`(authController.udfpsProps) + .thenReturn( + listOf( + FingerprintSensorPropertiesInternal( + 0 /* sensorId */, + SensorProperties.STRENGTH_STRONG, + 5 /* maxEnrollmentsPerUser */, + listOf<ComponentInfoInternal>(), + FingerprintSensorProperties.TYPE_UDFPS_ULTRASONIC, + false /* halControlsIllumination */, + true /* resetLockoutRequiresHardwareAuthToken */, + listOf<SensorLocationInternal>(SensorLocationInternal.DEFAULT), + ) + ) + ) + + // GIVEN fingerprint detection is running on keyguard + `when`(keyguardUpdateMonitor.isFingerprintDetectionRunning).thenReturn(true) + + // GIVEN view is already attached + controller.onViewAttached() + val captor = ArgumentCaptor.forClass(UdfpsController.Callback::class.java) + verify(udfpsController).addCallback(captor.capture()) + + // WHEN finger is down + captor.value.onFingerDown() + + // THEN never show dwell ripple + verify(rippleView, never()).startDwellRipple(false) + } + + @Test fun testUdfps_onFingerDown_notDeviceEntry_doesNotShowDwellRipple() { // GIVEN fingerprint detection is NOT running on keyguard `when`(keyguardUpdateMonitor.isFingerprintDetectionRunning).thenReturn(false) diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt index 677d1fdaa01d..6dcea144f2a3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt @@ -449,150 +449,214 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - fun shows_authenticated_no_errors_no_confirmation_required() = runGenericTest { - if (!testCase.confirmationRequested) { - val iconAsset by collectLastValue(kosmos.promptViewModel.iconViewModel.iconAsset) - val iconOverlayAsset by - collectLastValue(kosmos.promptViewModel.iconViewModel.iconOverlayAsset) - val iconContentDescriptionId by - collectLastValue(kosmos.promptViewModel.iconViewModel.contentDescriptionId) - val shouldAnimateIconView by - collectLastValue(kosmos.promptViewModel.iconViewModel.shouldAnimateIconView) - val shouldAnimateIconOverlay by - collectLastValue(kosmos.promptViewModel.iconViewModel.shouldAnimateIconOverlay) - verifyIconSize() + fun shows_error_to_unlock_or_success() { + // Face-only auth does not use error -> unlock or error -> success assets + if (testCase.isFingerprintOnly || testCase.isCoex) { + runGenericTest { + // Distinct asset for error -> success only applicable for fingerprint-only / + // explicit co-ex auth + val iconAsset by collectLastValue(kosmos.promptViewModel.iconViewModel.iconAsset) + val iconContentDescriptionId by + collectLastValue(kosmos.promptViewModel.iconViewModel.contentDescriptionId) + val shouldAnimateIconView by + collectLastValue(kosmos.promptViewModel.iconViewModel.shouldAnimateIconView) + + var forceExplicitFlow = + testCase.isCoex && testCase.confirmationRequested || + testCase.authenticatedByFingerprint + if (forceExplicitFlow) { + kosmos.promptViewModel.ensureFingerprintHasStarted(isDelayed = true) + } + verifyIconSize(forceExplicitFlow) - kosmos.promptViewModel.showAuthenticated( - modality = testCase.authenticatedModality, - dismissAfterDelay = DELAY - ) + kosmos.promptViewModel.ensureFingerprintHasStarted(isDelayed = true) + kosmos.promptViewModel.iconViewModel.setPreviousIconWasError(true) - if (testCase.isFingerprintOnly) { - // Fingerprint icon asset assertions - if (testCase.sensorType == FingerprintSensorProperties.TYPE_POWER_BUTTON) { - assertThat(iconAsset).isEqualTo(getSfpsBaseIconAsset()) - assertThat(iconOverlayAsset) - .isEqualTo(R.raw.biometricprompt_symbol_fingerprint_to_success_landscape) - assertThat(iconContentDescriptionId) - .isEqualTo(R.string.security_settings_sfps_enroll_find_sensor_message) - assertThat(shouldAnimateIconView).isEqualTo(true) - assertThat(shouldAnimateIconOverlay).isEqualTo(true) - } else { - assertThat(iconAsset) - .isEqualTo(R.raw.fingerprint_dialogue_fingerprint_to_success_lottie) + kosmos.promptViewModel.showAuthenticated( + modality = testCase.authenticatedModality, + dismissAfterDelay = DELAY + ) + + // TODO(b/350121748): SFPS test cases to be added after SFPS assets update + if (testCase.sensorType != FingerprintSensorProperties.TYPE_POWER_BUTTON) { + // Non-SFPS (UDFPS / rear-FPS) test cases + // Covers (1) fingerprint-only (2) co-ex, authenticated by fingerprint + if (testCase.authenticatedByFingerprint) { + assertThat(iconAsset) + .isEqualTo(R.raw.fingerprint_dialogue_error_to_success_lottie) + assertThat(iconContentDescriptionId) + .isEqualTo(R.string.fingerprint_dialog_touch_sensor) + assertThat(shouldAnimateIconView).isEqualTo(true) + } else { // co-ex, authenticated by face + assertThat(iconAsset) + .isEqualTo(R.raw.fingerprint_dialogue_error_to_unlock_lottie) + assertThat(iconContentDescriptionId) + .isEqualTo(R.string.fingerprint_dialog_authenticated_confirmation) + assertThat(shouldAnimateIconView).isEqualTo(true) + + // Confirm authentication + kosmos.promptViewModel.confirmAuthenticated() + + assertThat(iconAsset) + .isEqualTo( + R.raw.fingerprint_dialogue_unlocked_to_checkmark_success_lottie + ) + assertThat(iconContentDescriptionId) + .isEqualTo(R.string.fingerprint_dialog_touch_sensor) + assertThat(shouldAnimateIconView).isEqualTo(true) + } + } + } + } + } + + @Test + fun shows_authenticated_no_errors_no_confirmation_required() { + if (!testCase.confirmationRequested) { + runGenericTest { + val iconAsset by collectLastValue(kosmos.promptViewModel.iconViewModel.iconAsset) + val iconOverlayAsset by + collectLastValue(kosmos.promptViewModel.iconViewModel.iconOverlayAsset) + val iconContentDescriptionId by + collectLastValue(kosmos.promptViewModel.iconViewModel.contentDescriptionId) + val shouldAnimateIconView by + collectLastValue(kosmos.promptViewModel.iconViewModel.shouldAnimateIconView) + val shouldAnimateIconOverlay by + collectLastValue(kosmos.promptViewModel.iconViewModel.shouldAnimateIconOverlay) + verifyIconSize() + + kosmos.promptViewModel.showAuthenticated( + modality = testCase.authenticatedModality, + dismissAfterDelay = DELAY + ) + + if (testCase.isFingerprintOnly) { + // Fingerprint icon asset assertions + if (testCase.sensorType == FingerprintSensorProperties.TYPE_POWER_BUTTON) { + assertThat(iconAsset).isEqualTo(getSfpsBaseIconAsset()) + assertThat(iconOverlayAsset) + .isEqualTo( + R.raw.biometricprompt_symbol_fingerprint_to_success_landscape + ) + assertThat(iconContentDescriptionId) + .isEqualTo(R.string.security_settings_sfps_enroll_find_sensor_message) + assertThat(shouldAnimateIconView).isEqualTo(true) + assertThat(shouldAnimateIconOverlay).isEqualTo(true) + } else { + assertThat(iconAsset) + .isEqualTo(R.raw.fingerprint_dialogue_fingerprint_to_success_lottie) + assertThat(iconOverlayAsset).isEqualTo(-1) + assertThat(iconContentDescriptionId) + .isEqualTo(R.string.fingerprint_dialog_touch_sensor) + assertThat(shouldAnimateIconView).isEqualTo(true) + assertThat(shouldAnimateIconOverlay).isEqualTo(false) + } + } else if (testCase.isFaceOnly || testCase.isCoex) { + // Face icon asset assertions + // If co-ex, use implicit flow (explicit flow always requires confirmation) + assertThat(iconAsset).isEqualTo(R.raw.face_dialog_dark_to_checkmark) assertThat(iconOverlayAsset).isEqualTo(-1) assertThat(iconContentDescriptionId) - .isEqualTo(R.string.fingerprint_dialog_touch_sensor) + .isEqualTo(R.string.biometric_dialog_face_icon_description_authenticated) assertThat(shouldAnimateIconView).isEqualTo(true) assertThat(shouldAnimateIconOverlay).isEqualTo(false) } - } else if (testCase.isFaceOnly || testCase.isCoex) { - // Face icon asset assertions - // If co-ex, use implicit flow (explicit flow always requires confirmation) - assertThat(iconAsset).isEqualTo(R.raw.face_dialog_dark_to_checkmark) - assertThat(iconOverlayAsset).isEqualTo(-1) - assertThat(iconContentDescriptionId) - .isEqualTo(R.string.biometric_dialog_face_icon_description_authenticated) - assertThat(shouldAnimateIconView).isEqualTo(true) - assertThat(shouldAnimateIconOverlay).isEqualTo(false) } } } @Test - fun shows_pending_confirmation() = runGenericTest { - if ( - (testCase.isFaceOnly || testCase.isCoex) && - testCase.authenticatedByFace && - testCase.confirmationRequested - ) { - val iconAsset by collectLastValue(kosmos.promptViewModel.iconViewModel.iconAsset) - val iconOverlayAsset by - collectLastValue(kosmos.promptViewModel.iconViewModel.iconOverlayAsset) - val iconContentDescriptionId by - collectLastValue(kosmos.promptViewModel.iconViewModel.contentDescriptionId) - val shouldAnimateIconView by - collectLastValue(kosmos.promptViewModel.iconViewModel.shouldAnimateIconView) - val shouldAnimateIconOverlay by - collectLastValue(kosmos.promptViewModel.iconViewModel.shouldAnimateIconOverlay) - - val forceExplicitFlow = testCase.isCoex && testCase.confirmationRequested - verifyIconSize(forceExplicitFlow = forceExplicitFlow) - - kosmos.promptViewModel.showAuthenticated( - modality = testCase.authenticatedModality, - dismissAfterDelay = DELAY - ) + fun shows_pending_confirmation() { + if (testCase.authenticatedByFace && testCase.confirmationRequested) { + runGenericTest { + val iconAsset by collectLastValue(kosmos.promptViewModel.iconViewModel.iconAsset) + val iconOverlayAsset by + collectLastValue(kosmos.promptViewModel.iconViewModel.iconOverlayAsset) + val iconContentDescriptionId by + collectLastValue(kosmos.promptViewModel.iconViewModel.contentDescriptionId) + val shouldAnimateIconView by + collectLastValue(kosmos.promptViewModel.iconViewModel.shouldAnimateIconView) + val shouldAnimateIconOverlay by + collectLastValue(kosmos.promptViewModel.iconViewModel.shouldAnimateIconOverlay) + + val forceExplicitFlow = testCase.isCoex && testCase.confirmationRequested + verifyIconSize(forceExplicitFlow = forceExplicitFlow) + + kosmos.promptViewModel.showAuthenticated( + modality = testCase.authenticatedModality, + dismissAfterDelay = DELAY + ) - if (testCase.isFaceOnly) { - assertThat(iconAsset).isEqualTo(R.raw.face_dialog_wink_from_dark) - assertThat(iconOverlayAsset).isEqualTo(-1) - assertThat(iconContentDescriptionId) - .isEqualTo(R.string.biometric_dialog_face_icon_description_authenticated) - assertThat(shouldAnimateIconView).isEqualTo(true) - assertThat(shouldAnimateIconOverlay).isEqualTo(false) - } else if (testCase.isCoex) { // explicit flow, confirmation requested - // TODO: Update when SFPS co-ex is implemented - if (testCase.sensorType != FingerprintSensorProperties.TYPE_POWER_BUTTON) { - assertThat(iconAsset) - .isEqualTo(R.raw.fingerprint_dialogue_fingerprint_to_unlock_lottie) + if (testCase.isFaceOnly) { + assertThat(iconAsset).isEqualTo(R.raw.face_dialog_wink_from_dark) assertThat(iconOverlayAsset).isEqualTo(-1) assertThat(iconContentDescriptionId) - .isEqualTo(R.string.fingerprint_dialog_authenticated_confirmation) + .isEqualTo(R.string.biometric_dialog_face_icon_description_authenticated) assertThat(shouldAnimateIconView).isEqualTo(true) assertThat(shouldAnimateIconOverlay).isEqualTo(false) + } else if (testCase.isCoex) { // explicit flow, confirmation requested + // TODO: Update when SFPS co-ex is implemented + if (testCase.sensorType != FingerprintSensorProperties.TYPE_POWER_BUTTON) { + assertThat(iconAsset) + .isEqualTo(R.raw.fingerprint_dialogue_fingerprint_to_unlock_lottie) + assertThat(iconOverlayAsset).isEqualTo(-1) + assertThat(iconContentDescriptionId) + .isEqualTo(R.string.fingerprint_dialog_authenticated_confirmation) + assertThat(shouldAnimateIconView).isEqualTo(true) + assertThat(shouldAnimateIconOverlay).isEqualTo(false) + } } } } } @Test - fun shows_authenticated_explicitly_confirmed_iconUpdate() = runGenericTest { - if ( - (testCase.isFaceOnly || testCase.isCoex) && - testCase.authenticatedByFace && - testCase.confirmationRequested - ) { - val iconAsset by collectLastValue(kosmos.promptViewModel.iconViewModel.iconAsset) - val iconOverlayAsset by - collectLastValue(kosmos.promptViewModel.iconViewModel.iconOverlayAsset) - val iconContentDescriptionId by - collectLastValue(kosmos.promptViewModel.iconViewModel.contentDescriptionId) - val shouldAnimateIconView by - collectLastValue(kosmos.promptViewModel.iconViewModel.shouldAnimateIconView) - val shouldAnimateIconOverlay by - collectLastValue(kosmos.promptViewModel.iconViewModel.shouldAnimateIconOverlay) - val forceExplicitFlow = testCase.isCoex && testCase.confirmationRequested - verifyIconSize(forceExplicitFlow = forceExplicitFlow) - - kosmos.promptViewModel.showAuthenticated( - modality = testCase.authenticatedModality, - dismissAfterDelay = DELAY - ) - - kosmos.promptViewModel.confirmAuthenticated() + fun shows_authenticated_explicitly_confirmed() { + if (testCase.authenticatedByFace && testCase.confirmationRequested) { + runGenericTest { + val iconAsset by collectLastValue(kosmos.promptViewModel.iconViewModel.iconAsset) + val iconOverlayAsset by + collectLastValue(kosmos.promptViewModel.iconViewModel.iconOverlayAsset) + val iconContentDescriptionId by + collectLastValue(kosmos.promptViewModel.iconViewModel.contentDescriptionId) + val shouldAnimateIconView by + collectLastValue(kosmos.promptViewModel.iconViewModel.shouldAnimateIconView) + val shouldAnimateIconOverlay by + collectLastValue(kosmos.promptViewModel.iconViewModel.shouldAnimateIconOverlay) + val forceExplicitFlow = testCase.isCoex && testCase.confirmationRequested + verifyIconSize(forceExplicitFlow = forceExplicitFlow) + + kosmos.promptViewModel.showAuthenticated( + modality = testCase.authenticatedModality, + dismissAfterDelay = DELAY + ) - if (testCase.isFaceOnly) { - assertThat(iconAsset).isEqualTo(R.raw.face_dialog_dark_to_checkmark) - assertThat(iconOverlayAsset).isEqualTo(-1) - assertThat(iconContentDescriptionId) - .isEqualTo(R.string.biometric_dialog_face_icon_description_confirmed) - assertThat(shouldAnimateIconView).isEqualTo(true) - assertThat(shouldAnimateIconOverlay).isEqualTo(false) - } + kosmos.promptViewModel.confirmAuthenticated() - // explicit flow because confirmation requested - if (testCase.isCoex) { - // TODO: Update when SFPS co-ex is implemented - if (testCase.sensorType != FingerprintSensorProperties.TYPE_POWER_BUTTON) { - assertThat(iconAsset) - .isEqualTo(R.raw.fingerprint_dialogue_unlocked_to_checkmark_success_lottie) + if (testCase.isFaceOnly) { + assertThat(iconAsset).isEqualTo(R.raw.face_dialog_dark_to_checkmark) assertThat(iconOverlayAsset).isEqualTo(-1) assertThat(iconContentDescriptionId) - .isEqualTo(R.string.fingerprint_dialog_touch_sensor) + .isEqualTo(R.string.biometric_dialog_face_icon_description_confirmed) assertThat(shouldAnimateIconView).isEqualTo(true) assertThat(shouldAnimateIconOverlay).isEqualTo(false) } + + // explicit flow because confirmation requested + if (testCase.isCoex) { + // TODO: Update when SFPS co-ex is implemented + if (testCase.sensorType != FingerprintSensorProperties.TYPE_POWER_BUTTON) { + assertThat(iconAsset) + .isEqualTo( + R.raw.fingerprint_dialogue_unlocked_to_checkmark_success_lottie + ) + assertThat(iconOverlayAsset).isEqualTo(-1) + assertThat(iconContentDescriptionId) + .isEqualTo(R.string.fingerprint_dialog_touch_sensor) + assertThat(shouldAnimateIconView).isEqualTo(true) + assertThat(shouldAnimateIconOverlay).isEqualTo(false) + } + } } } } @@ -700,58 +764,68 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - fun sfpsIconUpdates_onFoldConfigurationChanged() = runGenericTest { + fun sfpsIconUpdates_onFoldConfigurationChanged() { if ( testCase.sensorType == FingerprintSensorProperties.TYPE_POWER_BUTTON && !testCase.isInRearDisplayMode ) { - val currentIcon by collectLastValue(kosmos.promptViewModel.iconViewModel.iconAsset) + runGenericTest { + val currentIcon by collectLastValue(kosmos.promptViewModel.iconViewModel.iconAsset) - kosmos.promptViewModel.iconViewModel.onConfigurationChanged(getFoldedConfiguration()) - val foldedIcon = currentIcon + kosmos.promptViewModel.iconViewModel.onConfigurationChanged( + getFoldedConfiguration() + ) + val foldedIcon = currentIcon - kosmos.promptViewModel.iconViewModel.onConfigurationChanged(getUnfoldedConfiguration()) - val unfoldedIcon = currentIcon + kosmos.promptViewModel.iconViewModel.onConfigurationChanged( + getUnfoldedConfiguration() + ) + val unfoldedIcon = currentIcon - assertThat(foldedIcon).isNotEqualTo(unfoldedIcon) + assertThat(foldedIcon).isNotEqualTo(unfoldedIcon) + } } } @Test - fun sfpsIconUpdates_onRotation() = runGenericTest { + fun sfpsIconUpdates_onRotation() { if (testCase.sensorType == FingerprintSensorProperties.TYPE_POWER_BUTTON) { - val currentIcon by collectLastValue(kosmos.promptViewModel.iconViewModel.iconAsset) + runGenericTest { + val currentIcon by collectLastValue(kosmos.promptViewModel.iconViewModel.iconAsset) - kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_0) - val iconRotation0 = currentIcon + kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_0) + val iconRotation0 = currentIcon - kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_90) - val iconRotation90 = currentIcon + kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_90) + val iconRotation90 = currentIcon - kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_180) - val iconRotation180 = currentIcon + kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_180) + val iconRotation180 = currentIcon - kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_270) - val iconRotation270 = currentIcon + kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_270) + val iconRotation270 = currentIcon - assertThat(iconRotation0).isEqualTo(iconRotation180) - assertThat(iconRotation0).isNotEqualTo(iconRotation90) - assertThat(iconRotation0).isNotEqualTo(iconRotation270) + assertThat(iconRotation0).isEqualTo(iconRotation180) + assertThat(iconRotation0).isNotEqualTo(iconRotation90) + assertThat(iconRotation0).isNotEqualTo(iconRotation270) + } } } @Test - fun sfpsIconUpdates_onRearDisplayMode() = runGenericTest { + fun sfpsIconUpdates_onRearDisplayMode() { if (testCase.sensorType == FingerprintSensorProperties.TYPE_POWER_BUTTON) { - val currentIcon by collectLastValue(kosmos.promptViewModel.iconViewModel.iconAsset) + runGenericTest { + val currentIcon by collectLastValue(kosmos.promptViewModel.iconViewModel.iconAsset) - kosmos.displayStateRepository.setIsInRearDisplayMode(false) - val iconNotRearDisplayMode = currentIcon + kosmos.displayStateRepository.setIsInRearDisplayMode(false) + val iconNotRearDisplayMode = currentIcon - kosmos.displayStateRepository.setIsInRearDisplayMode(true) - val iconRearDisplayMode = currentIcon + kosmos.displayStateRepository.setIsInRearDisplayMode(true) + val iconRearDisplayMode = currentIcon - assertThat(iconNotRearDisplayMode).isNotEqualTo(iconRearDisplayMode) + assertThat(iconNotRearDisplayMode).isNotEqualTo(iconRearDisplayMode) + } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/DetailDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/DetailDialogTest.kt index 10b3ce31a895..0489d815b074 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/DetailDialogTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/DetailDialogTest.kt @@ -89,7 +89,8 @@ class DetailDialogTest : SysuiTestCase() { verify(taskView).startActivity(any(), any(), capture(optionsCaptor), any()) assertThat(optionsCaptor.value.pendingIntentBackgroundActivityStartMode) - .isEqualTo(ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED) + .isAnyOf(ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED, + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS) assertThat(optionsCaptor.value.isPendingIntentBackgroundActivityLaunchAllowedByPermission) .isTrue() assertThat(optionsCaptor.value.taskAlwaysOnTop).isTrue() diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt index 693a87761b27..7cc91853a749 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt @@ -18,6 +18,7 @@ package com.android.systemui.keyguard.ui.view.layout.sections import android.graphics.Point +import android.platform.test.annotations.DisableFlags import android.view.WindowManager import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet @@ -106,8 +107,8 @@ class DefaultDeviceEntrySectionTest : SysuiTestCase() { } @Test + @DisableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) fun addViewsConditionally_migrateFlagOff() { - mSetFlagsRule.disableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) mSetFlagsRule.disableFlags(AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR) mSetFlagsRule.disableFlags(AConfigFlags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR) val constraintLayout = ConstraintLayout(context, null) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSectionTest.kt index 201ee88cdd80..1c99eff0d328 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSectionTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSectionTest.kt @@ -17,6 +17,7 @@ package com.android.systemui.keyguard.ui.view.layout.sections +import android.platform.test.annotations.EnableFlags import android.view.View import android.widget.LinearLayout import androidx.constraintlayout.widget.ConstraintLayout @@ -48,6 +49,7 @@ import org.mockito.MockitoAnnotations @RunWith(AndroidJUnit4::class) @SmallTest +@EnableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) class SmartspaceSectionTest : SysuiTestCase() { private lateinit var underTest: SmartspaceSection @Mock private lateinit var keyguardClockViewModel: KeyguardClockViewModel @@ -70,7 +72,6 @@ class SmartspaceSectionTest : SysuiTestCase() { @Before fun setup() { MockitoAnnotations.initMocks(this) - mSetFlagsRule.enableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) underTest = SmartspaceSection( mContext, diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt index 4f4aac454912..3b96be48b2cc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt @@ -152,7 +152,6 @@ class KeyguardBottomAreaViewModelTest(flags: FlagsParameterization) : SysuiTestC val featureFlags = FakeFeatureFlags().apply { set(Flags.LOCK_SCREEN_LONG_PRESS_ENABLED, false) - set(Flags.LOCK_SCREEN_LONG_PRESS_DIRECT_TO_WPP, false) } val withDeps = KeyguardInteractorFactory.create(featureFlags = featureFlags) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt index 9fb1aa795c3c..e89abf6fc5a1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt @@ -195,7 +195,6 @@ class KeyguardQuickAffordancesCombinedViewModelTest : SysuiTestCase() { val featureFlags = FakeFeatureFlags().apply { set(Flags.LOCK_SCREEN_LONG_PRESS_ENABLED, false) - set(Flags.LOCK_SCREEN_LONG_PRESS_DIRECT_TO_WPP, false) } val withDeps = KeyguardInteractorFactory.create(featureFlags = featureFlags) diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java index e46416c1b321..ebab04989590 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java @@ -75,6 +75,7 @@ import com.android.systemui.qs.QsEventLoggerFake; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.statusbar.StatusBarState; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -146,6 +147,12 @@ public class QSTileImplTest extends SysuiTestCase { mTile.setTileSpec(SPEC); } + @After + public void destroyTile() { + mTile.destroy(); + mTestableLooper.processAllMessages(); + } + @Test public void testClick_Metrics() { mTile.click(null /* expandable */); diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ModesTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ModesTileTest.kt index 4c77fb84d8ce..27b6ea61a922 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ModesTileTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ModesTileTest.kt @@ -27,6 +27,7 @@ import androidx.test.filters.SmallTest import com.android.internal.logging.MetricsLogger import com.android.settingslib.notification.data.repository.FakeZenModeRepository import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.classifier.FalsingManagerFake import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.statusbar.StatusBarStateController @@ -41,10 +42,12 @@ import com.android.systemui.qs.tiles.viewmodel.QSTileConfigProvider import com.android.systemui.qs.tiles.viewmodel.QSTileConfigTestBuilder import com.android.systemui.qs.tiles.viewmodel.QSTileUIConfig import com.android.systemui.res.R +import com.android.systemui.statusbar.policy.ui.dialog.ModesDialogDelegate import com.android.systemui.util.mockito.any import com.android.systemui.util.settings.FakeSettings import com.android.systemui.util.settings.SecureSettings import com.google.common.truth.Truth.assertThat +import kotlin.coroutines.EmptyCoroutineContext import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope @@ -79,6 +82,10 @@ class ModesTileTest : SysuiTestCase() { @Mock private lateinit var qsTileConfigProvider: QSTileConfigProvider + @Mock private lateinit var dialogTransitionAnimator: DialogTransitionAnimator + + @Mock private lateinit var dialogDelegate: ModesDialogDelegate + private val inputHandler = FakeQSTileIntentUserInputHandler() private val zenModeRepository = FakeZenModeRepository() private val tileDataInteractor = ModesTileDataInteractor(zenModeRepository) @@ -122,7 +129,13 @@ class ModesTileTest : SysuiTestCase() { } ) - userActionInteractor = ModesTileUserActionInteractor(inputHandler) + userActionInteractor = + ModesTileUserActionInteractor( + EmptyCoroutineContext, + inputHandler, + dialogTransitionAnimator, + dialogDelegate, + ) underTest = ModesTile( diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/policy/PolicyRequestProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/policy/PolicyRequestProcessorTest.kt index 3b0e194ce223..bab9bbbfde4f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/policy/PolicyRequestProcessorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/policy/PolicyRequestProcessorTest.kt @@ -16,13 +16,13 @@ package com.android.systemui.screenshot.policy import android.content.ComponentName -import androidx.test.ext.junit.runners.AndroidJUnit4 import android.graphics.Insets import android.graphics.Rect import android.os.UserHandle import android.view.Display.DEFAULT_DISPLAY import android.view.WindowManager.ScreenshotSource.SCREENSHOT_KEY_CHORD import android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN +import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.systemui.screenshot.ImageCapture import com.android.systemui.screenshot.ScreenshotData import com.android.systemui.screenshot.data.model.DisplayContentScenarios.ActivityNames.FILES @@ -40,20 +40,23 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class PolicyRequestProcessorTest { - val imageCapture = object : ImageCapture { - override fun captureDisplay(displayId: Int, crop: Rect?) = null - override suspend fun captureTask(taskId: Int) = null - } + val imageCapture = + object : ImageCapture { + override fun captureDisplay(displayId: Int, crop: Rect?) = null + + override suspend fun captureTask(taskId: Int) = null + } /** Tests behavior when no policies are applied */ @Test fun testProcess_defaultOwner_whenNoPolicyApplied() { val fullScreenWork = DisplayContentRepository { - singleFullScreen(TaskSpec(taskId = 1001, name = FILES, userId = WORK)) + singleFullScreen(TaskSpec(taskId = TASK_ID, name = FILES, userId = WORK)) } val request = - ScreenshotData(TAKE_SCREENSHOT_FULLSCREEN, + ScreenshotData( + TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_CHORD, null, topComponent = null, @@ -61,24 +64,34 @@ class PolicyRequestProcessorTest { taskId = -1, insets = Insets.NONE, bitmap = null, - displayId = DEFAULT_DISPLAY) + displayId = DEFAULT_DISPLAY + ) /* Create a policy request processor with no capture policies */ val requestProcessor = - PolicyRequestProcessor(Dispatchers.Unconfined, + PolicyRequestProcessor( + Dispatchers.Unconfined, imageCapture, policies = emptyList(), defaultOwner = UserHandle.of(PERSONAL), defaultComponent = ComponentName("default", "Component"), - displayTasks = fullScreenWork) + displayTasks = fullScreenWork + ) val result = runBlocking { requestProcessor.process(request) } - assertWithMessage( - "With no policy, the screenshot should be assigned to the default user" - ).that(result.userHandle).isEqualTo(UserHandle.of(PERSONAL)) + assertWithMessage("With no policy, the screenshot should be assigned to the default user") + .that(result.userHandle) + .isEqualTo(UserHandle.of(PERSONAL)) + + assertWithMessage("The topComponent of the screenshot") + .that(result.topComponent) + .isEqualTo(ComponentName.unflattenFromString(FILES)) + + assertWithMessage("Task ID").that(result.taskId).isEqualTo(TASK_ID) + } - assertWithMessage("The topComponent of the screenshot").that(result.topComponent) - .isEqualTo(ComponentName.unflattenFromString(FILES)) + companion object { + const val TASK_ID = 1001 } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt index 169511f827ef..86c9ab789429 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt @@ -312,6 +312,59 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { } @Test + fun lifecycle_doesNotResumeOnUserInteractivityOnceExpanded() = + with(kosmos) { + testScope.runTest { + // Communal is open. + goToScene(CommunalScenes.Communal) + + // Shade shows up. + shadeTestUtil.setShadeExpansion(1.0f) + testableLooper.processAllMessages() + underTest.onTouchEvent(DOWN_EVENT) + testableLooper.processAllMessages() + + assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED) + + // Shade starts collapsing. + shadeTestUtil.setShadeExpansion(.5f) + testableLooper.processAllMessages() + underTest.onTouchEvent(DOWN_EVENT) + testableLooper.processAllMessages() + + assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED) + + // Shade fully collpase, and then expand should with touch interaction should now + // be resumed. + shadeTestUtil.setShadeExpansion(0f) + testableLooper.processAllMessages() + shadeTestUtil.setShadeExpansion(.5f) + testableLooper.processAllMessages() + underTest.onTouchEvent(DOWN_EVENT) + testableLooper.processAllMessages() + + assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED) + } + } + + @Test + fun touchHandling_moveEventProcessedAfterCancel() = + with(kosmos) { + testScope.runTest { + // Communal is open. + goToScene(CommunalScenes.Communal) + + // Shade shows up. + shadeTestUtil.setQsExpansion(0.5f) + testableLooper.processAllMessages() + assertThat(underTest.onTouchEvent(DOWN_EVENT)).isTrue() + assertThat(underTest.onTouchEvent(CANCEL_EVENT)).isTrue() + assertThat(underTest.onTouchEvent(UP_EVENT)).isFalse() + assertThat(underTest.onTouchEvent(MOVE_EVENT)).isTrue() + } + } + + @Test fun editMode_communalAvailable() = with(kosmos) { testScope.runTest { @@ -488,5 +541,35 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { CONTAINER_HEIGHT.toFloat() / 2, 0 ) + + private val CANCEL_EVENT = + MotionEvent.obtain( + 0L, + 0L, + MotionEvent.ACTION_CANCEL, + CONTAINER_WIDTH.toFloat() / 2, + CONTAINER_HEIGHT.toFloat() / 2, + 0 + ) + + private val MOVE_EVENT = + MotionEvent.obtain( + 0L, + 0L, + MotionEvent.ACTION_MOVE, + CONTAINER_WIDTH.toFloat() / 2, + CONTAINER_HEIGHT.toFloat() / 2, + 0 + ) + + private val UP_EVENT = + MotionEvent.obtain( + 0L, + 0L, + MotionEvent.ACTION_UP, + CONTAINER_WIDTH.toFloat() / 2, + CONTAINER_HEIGHT.toFloat() / 2, + 0 + ) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java index b80d1a472a72..a5c4bcd46a1f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java @@ -18,7 +18,6 @@ package com.android.systemui.shade; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; -import static com.android.keyguard.KeyguardClockSwitch.LARGE; import static com.android.systemui.log.LogBufferHelperKt.logcatLogBuffer; import static com.google.common.truth.Truth.assertThat; @@ -404,7 +403,6 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { mFeatureFlags.set(Flags.QS_USER_DETAIL_SHORTCUT, false); mSetFlagsRule.disableFlags(com.android.systemui.Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR); - mSetFlagsRule.disableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT); mSetFlagsRule.disableFlags(com.android.systemui.Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR); mMainDispatcher = getMainDispatcher(); @@ -677,8 +675,14 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { mMainHandler = new Handler(Looper.getMainLooper()); + LongPressHandlingView longPressHandlingView = mock(LongPressHandlingView.class); when(mView.requireViewById(R.id.keyguard_long_press)) - .thenReturn(mock(LongPressHandlingView.class)); + .thenReturn(longPressHandlingView); + + Resources longPressHandlingViewRes = mock(Resources.class); + when(longPressHandlingView.getResources()).thenReturn(longPressHandlingViewRes); + when(longPressHandlingViewRes.getString(anyInt())).thenReturn(""); + mHeadsUpNotificationInteractor = new HeadsUpNotificationInteractor(mFakeHeadsUpNotificationRepository, @@ -801,7 +805,6 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { .setHeadsUpAppearanceController(mock(HeadsUpAppearanceController.class)); verify(mNotificationStackScrollLayoutController) .setOnEmptySpaceClickListener(mEmptySpaceClickListenerCaptor.capture()); - verify(mKeyguardStatusViewController).displayClock(LARGE, /* animate */ true); reset(mKeyguardStatusViewController); when(mNotificationPanelViewControllerLazy.get()) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java index 90e8ea5f34c4..905cc4cd13b2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java @@ -92,6 +92,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo * When the Back gesture starts (progress 0%), the scrim will stay at 100% scale (1.0f). */ @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void testBackGesture_min_scrimAtMaxScale() { mNotificationPanelViewController.onBackProgressed(0.0f); verify(mScrimController).applyBackScaling(1.0f); @@ -101,6 +102,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo * When the Back gesture is at max (progress 100%), the scrim will be scaled to its minimum. */ @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void testBackGesture_max_scrimAtMinScale() { mNotificationPanelViewController.onBackProgressed(1.0f); verify(mScrimController).applyBackScaling( @@ -108,6 +110,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void onNotificationHeightChangeWhileOnKeyguardWillComputeMaxKeyguardNotifications() { mStatusBarStateController.setState(KEYGUARD); ArgumentCaptor<OnHeightChangedListener> captor = @@ -124,6 +127,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void onNotificationHeightChangeWhileInShadeWillNotComputeMaxKeyguardNotifications() { mStatusBarStateController.setState(SHADE); ArgumentCaptor<OnHeightChangedListener> captor = @@ -140,6 +144,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void computeMaxKeyguardNotifications_lockscreenToShade_returnsExistingMax() { when(mAmbientState.getFractionToShade()).thenReturn(0.5f); mNotificationPanelViewController.setMaxDisplayedNotifications(-1); @@ -150,6 +155,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void computeMaxKeyguardNotifications_noTransition_updatesMax() { when(mAmbientState.getFractionToShade()).thenReturn(0f); mNotificationPanelViewController.setMaxDisplayedNotifications(-1); @@ -196,6 +202,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void getVerticalSpaceForLockscreenShelf_useLockIconBottomPadding_returnsShelfHeight() { enableSplitShade(/* enabled= */ false); setBottomPadding(/* stackScrollLayoutBottom= */ 100, @@ -213,6 +220,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void getVerticalSpaceForLockscreenShelf_useIndicationBottomPadding_returnsZero() { enableSplitShade(/* enabled= */ false); setBottomPadding(/* stackScrollLayoutBottom= */ 100, @@ -230,6 +238,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void getVerticalSpaceForLockscreenShelf_useAmbientBottomPadding_returnsZero() { enableSplitShade(/* enabled= */ false); setBottomPadding(/* stackScrollLayoutBottom= */ 100, @@ -247,6 +256,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void getVerticalSpaceForLockscreenShelf_useLockIconPadding_returnsLessThanShelfHeight() { enableSplitShade(/* enabled= */ false); setBottomPadding(/* stackScrollLayoutBottom= */ 100, @@ -264,6 +274,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void getVerticalSpaceForLockscreenShelf_splitShade() { enableSplitShade(/* enabled= */ true); setBottomPadding(/* stackScrollLayoutBottom= */ 100, @@ -281,6 +292,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void testSetPanelScrimMinFractionWhenHeadsUpIsDragged() { mNotificationPanelViewController.setHeadsUpDraggingStartingHeight( mNotificationPanelViewController.getMaxPanelHeight() / 2); @@ -288,6 +300,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void testSetDozing_notifiesNsslAndStateController() { mNotificationPanelViewController.setDozing(true /* dozing */, false /* animate */); verify(mNotificationStackScrollLayoutController).setDozing(eq(true), eq(false)); @@ -295,6 +308,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void testOnDozeAmountChanged_positionClockAndNotificationsUsesUdfpsLocation() { // GIVEN UDFPS is enrolled and we're on the keyguard final Point udfpsLocationCenter = new Point(0, 100); @@ -332,12 +346,14 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void testSetExpandedHeight() { mNotificationPanelViewController.setExpandedHeight(200); assertThat((int) mNotificationPanelViewController.getExpandedHeight()).isEqualTo(200); } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void testOnTouchEvent_expansionCanBeBlocked() { onTouchEvent(MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0)); onTouchEvent(MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_MOVE, 0f, 200f, 0)); @@ -350,6 +366,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void test_pulsing_onTouchEvent_noTracking() { // GIVEN device is pulsing mNotificationPanelViewController.setPulsing(true); @@ -367,6 +384,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void alternateBouncerVisible_onTouchEvent_notHandled() { mSetFlagsRule.enableFlags(com.android.systemui.Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR); // GIVEN alternate bouncer is visible @@ -385,6 +403,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void test_onTouchEvent_startTracking() { // GIVEN device is NOT pulsing mNotificationPanelViewController.setPulsing(false); @@ -402,9 +421,8 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void onInterceptTouchEvent_nsslMigrationOff_userActivity() { - mSetFlagsRule.disableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT); - mTouchHandler.onInterceptTouchEvent(MotionEvent.obtain(0L /* downTime */, 0L /* eventTime */, MotionEvent.ACTION_DOWN, 0f /* x */, 0f /* y */, 0 /* metaState */)); @@ -413,9 +431,8 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @EnableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void onInterceptTouchEvent_nsslMigrationOn_userActivity_not_called() { - mSetFlagsRule.enableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT); - mTouchHandler.onInterceptTouchEvent(MotionEvent.obtain(0L /* downTime */, 0L /* eventTime */, MotionEvent.ACTION_DOWN, 0f /* x */, 0f /* y */, 0 /* metaState */)); @@ -424,6 +441,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void testOnTouchEvent_expansionResumesAfterBriefTouch() { mFalsingManager.setIsClassifierEnabled(true); mFalsingManager.setIsFalseTouch(false); @@ -460,6 +478,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void testA11y_initializeNode() { AccessibilityNodeInfo nodeInfo = new AccessibilityNodeInfo(); mAccessibilityDelegate.onInitializeAccessibilityNodeInfo(mView, nodeInfo); @@ -473,6 +492,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void testA11y_scrollForward() { mAccessibilityDelegate.performAccessibilityAction( mView, @@ -483,6 +503,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void testA11y_scrollUp() { mAccessibilityDelegate.performAccessibilityAction( mView, @@ -493,6 +514,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void testKeyguardStatusViewInSplitShade_changesConstraintsDependingOnNotifications() { mStatusBarStateController.setState(KEYGUARD); enableSplitShade(/* enabled= */ true); @@ -511,6 +533,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void keyguardStatusView_splitShade_dozing_alwaysDozingOn_isCentered() { when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(2); when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true); @@ -523,6 +546,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void keyguardStatusView_splitShade_dozing_alwaysDozingOff_isNotCentered() { when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(2); when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true); @@ -535,6 +559,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void keyguardStatusView_splitShade_notDozing_alwaysDozingOn_isNotCentered() { when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(2); when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true); @@ -547,6 +572,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void keyguardStatusView_splitShade_pulsing_isNotCentered() { when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(2); when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true); @@ -560,6 +586,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void keyguardStatusView_splitShade_notPulsing_isNotCentered() { when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(2); when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true); @@ -573,6 +600,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void keyguardStatusView_singleShade_isCentered() { enableSplitShade(/* enabled= */ false); // The conditions below would make the clock NOT be centered on split shade. @@ -587,6 +615,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void keyguardStatusView_willPlayDelayedDoze_isCentered_thenNot() { when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(2); when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true); @@ -602,6 +631,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void keyguardStatusView_willPlayDelayedDoze_notifiesKeyguardMediaController() { when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(2); when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(true); @@ -614,6 +644,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void keyguardStatusView_willPlayDelayedDoze_isCentered_thenStillCenteredIfNoNotifs() { when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(0); when(mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()).thenReturn(false); @@ -629,6 +660,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void onKeyguardStatusViewHeightChange_animatesNextTopPaddingChangeForNSSL() { ArgumentCaptor<View.OnLayoutChangeListener> captor = ArgumentCaptor.forClass(View.OnLayoutChangeListener.class); @@ -646,6 +678,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void testCanCollapsePanelOnTouch_trueForKeyGuard() { mStatusBarStateController.setState(KEYGUARD); @@ -653,6 +686,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void testCanCollapsePanelOnTouch_trueWhenScrolledToBottom() { mStatusBarStateController.setState(SHADE); when(mNotificationStackScrollLayoutController.isScrolledToBottom()).thenReturn(true); @@ -661,6 +695,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void testCanCollapsePanelOnTouch_trueWhenInSettings() { mStatusBarStateController.setState(SHADE); when(mQsController.getExpanded()).thenReturn(true); @@ -669,6 +704,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void testCanCollapsePanelOnTouch_falseInDualPaneShade() { mStatusBarStateController.setState(SHADE); enableSplitShade(/* enabled= */ true); @@ -695,6 +731,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void testCancelSwipeWhileLocked_notifiesKeyguardState() { mStatusBarStateController.setState(KEYGUARD); @@ -707,6 +744,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void testSwipe_exactlyToTarget_notifiesNssl() { // No over-expansion mNotificationPanelViewController.setOverExpansion(0f); @@ -722,6 +760,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void testRotatingToSplitShadeWithQsExpanded_transitionsToShadeLocked() { mStatusBarStateController.setState(KEYGUARD); when(mQsController.getExpanded()).thenReturn(true); @@ -732,6 +771,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void testUnlockedSplitShadeTransitioningToKeyguard_closesQS() { enableSplitShade(true); mStatusBarStateController.setState(SHADE); @@ -741,6 +781,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void testLockedSplitShadeTransitioningToKeyguard_closesQS() { enableSplitShade(true); mStatusBarStateController.setState(SHADE_LOCKED); @@ -750,6 +791,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void testSwitchesToCorrectClockInSinglePaneShade() { mStatusBarStateController.setState(KEYGUARD); @@ -765,6 +807,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void testSwitchesToCorrectClockInSplitShade() { mStatusBarStateController.setState(KEYGUARD); enableSplitShade(/* enabled= */ true); @@ -785,6 +828,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void testHasNotifications_switchesToLargeClockWhenEnteringSplitShade() { mStatusBarStateController.setState(KEYGUARD); when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(1); @@ -796,6 +840,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void testNoNotifications_switchesToLargeClockWhenEnteringSplitShade() { mStatusBarStateController.setState(KEYGUARD); when(mNotificationStackScrollLayoutController.getVisibleNotificationCount()).thenReturn(0); @@ -807,6 +852,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void testHasNotifications_switchesToSmallClockWhenExitingSplitShade() { mStatusBarStateController.setState(KEYGUARD); enableSplitShade(/* enabled= */ true); @@ -820,6 +866,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void testNoNotifications_switchesToLargeClockWhenExitingSplitShade() { mStatusBarStateController.setState(KEYGUARD); enableSplitShade(/* enabled= */ true); @@ -833,6 +880,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void clockSize_mediaShowing_inSplitShade_onAod_isLarge() { when(mDozeParameters.getAlwaysOn()).thenReturn(true); mStatusBarStateController.setState(KEYGUARD); @@ -848,6 +896,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void clockSize_mediaShowing_inSplitShade_screenOff_notAod_isSmall() { when(mDozeParameters.getAlwaysOn()).thenReturn(false); mStatusBarStateController.setState(KEYGUARD); @@ -863,6 +912,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void onQsSetExpansionHeightCalled_qsFullyExpandedOnKeyguard_showNSSL() { // GIVEN mStatusBarStateController.setState(KEYGUARD); @@ -883,6 +933,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void onQsSetExpansionHeightCalled_qsFullyExpandedOnKeyguard_hideNSSL() { // GIVEN mStatusBarStateController.setState(KEYGUARD); @@ -904,6 +955,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void testSwitchesToBigClockInSplitShadeOnAodAnimateDisabled() { when(mScreenOffAnimationController.shouldAnimateClockChange()).thenReturn(false); mStatusBarStateController.setState(KEYGUARD); @@ -919,6 +971,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void switchesToBigClockInSplitShadeOn_landFlagOn_ForceSmallClock() { when(mScreenOffAnimationController.shouldAnimateClockChange()).thenReturn(false); mStatusBarStateController.setState(KEYGUARD); @@ -938,6 +991,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void switchesToBigClockInSplitShadeOn_landFlagOff_DontForceSmallClock() { when(mScreenOffAnimationController.shouldAnimateClockChange()).thenReturn(false); mStatusBarStateController.setState(KEYGUARD); @@ -957,6 +1011,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void testDisplaysSmallClockOnLockscreenInSplitShadeWhenMediaIsPlaying() { mStatusBarStateController.setState(KEYGUARD); enableSplitShade(/* enabled= */ true); @@ -978,6 +1033,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void testFoldToAodAnimationCleansupInAnimationEnd() { ArgumentCaptor<Animator.AnimatorListener> animCaptor = ArgumentCaptor.forClass(Animator.AnimatorListener.class); @@ -997,6 +1053,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void testExpandWithQsMethodIsUsingLockscreenTransitionController() { enableSplitShade(/* enabled= */ true); mStatusBarStateController.setState(KEYGUARD); @@ -1008,6 +1065,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void setKeyguardStatusBarAlpha_setsAlphaOnKeyguardStatusBarController() { float statusBarAlpha = 0.5f; @@ -1017,6 +1075,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void testQsToBeImmediatelyExpandedWhenOpeningPanelInSplitShade() { enableSplitShade(/* enabled= */ true); mShadeExpansionStateManager.updateState(STATE_OPEN); @@ -1030,6 +1089,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void testQsNotToBeImmediatelyExpandedWhenGoingFromUnlockedToLocked() { enableSplitShade(/* enabled= */ true); mShadeExpansionStateManager.updateState(STATE_CLOSED); @@ -1042,6 +1102,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void testQsImmediateResetsWhenPanelOpensOrCloses() { mShadeExpansionStateManager.updateState(STATE_OPEN); mShadeExpansionStateManager.updateState(STATE_CLOSED); @@ -1049,6 +1110,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void testQsExpansionChangedToDefaultWhenRotatingFromOrToSplitShade() { when(mCommandQueue.panelsEnabled()).thenReturn(true); @@ -1065,6 +1127,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void testPanelClosedWhenClosingQsInSplitShade() { mShadeExpansionStateManager.onPanelExpansionChanged(/* fraction= */ 1, /* expanded= */ true, /* tracking= */ false); @@ -1078,6 +1141,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void getMaxPanelTransitionDistance_expanding_inSplitShade_returnsSplitShadeFullTransitionDistance() { enableSplitShade(true); mNotificationPanelViewController.expandToQs(); @@ -1088,6 +1152,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void isExpandingOrCollapsing_returnsTrue_whenQsLockscreenDragInProgress() { when(mQsController.getLockscreenShadeDragProgress()).thenReturn(0.5f); assertThat(mNotificationPanelViewController.isExpandingOrCollapsing()).isTrue(); @@ -1095,6 +1160,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void getMaxPanelTransitionDistance_inSplitShade_withHeadsUp_returnsBiggerValue() { enableSplitShade(true); mNotificationPanelViewController.expandToQs(); @@ -1111,6 +1177,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void getMaxPanelTransitionDistance_expandingSplitShade_keyguard_returnsNonSplitShadeValue() { mStatusBarStateController.setState(KEYGUARD); enableSplitShade(true); @@ -1122,6 +1189,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void getMaxPanelTransitionDistance_expanding_notSplitShade_returnsNonSplitShadeValue() { enableSplitShade(false); mNotificationPanelViewController.expandToQs(); @@ -1132,6 +1200,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void onLayoutChange_fullWidth_updatesQSWithFullWithTrue() { setIsFullWidth(true); @@ -1139,6 +1208,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void onLayoutChange_notFullWidth_updatesQSWithFullWithFalse() { setIsFullWidth(false); @@ -1146,6 +1216,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void onLayoutChange_qsNotSet_doesNotCrash() { mQuickSettingsController.setQs(null); @@ -1153,6 +1224,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void onEmptySpaceClicked_notDozingAndOnKeyguard_requestsFaceAuth() { StatusBarStateController.StateListener statusBarStateListener = mNotificationPanelViewController.getStatusBarStateListener(); @@ -1167,8 +1239,8 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @EnableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void nsslFlagEnabled_allowOnlyExternalTouches() { - mSetFlagsRule.enableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT); // This sets the dozing state that is read when onMiddleClicked is eventually invoked. mTouchHandler.onTouch(mock(View.class), mDownMotionEvent); @@ -1179,6 +1251,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void onSplitShadeChanged_duringShadeExpansion_resetsOverScrollState() { // There was a bug where there was left-over overscroll state after going from split shade // to single shade. @@ -1200,6 +1273,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void onSplitShadeChanged_alwaysResetsOverScrollState() { enableSplitShade(true); enableSplitShade(false); @@ -1217,6 +1291,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo * to ensure scrollY can be correctly set to be 0 */ @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void onShadeFlingClosingEnd_mAmbientStateSetClose_thenOnExpansionStopped() { // Given: Shade is expanded mNotificationPanelViewController.notifyExpandingFinished(); @@ -1237,6 +1312,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void onShadeFlingEnd_mExpandImmediateShouldBeReset() { mNotificationPanelViewController.onFlingEnd(false); @@ -1244,6 +1320,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void inUnlockedSplitShade_transitioningMaxTransitionDistance_makesShadeFullyExpanded() { mStatusBarStateController.setState(SHADE); enableSplitShade(true); @@ -1253,6 +1330,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void shadeFullyExpanded_inShadeState() { mStatusBarStateController.setState(SHADE); @@ -1265,6 +1343,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void shadeFullyExpanded_onKeyguard() { mStatusBarStateController.setState(KEYGUARD); @@ -1274,12 +1353,14 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void shadeFullyExpanded_onShadeLocked() { mStatusBarStateController.setState(SHADE_LOCKED); assertThat(mNotificationPanelViewController.isShadeFullyExpanded()).isTrue(); } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void shadeExpanded_whenHasHeight() { int transitionDistance = mNotificationPanelViewController.getMaxPanelTransitionDistance(); mNotificationPanelViewController.setExpandedHeight(transitionDistance); @@ -1287,6 +1368,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void shadeExpanded_whenInstantExpanding() { mNotificationPanelViewController.expand(true); assertThat(mNotificationPanelViewController.isExpanded()).isTrue(); @@ -1300,12 +1382,14 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void shadeExpanded_whenUnlockedOffscreenAnimationRunning() { when(mUnlockedScreenOffAnimationController.isAnimationPlaying()).thenReturn(true); assertThat(mNotificationPanelViewController.isExpanded()).isTrue(); } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void shadeExpanded_whenInputFocusTransferStarted() { when(mCommandQueue.panelsEnabled()).thenReturn(true); @@ -1315,6 +1399,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void shadeNotExpanded_whenInputFocusTransferStartedButPanelsDisabled() { when(mCommandQueue.panelsEnabled()).thenReturn(false); @@ -1324,6 +1409,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void cancelInputFocusTransfer_shadeCollapsed() { when(mCommandQueue.panelsEnabled()).thenReturn(true); mNotificationPanelViewController.startInputFocusTransfer(); @@ -1334,6 +1420,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void finishInputFocusTransfer_shadeFlingingOpen() { when(mCommandQueue.panelsEnabled()).thenReturn(true); mNotificationPanelViewController.startInputFocusTransfer(); @@ -1344,6 +1431,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void getFalsingThreshold_deviceNotInteractive_isQsThreshold() { PowerInteractor.Companion.setAsleepForTest( mPowerInteractor, PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON); @@ -1353,6 +1441,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void getFalsingThreshold_lastWakeNotDueToTouch_isQsThreshold() { PowerInteractor.Companion.setAwakeForTest( mPowerInteractor, PowerManager.WAKE_REASON_POWER_BUTTON); @@ -1362,6 +1451,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void getFalsingThreshold_lastWakeDueToTouch_greaterThanQsThreshold() { PowerInteractor.Companion.setAwakeForTest(mPowerInteractor, PowerManager.WAKE_REASON_TAP); when(mQsController.getFalsingThreshold()).thenReturn(14); diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt index e1d92e780c2a..64eadb7db1e3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt @@ -18,6 +18,7 @@ package com.android.systemui.shade +import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.testing.TestableLooper import android.view.HapticFeedbackConstants @@ -27,6 +28,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.util.CollectionUtils import com.android.keyguard.KeyguardClockSwitch.LARGE +import com.android.systemui.Flags import com.android.systemui.coroutines.collectLastValue import com.android.systemui.res.R import com.android.systemui.statusbar.StatusBarState.KEYGUARD @@ -34,7 +36,6 @@ import com.android.systemui.statusbar.StatusBarState.SHADE import com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor -import com.android.systemui.statusbar.notification.stack.data.repository.setNotifications import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat @@ -58,6 +59,7 @@ import org.mockito.Mockito.verify @RunWith(AndroidJUnit4::class) @TestableLooper.RunWithLooper(setAsMainLooper = true) @SmallTest +@DisableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) class NotificationPanelViewControllerWithCoroutinesTest : NotificationPanelViewControllerBaseTest() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt index 74a299910b18..6f2302a22d7b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt @@ -17,6 +17,8 @@ package com.android.systemui.shade import android.content.Context +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import android.platform.test.annotations.RequiresFlagsDisabled import android.platform.test.flag.junit.FlagsParameterization import android.testing.TestableLooper @@ -31,6 +33,7 @@ import com.android.keyguard.KeyguardSecurityContainerController import com.android.keyguard.LegacyLockIconViewController import com.android.keyguard.dagger.KeyguardBouncerComponent import com.android.systemui.Flags +import com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT import com.android.systemui.SysuiTestCase import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor @@ -398,8 +401,8 @@ class NotificationShadeWindowViewControllerTest(flags: FlagsParameterization) : @Test @DisableSceneContainer + @DisableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) fun handleDispatchTouchEvent_nsslMigrationOff_userActivity_not_called() { - mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) underTest.setStatusBarViewController(phoneStatusBarViewController) interactionEventHandler.handleDispatchTouchEvent(DOWN_EVENT) @@ -408,8 +411,8 @@ class NotificationShadeWindowViewControllerTest(flags: FlagsParameterization) : } @Test + @EnableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) fun handleDispatchTouchEvent_nsslMigrationOn_userActivity() { - enableMigrateClocksFlag() underTest.setStatusBarViewController(phoneStatusBarViewController) interactionEventHandler.handleDispatchTouchEvent(DOWN_EVENT) @@ -440,6 +443,7 @@ class NotificationShadeWindowViewControllerTest(flags: FlagsParameterization) : } @Test + @EnableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) fun shouldInterceptTouchEvent_dozing_touchInLockIconArea_touchNotIntercepted() { // GIVEN dozing whenever(sysuiStatusBarStateController.isDozing).thenReturn(true) @@ -452,13 +456,12 @@ class NotificationShadeWindowViewControllerTest(flags: FlagsParameterization) : // AND the lock icon wants the touch whenever(lockIconViewController.willHandleTouchWhileDozing(DOWN_EVENT)).thenReturn(true) - enableMigrateClocksFlag() - // THEN touch should NOT be intercepted by NotificationShade assertThat(interactionEventHandler.shouldInterceptTouchEvent(DOWN_EVENT)).isFalse() } @Test + @EnableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) fun shouldInterceptTouchEvent_dozing_touchNotInLockIconArea_touchIntercepted() { // GIVEN dozing whenever(sysuiStatusBarStateController.isDozing).thenReturn(true) @@ -471,13 +474,12 @@ class NotificationShadeWindowViewControllerTest(flags: FlagsParameterization) : whenever(quickSettingsController.shouldQuickSettingsIntercept(any(), any(), any())) .thenReturn(false) - enableMigrateClocksFlag() - // THEN touch should be intercepted by NotificationShade assertThat(interactionEventHandler.shouldInterceptTouchEvent(DOWN_EVENT)).isTrue() } @Test + @EnableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) fun shouldInterceptTouchEvent_dozing_touchInStatusBar_touchIntercepted() { // GIVEN dozing whenever(sysuiStatusBarStateController.isDozing).thenReturn(true) @@ -490,13 +492,12 @@ class NotificationShadeWindowViewControllerTest(flags: FlagsParameterization) : whenever(quickSettingsController.shouldQuickSettingsIntercept(any(), any(), any())) .thenReturn(true) - enableMigrateClocksFlag() - // THEN touch should be intercepted by NotificationShade assertThat(interactionEventHandler.shouldInterceptTouchEvent(DOWN_EVENT)).isTrue() } @Test + @EnableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) fun shouldInterceptTouchEvent_dozingAndPulsing_touchIntercepted() { // GIVEN dozing whenever(sysuiStatusBarStateController.isDozing).thenReturn(true) @@ -517,8 +518,6 @@ class NotificationShadeWindowViewControllerTest(flags: FlagsParameterization) : whenever(shadeViewController.handleExternalInterceptTouch(DOWN_EVENT)) .thenReturn(true) - enableMigrateClocksFlag() - // THEN touch should be intercepted by NotificationShade assertThat(interactionEventHandler.shouldInterceptTouchEvent(DOWN_EVENT)).isTrue() } @@ -652,19 +651,13 @@ class NotificationShadeWindowViewControllerTest(flags: FlagsParameterization) : } @Test + @EnableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) fun cancelCurrentTouch_callsDragDownHelper() { - enableMigrateClocksFlag() underTest.cancelCurrentTouch() verify(dragDownHelper).stopDragging() } - private fun enableMigrateClocksFlag() { - if (!Flags.migrateClocksToBlueprint()) { - mSetFlagsRule.enableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) - } - } - companion object { private val DOWN_EVENT = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0) private val MOVE_EVENT = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_MOVE, 0f, 0f, 0) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt index fec7424ac006..ca29dd98a637 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.shade import android.os.SystemClock +import android.platform.test.annotations.DisableFlags import android.testing.TestableLooper.RunWithLooper import android.view.MotionEvent import android.widget.FrameLayout @@ -208,9 +209,9 @@ class NotificationShadeWindowViewTest : SysuiTestCase() { } @Test + @DisableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) fun testDragDownHelperCalledWhenDraggingDown() = testScope.runTest { - mSetFlagsRule.disableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) whenever(dragDownHelper.isDraggingDown).thenReturn(true) val now = SystemClock.elapsedRealtime() val ev = MotionEvent.obtain(now, now, MotionEvent.ACTION_UP, 0f, 0f, 0 /* meta */) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt index c9a7c82d6b3f..02764f8a15fd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt @@ -16,9 +16,13 @@ package com.android.systemui.statusbar.chips.casttootherdevice.ui.viewmodel +import android.content.DialogInterface import android.view.View import androidx.test.filters.SmallTest +import com.android.internal.jank.Cuj import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.DialogCuj +import com.android.systemui.animation.mockDialogTransitionAnimator import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.coroutines.collectLastValue @@ -37,6 +41,8 @@ import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.Me import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.setUpPackageManagerForMediaProjection import com.android.systemui.statusbar.chips.ui.model.ColorsModel import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel +import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer +import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModelTest.Companion.getStopActionFromDialog import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory import com.android.systemui.statusbar.policy.CastDevice @@ -45,7 +51,10 @@ import com.google.common.truth.Truth.assertThat import kotlin.test.Test import kotlinx.coroutines.test.runTest import org.junit.Before +import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @@ -60,6 +69,16 @@ class CastToOtherDeviceChipViewModelTest : SysuiTestCase() { private val mockScreenCastDialog = mock<SystemUIDialog>() private val mockGenericCastDialog = mock<SystemUIDialog>() + private val chipBackgroundView = mock<ChipBackgroundContainer>() + private val chipView = + mock<View>().apply { + whenever( + this.requireViewById<ChipBackgroundContainer>( + R.id.ongoing_activity_chip_background + ) + ) + .thenReturn(chipBackgroundView) + } private val underTest = kosmos.castToOtherDeviceChipViewModel @@ -193,6 +212,63 @@ class CastToOtherDeviceChipViewModelTest : SysuiTestCase() { } @Test + fun chip_projectionStoppedFromDialog_chipImmediatelyHidden() = + testScope.runTest { + val latest by collectLastValue(underTest.chip) + + mediaProjectionRepo.mediaProjectionState.value = + MediaProjectionState.Projecting.EntireScreen(CAST_TO_OTHER_DEVICES_PACKAGE) + + assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java) + + // WHEN the stop action on the dialog is clicked + val dialogStopAction = + getStopActionFromDialog(latest, chipView, mockScreenCastDialog, kosmos) + dialogStopAction.onClick(mock<DialogInterface>(), 0) + + // THEN the chip is immediately hidden... + assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java) + // ...even though the repo still says it's projecting + assertThat(mediaProjectionRepo.mediaProjectionState.value) + .isInstanceOf(MediaProjectionState.Projecting::class.java) + + // AND we specify no animation + assertThat((latest as OngoingActivityChipModel.Hidden).shouldAnimate).isFalse() + } + + @Test + fun chip_routeStoppedFromDialog_chipImmediatelyHidden() = + testScope.runTest { + val latest by collectLastValue(underTest.chip) + + mediaRouterRepo.castDevices.value = + listOf( + CastDevice( + state = CastDevice.CastState.Connected, + id = "id", + name = "name", + description = "desc", + origin = CastDevice.CastOrigin.MediaRouter, + ) + ) + + assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java) + + // WHEN the stop action on the dialog is clicked + val dialogStopAction = + getStopActionFromDialog(latest, chipView, mockGenericCastDialog, kosmos) + dialogStopAction.onClick(mock<DialogInterface>(), 0) + + // THEN the chip is immediately hidden... + assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java) + // ...even though the repo still says it's projecting + assertThat(mediaRouterRepo.castDevices.value).isNotEmpty() + + // AND we specify no animation + assertThat((latest as OngoingActivityChipModel.Hidden).shouldAnimate).isFalse() + } + + @Test fun chip_colorsAreRed() = testScope.runTest { val latest by collectLastValue(underTest.chip) @@ -297,8 +373,14 @@ class CastToOtherDeviceChipViewModelTest : SysuiTestCase() { val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener) assertThat(clickListener).isNotNull() - clickListener!!.onClick(mock<View>()) - verify(mockScreenCastDialog).show() + clickListener!!.onClick(chipView) + verify(kosmos.mockDialogTransitionAnimator) + .showFromView( + eq(mockScreenCastDialog), + eq(chipBackgroundView), + any(), + anyBoolean(), + ) } @Test @@ -316,8 +398,14 @@ class CastToOtherDeviceChipViewModelTest : SysuiTestCase() { val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener) assertThat(clickListener).isNotNull() - clickListener!!.onClick(mock<View>()) - verify(mockScreenCastDialog).show() + clickListener!!.onClick(chipView) + verify(kosmos.mockDialogTransitionAnimator) + .showFromView( + eq(mockScreenCastDialog), + eq(chipBackgroundView), + any(), + anyBoolean(), + ) } @Test @@ -339,7 +427,70 @@ class CastToOtherDeviceChipViewModelTest : SysuiTestCase() { val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener) assertThat(clickListener).isNotNull() - clickListener!!.onClick(mock<View>()) - verify(mockGenericCastDialog).show() + clickListener!!.onClick(chipView) + verify(kosmos.mockDialogTransitionAnimator) + .showFromView( + eq(mockGenericCastDialog), + eq(chipBackgroundView), + any(), + anyBoolean(), + ) + } + + @Test + fun chip_projectionStateCasting_clickListenerHasCuj() = + testScope.runTest { + val latest by collectLastValue(underTest.chip) + mediaProjectionRepo.mediaProjectionState.value = + MediaProjectionState.Projecting.EntireScreen(CAST_TO_OTHER_DEVICES_PACKAGE) + + val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener) + clickListener!!.onClick(chipView) + + val cujCaptor = argumentCaptor<DialogCuj>() + verify(kosmos.mockDialogTransitionAnimator) + .showFromView( + any(), + any(), + cujCaptor.capture(), + anyBoolean(), + ) + + assertThat(cujCaptor.firstValue.cujType) + .isEqualTo(Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP) + assertThat(cujCaptor.firstValue.tag).contains("Cast") + } + + @Test + fun chip_routerStateCasting_clickListenerHasCuj() = + testScope.runTest { + val latest by collectLastValue(underTest.chip) + + mediaRouterRepo.castDevices.value = + listOf( + CastDevice( + state = CastDevice.CastState.Connected, + id = "id", + name = "name", + description = "desc", + origin = CastDevice.CastOrigin.MediaRouter, + ) + ) + + val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener) + clickListener!!.onClick(chipView) + + val cujCaptor = argumentCaptor<DialogCuj>() + verify(kosmos.mockDialogTransitionAnimator) + .showFromView( + any(), + any(), + cujCaptor.capture(), + anyBoolean(), + ) + + assertThat(cujCaptor.firstValue.cujType) + .isEqualTo(Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP) + assertThat(cujCaptor.firstValue.tag).contains("Cast") } } 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 4728c649b9a7..b4a37ee1a55e 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 @@ -16,9 +16,13 @@ package com.android.systemui.statusbar.chips.screenrecord.ui.viewmodel +import android.content.DialogInterface import android.view.View import androidx.test.filters.SmallTest +import com.android.internal.jank.Cuj import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.DialogCuj +import com.android.systemui.animation.mockDialogTransitionAnimator import com.android.systemui.common.shared.model.Icon import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.Kosmos @@ -30,9 +34,13 @@ import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager import com.android.systemui.res.R import com.android.systemui.screenrecord.data.model.ScreenRecordModel import com.android.systemui.screenrecord.data.repository.screenRecordRepository +import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.setUpPackageManagerForMediaProjection import com.android.systemui.statusbar.chips.screenrecord.ui.view.EndScreenRecordingDialogDelegate +import com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel.shareToAppChipViewModel import com.android.systemui.statusbar.chips.ui.model.ColorsModel import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel +import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer +import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModelTest.Companion.getStopActionFromDialog import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory import com.android.systemui.util.time.fakeSystemClock @@ -40,7 +48,10 @@ import com.google.common.truth.Truth.assertThat import kotlin.test.Test import kotlinx.coroutines.test.runTest import org.junit.Before +import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @@ -53,11 +64,22 @@ class ScreenRecordChipViewModelTest : SysuiTestCase() { private val mediaProjectionRepo = kosmos.fakeMediaProjectionRepository private val systemClock = kosmos.fakeSystemClock private val mockSystemUIDialog = mock<SystemUIDialog>() + private val chipBackgroundView = mock<ChipBackgroundContainer>() + private val chipView = + mock<View>().apply { + whenever( + this.requireViewById<ChipBackgroundContainer>( + R.id.ongoing_activity_chip_background + ) + ) + .thenReturn(chipBackgroundView) + } private val underTest = kosmos.screenRecordChipViewModel @Before fun setUp() { + setUpPackageManagerForMediaProjection(kosmos) whenever(kosmos.mockSystemUIDialogFactory.create(any<EndScreenRecordingDialogDelegate>())) .thenReturn(mockSystemUIDialog) } @@ -132,6 +154,40 @@ class ScreenRecordChipViewModelTest : SysuiTestCase() { } @Test + fun chip_recordingStoppedFromDialog_screenRecordAndShareToAppChipImmediatelyHidden() = + testScope.runTest { + val latest by collectLastValue(underTest.chip) + val latestShareToApp by collectLastValue(kosmos.shareToAppChipViewModel.chip) + + // On real devices, when screen recording is active then share-to-app is also active + // because screen record is just a special case of share-to-app where the app receiving + // the share is SysUI + screenRecordRepo.screenRecordState.value = ScreenRecordModel.Recording + mediaProjectionRepo.mediaProjectionState.value = + MediaProjectionState.Projecting.EntireScreen("fake.package") + + assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java) + assertThat(latestShareToApp).isInstanceOf(OngoingActivityChipModel.Shown::class.java) + + // WHEN the stop action on the dialog is clicked + val dialogStopAction = + getStopActionFromDialog(latest, chipView, mockSystemUIDialog, kosmos) + dialogStopAction.onClick(mock<DialogInterface>(), 0) + + // THEN both the screen record chip and the share-to-app chip are immediately hidden... + assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java) + assertThat(latestShareToApp).isInstanceOf(OngoingActivityChipModel.Hidden::class.java) + // ...even though the repos still say it's recording + assertThat(screenRecordRepo.screenRecordState.value) + .isEqualTo(ScreenRecordModel.Recording) + assertThat(mediaProjectionRepo.mediaProjectionState.value) + .isInstanceOf(MediaProjectionState.Projecting::class.java) + + // AND we specify no animation + assertThat((latest as OngoingActivityChipModel.Hidden).shouldAnimate).isFalse() + } + + @Test fun chip_startingState_colorsAreRed() = testScope.runTest { val latest by collectLastValue(underTest.chip) @@ -182,9 +238,15 @@ class ScreenRecordChipViewModelTest : SysuiTestCase() { val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener) assertThat(clickListener).isNotNull() - clickListener!!.onClick(mock<View>()) + clickListener!!.onClick(chipView) // EndScreenRecordingDialogDelegate will test that the dialog has the right message - verify(mockSystemUIDialog).show() + verify(kosmos.mockDialogTransitionAnimator) + .showFromView( + eq(mockSystemUIDialog), + eq(chipBackgroundView), + any(), + anyBoolean(), + ) } @Test @@ -198,9 +260,15 @@ class ScreenRecordChipViewModelTest : SysuiTestCase() { val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener) assertThat(clickListener).isNotNull() - clickListener!!.onClick(mock<View>()) + clickListener!!.onClick(chipView) // EndScreenRecordingDialogDelegate will test that the dialog has the right message - verify(mockSystemUIDialog).show() + verify(kosmos.mockDialogTransitionAnimator) + .showFromView( + eq(mockSystemUIDialog), + eq(chipBackgroundView), + any(), + anyBoolean(), + ) } @Test @@ -218,8 +286,39 @@ class ScreenRecordChipViewModelTest : SysuiTestCase() { val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener) assertThat(clickListener).isNotNull() - clickListener!!.onClick(mock<View>()) + clickListener!!.onClick(chipView) // EndScreenRecordingDialogDelegate will test that the dialog has the right message - verify(mockSystemUIDialog).show() + verify(kosmos.mockDialogTransitionAnimator) + .showFromView( + eq(mockSystemUIDialog), + eq(chipBackgroundView), + any(), + anyBoolean(), + ) + } + + @Test + fun chip_clickListenerHasCuj() = + testScope.runTest { + val latest by collectLastValue(underTest.chip) + screenRecordRepo.screenRecordState.value = ScreenRecordModel.Recording + mediaProjectionRepo.mediaProjectionState.value = + MediaProjectionState.Projecting.EntireScreen("host.package") + + val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener) + clickListener!!.onClick(chipView) + + val cujCaptor = argumentCaptor<DialogCuj>() + verify(kosmos.mockDialogTransitionAnimator) + .showFromView( + any(), + any(), + cujCaptor.capture(), + anyBoolean(), + ) + + assertThat(cujCaptor.firstValue.cujType) + .isEqualTo(Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP) + assertThat(cujCaptor.firstValue.tag).contains("Screen record") } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt index f87b17dc92d1..2658679dee08 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt @@ -16,9 +16,13 @@ package com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel +import android.content.DialogInterface import android.view.View import androidx.test.filters.SmallTest +import com.android.internal.jank.Cuj import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.DialogCuj +import com.android.systemui.animation.mockDialogTransitionAnimator import com.android.systemui.common.shared.model.Icon import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.Kosmos @@ -34,6 +38,8 @@ import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.Me import com.android.systemui.statusbar.chips.sharetoapp.ui.view.EndShareToAppDialogDelegate import com.android.systemui.statusbar.chips.ui.model.ColorsModel import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel +import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer +import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModelTest.Companion.getStopActionFromDialog import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory import com.android.systemui.util.time.fakeSystemClock @@ -41,7 +47,10 @@ import com.google.common.truth.Truth.assertThat import kotlin.test.Test import kotlinx.coroutines.test.runTest import org.junit.Before +import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @@ -54,6 +63,16 @@ class ShareToAppChipViewModelTest : SysuiTestCase() { private val systemClock = kosmos.fakeSystemClock private val mockShareDialog = mock<SystemUIDialog>() + private val chipBackgroundView = mock<ChipBackgroundContainer>() + private val chipView = + mock<View>().apply { + whenever( + this.requireViewById<ChipBackgroundContainer>( + R.id.ongoing_activity_chip_background + ) + ) + .thenReturn(chipBackgroundView) + } private val underTest = kosmos.shareToAppChipViewModel @@ -134,6 +153,31 @@ class ShareToAppChipViewModelTest : SysuiTestCase() { } @Test + fun chip_shareStoppedFromDialog_chipImmediatelyHidden() = + testScope.runTest { + val latest by collectLastValue(underTest.chip) + + mediaProjectionRepo.mediaProjectionState.value = + MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE) + + assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java) + + // WHEN the stop action on the dialog is clicked + val dialogStopAction = + getStopActionFromDialog(latest, chipView, mockShareDialog, kosmos) + dialogStopAction.onClick(mock<DialogInterface>(), 0) + + // THEN the chip is immediately hidden... + assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java) + // ...even though the repo still says it's projecting + assertThat(mediaProjectionRepo.mediaProjectionState.value) + .isInstanceOf(MediaProjectionState.Projecting::class.java) + + // AND we specify no animation + assertThat((latest as OngoingActivityChipModel.Hidden).shouldAnimate).isFalse() + } + + @Test fun chip_colorsAreRed() = testScope.runTest { val latest by collectLastValue(underTest.chip) @@ -181,8 +225,14 @@ class ShareToAppChipViewModelTest : SysuiTestCase() { val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener) assertThat(clickListener).isNotNull() - clickListener!!.onClick(mock<View>()) - verify(mockShareDialog).show() + clickListener!!.onClick(chipView) + verify(kosmos.mockDialogTransitionAnimator) + .showFromView( + eq(mockShareDialog), + eq(chipBackgroundView), + any(), + anyBoolean(), + ) } @Test @@ -199,7 +249,41 @@ class ShareToAppChipViewModelTest : SysuiTestCase() { val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener) assertThat(clickListener).isNotNull() - clickListener!!.onClick(mock<View>()) - verify(mockShareDialog).show() + clickListener!!.onClick(chipView) + verify(kosmos.mockDialogTransitionAnimator) + .showFromView( + eq(mockShareDialog), + eq(chipBackgroundView), + any(), + anyBoolean(), + ) + } + + @Test + fun chip_clickListenerHasCuj() = + testScope.runTest { + val latest by collectLastValue(underTest.chip) + mediaProjectionRepo.mediaProjectionState.value = + MediaProjectionState.Projecting.SingleTask( + NORMAL_PACKAGE, + hostDeviceName = null, + createTask(taskId = 1), + ) + + val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener) + clickListener!!.onClick(chipView) + + val cujCaptor = argumentCaptor<DialogCuj>() + verify(kosmos.mockDialogTransitionAnimator) + .showFromView( + any(), + any(), + cujCaptor.capture(), + anyBoolean(), + ) + + assertThat(cujCaptor.firstValue.cujType) + .isEqualTo(Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP) + assertThat(cujCaptor.firstValue.tag).contains("Share") } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt new file mode 100644 index 000000000000..b9049e8f76b6 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt @@ -0,0 +1,154 @@ +/* + * 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.statusbar.chips.ui.viewmodel + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testScope +import com.android.systemui.res.R +import com.android.systemui.statusbar.chips.ui.model.ColorsModel +import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel +import com.google.common.truth.Truth.assertThat +import kotlin.test.Test +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest + +@SmallTest +@OptIn(ExperimentalCoroutinesApi::class) +class ChipTransitionHelperTest : SysuiTestCase() { + private val kosmos = Kosmos() + private val testScope = kosmos.testScope + + @Test + fun createChipFlow_typicallyFollowsInputFlow() = + testScope.runTest { + val underTest = ChipTransitionHelper(kosmos.applicationCoroutineScope) + val inputChipFlow = + MutableStateFlow<OngoingActivityChipModel>(OngoingActivityChipModel.Hidden()) + val latest by collectLastValue(underTest.createChipFlow(inputChipFlow)) + + val newChip = + OngoingActivityChipModel.Shown.Timer( + icon = Icon.Resource(R.drawable.ic_cake, contentDescription = null), + colors = ColorsModel.Themed, + startTimeMs = 100L, + onClickListener = null, + ) + + inputChipFlow.value = newChip + + assertThat(latest).isEqualTo(newChip) + + val newerChip = + OngoingActivityChipModel.Shown.IconOnly( + icon = Icon.Resource(R.drawable.ic_hotspot, contentDescription = null), + colors = ColorsModel.Themed, + onClickListener = null, + ) + + inputChipFlow.value = newerChip + + assertThat(latest).isEqualTo(newerChip) + } + + @Test + fun activityStopped_chipHiddenWithoutAnimationFor500ms() = + testScope.runTest { + val underTest = ChipTransitionHelper(kosmos.applicationCoroutineScope) + val inputChipFlow = + MutableStateFlow<OngoingActivityChipModel>(OngoingActivityChipModel.Hidden()) + val latest by collectLastValue(underTest.createChipFlow(inputChipFlow)) + + val shownChip = + OngoingActivityChipModel.Shown.Timer( + icon = Icon.Resource(R.drawable.ic_cake, contentDescription = null), + colors = ColorsModel.Themed, + startTimeMs = 100L, + onClickListener = null, + ) + + inputChipFlow.value = shownChip + + assertThat(latest).isEqualTo(shownChip) + + // WHEN #onActivityStopped is invoked + underTest.onActivityStoppedFromDialog() + runCurrent() + + // THEN the chip is hidden and has no animation + assertThat(latest).isEqualTo(OngoingActivityChipModel.Hidden(shouldAnimate = false)) + + // WHEN only 250ms have elapsed + advanceTimeBy(250) + + // THEN the chip is still hidden + assertThat(latest).isEqualTo(OngoingActivityChipModel.Hidden(shouldAnimate = false)) + + // WHEN over 500ms have elapsed + advanceTimeBy(251) + + // THEN the chip returns to the original input flow value + assertThat(latest).isEqualTo(shownChip) + } + + @Test + fun activityStopped_stoppedAgainBefore500ms_chipReshownAfterSecond500ms() = + testScope.runTest { + val underTest = ChipTransitionHelper(kosmos.applicationCoroutineScope) + val inputChipFlow = + MutableStateFlow<OngoingActivityChipModel>(OngoingActivityChipModel.Hidden()) + val latest by collectLastValue(underTest.createChipFlow(inputChipFlow)) + + val shownChip = + OngoingActivityChipModel.Shown.Timer( + icon = Icon.Resource(R.drawable.ic_cake, contentDescription = null), + colors = ColorsModel.Themed, + startTimeMs = 100L, + onClickListener = null, + ) + + inputChipFlow.value = shownChip + + assertThat(latest).isEqualTo(shownChip) + + // WHEN #onActivityStopped is invoked + underTest.onActivityStoppedFromDialog() + runCurrent() + + // THEN the chip is hidden and has no animation + assertThat(latest).isEqualTo(OngoingActivityChipModel.Hidden(shouldAnimate = false)) + + // WHEN 250ms have elapsed, get another stop event + advanceTimeBy(250) + underTest.onActivityStoppedFromDialog() + runCurrent() + + // THEN the chip is still hidden for another 500ms afterwards + assertThat(latest).isEqualTo(OngoingActivityChipModel.Hidden(shouldAnimate = false)) + advanceTimeBy(499) + assertThat(latest).isEqualTo(OngoingActivityChipModel.Hidden(shouldAnimate = false)) + advanceTimeBy(2) + assertThat(latest).isEqualTo(shownChip) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt index ca043f163854..6e4d8863fee2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt @@ -18,32 +18,58 @@ package com.android.systemui.statusbar.chips.ui.viewmodel import android.view.View import androidx.test.filters.SmallTest +import com.android.internal.jank.Cuj import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.DialogCuj +import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.log.logcatLogBuffer +import com.android.systemui.res.R +import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel.Companion.createDialogLaunchOnClickListener import com.android.systemui.statusbar.phone.SystemUIDialog import kotlin.test.Test +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever @SmallTest class OngoingActivityChipViewModelTest : SysuiTestCase() { private val mockSystemUIDialog = mock<SystemUIDialog>() private val dialogDelegate = SystemUIDialog.Delegate { mockSystemUIDialog } + private val dialogTransitionAnimator = mock<DialogTransitionAnimator>() + + private val chipBackgroundView = mock<ChipBackgroundContainer>() + private val chipView = + mock<View>().apply { + whenever( + this.requireViewById<ChipBackgroundContainer>( + R.id.ongoing_activity_chip_background + ) + ) + .thenReturn(chipBackgroundView) + } @Test fun createDialogLaunchOnClickListener_showsDialogOnClick() { + val cuj = DialogCuj(Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP, tag = "Test") val clickListener = createDialogLaunchOnClickListener( dialogDelegate, + dialogTransitionAnimator, + cuj, logcatLogBuffer("OngoingActivityChipViewModelTest"), "tag", ) - // Dialogs must be created on the main thread - context.mainExecutor.execute { - clickListener.onClick(mock<View>()) - verify(mockSystemUIDialog).show() - } + clickListener.onClick(chipView) + verify(dialogTransitionAnimator) + .showFromView( + eq(mockSystemUIDialog), + eq(chipBackgroundView), + eq(cuj), + anyBoolean(), + ) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt index b1a8d0beab34..ee249f0f8a2c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt @@ -16,6 +16,10 @@ package com.android.systemui.statusbar.chips.ui.viewmodel +import android.content.DialogInterface +import android.content.packageManager +import android.content.pm.PackageManager +import android.view.View import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -33,10 +37,14 @@ import com.android.systemui.screenrecord.data.repository.screenRecordRepository import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.NORMAL_PACKAGE import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.setUpPackageManagerForMediaProjection import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel +import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer +import com.android.systemui.statusbar.phone.SystemUIDialog +import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel import com.android.systemui.util.time.fakeSystemClock import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.test.runCurrent @@ -44,9 +52,17 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Mockito +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidJUnit4::class) +@OptIn(ExperimentalCoroutinesApi::class) class OngoingActivityChipsViewModelTest : SysuiTestCase() { private val kosmos = Kosmos().also { it.testCase = this } private val testScope = kosmos.testScope @@ -56,6 +72,18 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() { private val mediaProjectionState = kosmos.fakeMediaProjectionRepository.mediaProjectionState private val callRepo = kosmos.ongoingCallRepository + private val mockSystemUIDialog = mock<SystemUIDialog>() + private val chipBackgroundView = mock<ChipBackgroundContainer>() + private val chipView = + mock<View>().apply { + whenever( + this.requireViewById<ChipBackgroundContainer>( + R.id.ongoing_activity_chip_background + ) + ) + .thenReturn(chipBackgroundView) + } + private val underTest = kosmos.ongoingActivityChipsViewModel @Before @@ -72,7 +100,7 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() { val latest by collectLastValue(underTest.chip) - assertThat(latest).isEqualTo(OngoingActivityChipModel.Hidden) + assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java) } @Test @@ -230,7 +258,81 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() { job2.cancel() } + @Test + fun chip_screenRecordStoppedViaDialog_chipHiddenWithoutAnimation() = + testScope.runTest { + screenRecordState.value = ScreenRecordModel.Recording + mediaProjectionState.value = MediaProjectionState.NotProjecting + callRepo.setOngoingCallState(OngoingCallModel.NoCall) + + val latest by collectLastValue(underTest.chip) + + assertIsScreenRecordChip(latest) + + // WHEN screen record gets stopped via dialog + val dialogStopAction = + getStopActionFromDialog(latest, chipView, mockSystemUIDialog, kosmos) + dialogStopAction.onClick(mock<DialogInterface>(), 0) + + // THEN the chip is immediately hidden with no animation + assertThat(latest).isEqualTo(OngoingActivityChipModel.Hidden(shouldAnimate = false)) + } + + @Test + fun chip_projectionStoppedViaDialog_chipHiddenWithoutAnimation() = + testScope.runTest { + mediaProjectionState.value = + MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE) + screenRecordState.value = ScreenRecordModel.DoingNothing + callRepo.setOngoingCallState(OngoingCallModel.NoCall) + + val latest by collectLastValue(underTest.chip) + + assertIsShareToAppChip(latest) + + // WHEN media projection gets stopped via dialog + val dialogStopAction = + getStopActionFromDialog(latest, chipView, mockSystemUIDialog, kosmos) + dialogStopAction.onClick(mock<DialogInterface>(), 0) + + // THEN the chip is immediately hidden with no animation + assertThat(latest).isEqualTo(OngoingActivityChipModel.Hidden(shouldAnimate = false)) + } + companion object { + /** + * Assuming that the click listener in [latest] opens a dialog, this fetches the action + * associated with the positive button, which we assume is the "Stop sharing" action. + */ + fun getStopActionFromDialog( + latest: OngoingActivityChipModel?, + chipView: View, + dialog: SystemUIDialog, + kosmos: Kosmos + ): DialogInterface.OnClickListener { + // Capture the action that would get invoked when the user clicks "Stop" on the dialog + lateinit var dialogStopAction: DialogInterface.OnClickListener + Mockito.doAnswer { + val delegate = it.arguments[0] as SystemUIDialog.Delegate + delegate.beforeCreate(dialog, /* savedInstanceState= */ null) + + val stopActionCaptor = argumentCaptor<DialogInterface.OnClickListener>() + verify(dialog).setPositiveButton(any(), stopActionCaptor.capture()) + dialogStopAction = stopActionCaptor.firstValue + + return@doAnswer dialog + } + .whenever(kosmos.mockSystemUIDialogFactory) + .create(any<SystemUIDialog.Delegate>()) + whenever(kosmos.packageManager.getApplicationInfo(eq(NORMAL_PACKAGE), any<Int>())) + .thenThrow(PackageManager.NameNotFoundException()) + // Click the chip so that we open the dialog and we fill in [dialogStopAction] + val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener) + clickListener!!.onClick(chipView) + + return dialogStopAction + } + fun assertIsScreenRecordChip(latest: OngoingActivityChipModel?) { assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java) val icon = (latest as OngoingActivityChipModel.Shown).icon diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt index acb005f6e72f..0407fc14d35a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt @@ -42,7 +42,7 @@ import org.mockito.quality.Strictness @RunWith(AndroidJUnit4::class) @SmallTest // this class has no testable logic with either of these flags enabled -@DisableFlags(PriorityPeopleSection.FLAG_NAME, NotificationMinimalismPrototype.V2.FLAG_NAME) +@DisableFlags(PriorityPeopleSection.FLAG_NAME, NotificationMinimalismPrototype.FLAG_NAME) class NotificationSectionsFeatureManagerTest : SysuiTestCase() { lateinit var manager: NotificationSectionsFeatureManager private val proxyFake = DeviceConfigProxyFake() diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt index d87b3e23b471..4218be26c58e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt @@ -13,88 +13,57 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@file:OptIn(ExperimentalCoroutinesApi::class) package com.android.systemui.statusbar.notification.collection.coordinator -import android.app.Notification -import android.os.UserHandle -import android.platform.test.flag.junit.FlagsParameterization -import android.provider.Settings +import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.dump.DumpManager -import com.android.systemui.flags.andSceneContainer -import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository -import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository -import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor -import com.android.systemui.keyguard.shared.model.KeyguardState -import com.android.systemui.keyguard.shared.model.TransitionStep -import com.android.systemui.kosmos.Kosmos -import com.android.systemui.log.logcatLogBuffer import com.android.systemui.plugins.statusbar.StatusBarStateController -import com.android.systemui.scene.data.repository.Idle -import com.android.systemui.scene.data.repository.setTransition -import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.statusbar.StatusBarState -import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder import com.android.systemui.statusbar.notification.collection.NotifPipeline -import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder -import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter -import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Pluggable -import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener import com.android.systemui.statusbar.notification.collection.provider.SectionHeaderVisibilityProvider -import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository -import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider -import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow -import com.android.systemui.statusbar.policy.HeadsUpManager -import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener -import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.eq -import com.android.systemui.util.mockito.mock -import com.android.systemui.util.mockito.withArgCaptor -import com.android.systemui.util.settings.FakeSettings -import com.google.common.truth.Truth.assertThat import java.util.function.Consumer -import kotlin.time.Duration.Companion.seconds -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestCoroutineScheduler -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.runTest +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers.same -import org.mockito.Mockito.anyString import org.mockito.Mockito.clearInvocations -import org.mockito.Mockito.never import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` as whenever -import platform.test.runner.parameterized.ParameterizedAndroidJunit4 -import platform.test.runner.parameterized.Parameters +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever @SmallTest -@RunWith(ParameterizedAndroidJunit4::class) -class KeyguardCoordinatorTest(flags: FlagsParameterization) : SysuiTestCase() { +@RunWith(AndroidJUnit4::class) +class KeyguardCoordinatorTest : SysuiTestCase() { - private val kosmos = Kosmos() - - private val headsUpManager: HeadsUpManager = mock() private val keyguardNotifVisibilityProvider: KeyguardNotificationVisibilityProvider = mock() - private val keyguardRepository = FakeKeyguardRepository() - private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository private val notifPipeline: NotifPipeline = mock() private val sectionHeaderVisibilityProvider: SectionHeaderVisibilityProvider = mock() private val statusBarStateController: StatusBarStateController = mock() - init { - mSetFlagsRule.setFlagsParameterization(flags) + private lateinit var onStateChangeListener: Consumer<String> + + @Before + fun setup() { + val keyguardCoordinator = + KeyguardCoordinator( + keyguardNotifVisibilityProvider, + sectionHeaderVisibilityProvider, + statusBarStateController, + ) + keyguardCoordinator.attach(notifPipeline) + onStateChangeListener = + argumentCaptor { + verify(keyguardNotifVisibilityProvider).addOnStateChangedListener(capture()) + } + .lastValue } @Test - fun testSetSectionHeadersVisibleInShade() = runKeyguardCoordinatorTest { + fun testSetSectionHeadersVisibleInShade() { clearInvocations(sectionHeaderVisibilityProvider) whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE) onStateChangeListener.accept("state change") @@ -102,617 +71,10 @@ class KeyguardCoordinatorTest(flags: FlagsParameterization) : SysuiTestCase() { } @Test - fun testSetSectionHeadersNotVisibleOnKeyguard() = runKeyguardCoordinatorTest { + fun testSetSectionHeadersNotVisibleOnKeyguard() { clearInvocations(sectionHeaderVisibilityProvider) whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD) onStateChangeListener.accept("state change") verify(sectionHeaderVisibilityProvider).sectionHeadersVisible = eq(false) } - - @Test - fun unseenFilterSuppressesSeenNotifWhileKeyguardShowing() { - // GIVEN: Keyguard is not showing, shade is expanded, and a notification is present - keyguardRepository.setKeyguardShowing(false) - whenever(statusBarStateController.isExpanded).thenReturn(true) - runKeyguardCoordinatorTest { - val fakeEntry = NotificationEntryBuilder().build() - collectionListener.onEntryAdded(fakeEntry) - - // WHEN: The keyguard is now showing - keyguardRepository.setKeyguardShowing(true) - testScheduler.runCurrent() - - // THEN: The notification is recognized as "seen" and is filtered out. - assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue() - - // WHEN: The keyguard goes away - keyguardRepository.setKeyguardShowing(false) - testScheduler.runCurrent() - - // THEN: The notification is shown regardless - assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse() - } - } - - @Test - fun unseenFilterStopsMarkingSeenNotifWhenTransitionToAod() { - // GIVEN: Keyguard is not showing, shade is not expanded, and a notification is present - keyguardRepository.setKeyguardShowing(false) - whenever(statusBarStateController.isExpanded).thenReturn(false) - runKeyguardCoordinatorTest { - val fakeEntry = NotificationEntryBuilder().build() - collectionListener.onEntryAdded(fakeEntry) - - // WHEN: The device transitions to AOD - keyguardTransitionRepository.sendTransitionSteps( - from = KeyguardState.GONE, - to = KeyguardState.AOD, - this.testScheduler, - ) - testScheduler.runCurrent() - - // THEN: We are no longer listening for shade expansions - verify(statusBarStateController, never()).addCallback(any()) - } - } - - @Test - fun unseenFilter_headsUpMarkedAsSeen() { - // GIVEN: Keyguard is not showing, shade is not expanded - keyguardRepository.setKeyguardShowing(false) - whenever(statusBarStateController.isExpanded).thenReturn(false) - runKeyguardCoordinatorTest { - kosmos.setTransition( - sceneTransition = Idle(Scenes.Gone), - stateTransition = TransitionStep(KeyguardState.LOCKSCREEN, KeyguardState.GONE) - ) - - // WHEN: A notification is posted - val fakeEntry = NotificationEntryBuilder().build() - collectionListener.onEntryAdded(fakeEntry) - - // WHEN: That notification is heads up - onHeadsUpChangedListener.onHeadsUpStateChanged(fakeEntry, /* isHeadsUp= */ true) - testScheduler.runCurrent() - - // WHEN: The keyguard is now showing - keyguardRepository.setKeyguardShowing(true) - kosmos.setTransition( - sceneTransition = Idle(Scenes.Lockscreen), - stateTransition = TransitionStep(KeyguardState.GONE, KeyguardState.AOD) - ) - - // THEN: The notification is recognized as "seen" and is filtered out. - assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue() - - // WHEN: The keyguard goes away - keyguardRepository.setKeyguardShowing(false) - kosmos.setTransition( - sceneTransition = Idle(Scenes.Gone), - stateTransition = TransitionStep(KeyguardState.AOD, KeyguardState.GONE) - ) - - // THEN: The notification is shown regardless - assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse() - } - } - - @Test - fun unseenFilterDoesNotSuppressSeenOngoingNotifWhileKeyguardShowing() { - // GIVEN: Keyguard is not showing, shade is expanded, and an ongoing notification is present - keyguardRepository.setKeyguardShowing(false) - whenever(statusBarStateController.isExpanded).thenReturn(true) - runKeyguardCoordinatorTest { - val fakeEntry = - NotificationEntryBuilder() - .setNotification(Notification.Builder(mContext, "id").setOngoing(true).build()) - .build() - collectionListener.onEntryAdded(fakeEntry) - - // WHEN: The keyguard is now showing - keyguardRepository.setKeyguardShowing(true) - testScheduler.runCurrent() - - // THEN: The notification is recognized as "ongoing" and is not filtered out. - assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse() - } - } - - @Test - fun unseenFilterDoesNotSuppressSeenMediaNotifWhileKeyguardShowing() { - // GIVEN: Keyguard is not showing, shade is expanded, and a media notification is present - keyguardRepository.setKeyguardShowing(false) - whenever(statusBarStateController.isExpanded).thenReturn(true) - runKeyguardCoordinatorTest { - val fakeEntry = - NotificationEntryBuilder().build().apply { - row = - mock<ExpandableNotificationRow>().apply { - whenever(isMediaRow).thenReturn(true) - } - } - collectionListener.onEntryAdded(fakeEntry) - - // WHEN: The keyguard is now showing - keyguardRepository.setKeyguardShowing(true) - testScheduler.runCurrent() - - // THEN: The notification is recognized as "media" and is not filtered out. - assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse() - } - } - - @Test - fun unseenFilterUpdatesSeenProviderWhenSuppressing() { - // GIVEN: Keyguard is not showing, shade is expanded, and a notification is present - keyguardRepository.setKeyguardShowing(false) - whenever(statusBarStateController.isExpanded).thenReturn(true) - runKeyguardCoordinatorTest { - val fakeEntry = NotificationEntryBuilder().build() - collectionListener.onEntryAdded(fakeEntry) - - // WHEN: The keyguard is now showing - keyguardRepository.setKeyguardShowing(true) - testScheduler.runCurrent() - - // THEN: The notification is recognized as "seen" and is filtered out. - assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue() - - // WHEN: The filter is cleaned up - unseenFilter.onCleanup() - - // THEN: The SeenNotificationProvider has been updated to reflect the suppression - assertThat(seenNotificationsInteractor.hasFilteredOutSeenNotifications.value).isTrue() - } - } - - @Test - fun unseenFilterInvalidatesWhenSettingChanges() { - // GIVEN: Keyguard is not showing, and shade is expanded - keyguardRepository.setKeyguardShowing(false) - whenever(statusBarStateController.isExpanded).thenReturn(true) - runKeyguardCoordinatorTest { - // GIVEN: A notification is present - val fakeEntry = NotificationEntryBuilder().build() - collectionListener.onEntryAdded(fakeEntry) - - // GIVEN: The setting for filtering unseen notifications is disabled - showOnlyUnseenNotifsOnKeyguardSetting = false - - // GIVEN: The pipeline has registered the unseen filter for invalidation - val invalidationListener: Pluggable.PluggableListener<NotifFilter> = mock() - unseenFilter.setInvalidationListener(invalidationListener) - - // WHEN: The keyguard is now showing - keyguardRepository.setKeyguardShowing(true) - testScheduler.runCurrent() - - // THEN: The notification is not filtered out - assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse() - - // WHEN: The secure setting is changed - showOnlyUnseenNotifsOnKeyguardSetting = true - - // THEN: The pipeline is invalidated - verify(invalidationListener).onPluggableInvalidated(same(unseenFilter), anyString()) - - // THEN: The notification is recognized as "seen" and is filtered out. - assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue() - } - } - - @Test - fun unseenFilterAllowsNewNotif() { - // GIVEN: Keyguard is showing, no notifications present - keyguardRepository.setKeyguardShowing(true) - runKeyguardCoordinatorTest { - // WHEN: A new notification is posted - val fakeEntry = NotificationEntryBuilder().build() - collectionListener.onEntryAdded(fakeEntry) - - // THEN: The notification is recognized as "unseen" and is not filtered out. - assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse() - } - } - - @Test - fun unseenFilterSeenGroupSummaryWithUnseenChild() { - // GIVEN: Keyguard is not showing, shade is expanded, and a notification is present - keyguardRepository.setKeyguardShowing(false) - whenever(statusBarStateController.isExpanded).thenReturn(true) - runKeyguardCoordinatorTest { - // WHEN: A new notification is posted - val fakeSummary = NotificationEntryBuilder().build() - val fakeChild = - NotificationEntryBuilder() - .setGroup(context, "group") - .setGroupSummary(context, false) - .build() - GroupEntryBuilder().setSummary(fakeSummary).addChild(fakeChild).build() - - collectionListener.onEntryAdded(fakeSummary) - collectionListener.onEntryAdded(fakeChild) - - // WHEN: Keyguard is now showing, both notifications are marked as seen - keyguardRepository.setKeyguardShowing(true) - testScheduler.runCurrent() - - // WHEN: The child notification is now unseen - collectionListener.onEntryUpdated(fakeChild) - - // THEN: The summary is not filtered out, because the child is unseen - assertThat(unseenFilter.shouldFilterOut(fakeSummary, 0L)).isFalse() - } - } - - @Test - fun unseenNotificationIsMarkedAsSeenWhenKeyguardGoesAway() { - // GIVEN: Keyguard is showing, not dozing, unseen notification is present - keyguardRepository.setKeyguardShowing(true) - keyguardRepository.setIsDozing(false) - runKeyguardCoordinatorTest { - val fakeEntry = NotificationEntryBuilder().build() - collectionListener.onEntryAdded(fakeEntry) - keyguardTransitionRepository.sendTransitionSteps( - from = KeyguardState.AOD, - to = KeyguardState.LOCKSCREEN, - this.testScheduler, - ) - testScheduler.runCurrent() - - // WHEN: five seconds have passed - testScheduler.advanceTimeBy(5.seconds) - testScheduler.runCurrent() - - // WHEN: Keyguard is no longer showing - keyguardRepository.setKeyguardShowing(false) - kosmos.setTransition( - sceneTransition = Idle(Scenes.Gone), - stateTransition = TransitionStep(KeyguardState.LOCKSCREEN, KeyguardState.GONE) - ) - - // WHEN: Keyguard is shown again - keyguardRepository.setKeyguardShowing(true) - kosmos.setTransition( - sceneTransition = Idle(Scenes.Lockscreen), - stateTransition = TransitionStep(KeyguardState.GONE, KeyguardState.AOD) - ) - - // THEN: The notification is now recognized as "seen" and is filtered out. - assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue() - } - } - - @Test - fun unseenNotificationIsNotMarkedAsSeenIfShadeNotExpanded() { - // GIVEN: Keyguard is showing, unseen notification is present - keyguardRepository.setKeyguardShowing(true) - runKeyguardCoordinatorTest { - keyguardTransitionRepository.sendTransitionSteps( - from = KeyguardState.GONE, - to = KeyguardState.LOCKSCREEN, - this.testScheduler, - ) - val fakeEntry = NotificationEntryBuilder().build() - collectionListener.onEntryAdded(fakeEntry) - - // WHEN: Keyguard is no longer showing - keyguardRepository.setKeyguardShowing(false) - keyguardTransitionRepository.sendTransitionSteps( - from = KeyguardState.LOCKSCREEN, - to = KeyguardState.GONE, - this.testScheduler, - ) - - // WHEN: Keyguard is shown again - keyguardRepository.setKeyguardShowing(true) - testScheduler.runCurrent() - - // THEN: The notification is not recognized as "seen" and is not filtered out. - assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse() - } - } - - @Test - fun unseenNotificationIsNotMarkedAsSeenIfNotOnKeyguardLongEnough() { - // GIVEN: Keyguard is showing, not dozing, unseen notification is present - keyguardRepository.setKeyguardShowing(true) - keyguardRepository.setIsDozing(false) - runKeyguardCoordinatorTest { - kosmos.setTransition( - sceneTransition = Idle(Scenes.Lockscreen), - stateTransition = TransitionStep(KeyguardState.GONE, KeyguardState.LOCKSCREEN) - ) - val firstEntry = NotificationEntryBuilder().setId(1).build() - collectionListener.onEntryAdded(firstEntry) - testScheduler.runCurrent() - - // WHEN: one second has passed - testScheduler.advanceTimeBy(1.seconds) - testScheduler.runCurrent() - - // WHEN: another unseen notification is posted - val secondEntry = NotificationEntryBuilder().setId(2).build() - collectionListener.onEntryAdded(secondEntry) - testScheduler.runCurrent() - - // WHEN: four more seconds have passed - testScheduler.advanceTimeBy(4.seconds) - testScheduler.runCurrent() - - // WHEN: the keyguard is no longer showing - keyguardRepository.setKeyguardShowing(false) - kosmos.setTransition( - sceneTransition = Idle(Scenes.Gone), - stateTransition = TransitionStep(KeyguardState.LOCKSCREEN, KeyguardState.GONE) - ) - - // WHEN: Keyguard is shown again - keyguardRepository.setKeyguardShowing(true) - kosmos.setTransition( - sceneTransition = Idle(Scenes.Lockscreen), - stateTransition = TransitionStep(KeyguardState.GONE, KeyguardState.LOCKSCREEN) - ) - - // THEN: The first notification is considered seen and is filtered out. - assertThat(unseenFilter.shouldFilterOut(firstEntry, 0L)).isTrue() - - // THEN: The second notification is still considered unseen and is not filtered out - assertThat(unseenFilter.shouldFilterOut(secondEntry, 0L)).isFalse() - } - } - - @Test - fun unseenNotificationOnKeyguardNotMarkedAsSeenIfRemovedAfterThreshold() { - // GIVEN: Keyguard is showing, not dozing - keyguardRepository.setKeyguardShowing(true) - keyguardRepository.setIsDozing(false) - runKeyguardCoordinatorTest { - keyguardTransitionRepository.sendTransitionSteps( - from = KeyguardState.GONE, - to = KeyguardState.LOCKSCREEN, - this.testScheduler, - ) - testScheduler.runCurrent() - - // WHEN: a new notification is posted - val entry = NotificationEntryBuilder().setId(1).build() - collectionListener.onEntryAdded(entry) - testScheduler.runCurrent() - - // WHEN: five more seconds have passed - testScheduler.advanceTimeBy(5.seconds) - testScheduler.runCurrent() - - // WHEN: the notification is removed - collectionListener.onEntryRemoved(entry, 0) - testScheduler.runCurrent() - - // WHEN: the notification is re-posted - collectionListener.onEntryAdded(entry) - testScheduler.runCurrent() - - // WHEN: one more second has passed - testScheduler.advanceTimeBy(1.seconds) - testScheduler.runCurrent() - - // WHEN: the keyguard is no longer showing - keyguardRepository.setKeyguardShowing(false) - keyguardTransitionRepository.sendTransitionSteps( - from = KeyguardState.LOCKSCREEN, - to = KeyguardState.GONE, - this.testScheduler, - ) - testScheduler.runCurrent() - - // WHEN: Keyguard is shown again - keyguardRepository.setKeyguardShowing(true) - keyguardTransitionRepository.sendTransitionSteps( - from = KeyguardState.GONE, - to = KeyguardState.LOCKSCREEN, - this.testScheduler, - ) - testScheduler.runCurrent() - - // THEN: The notification is considered unseen and is not filtered out. - assertThat(unseenFilter.shouldFilterOut(entry, 0L)).isFalse() - } - } - - @Test - fun unseenNotificationOnKeyguardNotMarkedAsSeenIfRemovedBeforeThreshold() { - // GIVEN: Keyguard is showing, not dozing - keyguardRepository.setKeyguardShowing(true) - keyguardRepository.setIsDozing(false) - runKeyguardCoordinatorTest { - keyguardTransitionRepository.sendTransitionSteps( - from = KeyguardState.GONE, - to = KeyguardState.LOCKSCREEN, - this.testScheduler, - ) - testScheduler.runCurrent() - - // WHEN: a new notification is posted - val entry = NotificationEntryBuilder().setId(1).build() - collectionListener.onEntryAdded(entry) - testScheduler.runCurrent() - - // WHEN: one second has passed - testScheduler.advanceTimeBy(1.seconds) - testScheduler.runCurrent() - - // WHEN: the notification is removed - collectionListener.onEntryRemoved(entry, 0) - testScheduler.runCurrent() - - // WHEN: the notification is re-posted - collectionListener.onEntryAdded(entry) - testScheduler.runCurrent() - - // WHEN: one more second has passed - testScheduler.advanceTimeBy(1.seconds) - testScheduler.runCurrent() - - // WHEN: the keyguard is no longer showing - keyguardRepository.setKeyguardShowing(false) - keyguardTransitionRepository.sendTransitionSteps( - from = KeyguardState.LOCKSCREEN, - to = KeyguardState.GONE, - this.testScheduler, - ) - testScheduler.runCurrent() - - // WHEN: Keyguard is shown again - keyguardRepository.setKeyguardShowing(true) - keyguardTransitionRepository.sendTransitionSteps( - from = KeyguardState.GONE, - to = KeyguardState.LOCKSCREEN, - this.testScheduler, - ) - testScheduler.runCurrent() - - // THEN: The notification is considered unseen and is not filtered out. - assertThat(unseenFilter.shouldFilterOut(entry, 0L)).isFalse() - } - } - - @Test - fun unseenNotificationOnKeyguardNotMarkedAsSeenIfUpdatedBeforeThreshold() { - // GIVEN: Keyguard is showing, not dozing - keyguardRepository.setKeyguardShowing(true) - keyguardRepository.setIsDozing(false) - runKeyguardCoordinatorTest { - keyguardTransitionRepository.sendTransitionSteps( - from = KeyguardState.GONE, - to = KeyguardState.LOCKSCREEN, - this.testScheduler, - ) - testScheduler.runCurrent() - - // WHEN: a new notification is posted - val entry = NotificationEntryBuilder().setId(1).build() - collectionListener.onEntryAdded(entry) - testScheduler.runCurrent() - - // WHEN: one second has passed - testScheduler.advanceTimeBy(1.seconds) - testScheduler.runCurrent() - - // WHEN: the notification is updated - collectionListener.onEntryUpdated(entry) - testScheduler.runCurrent() - - // WHEN: four more seconds have passed - testScheduler.advanceTimeBy(4.seconds) - testScheduler.runCurrent() - - // WHEN: the keyguard is no longer showing - keyguardRepository.setKeyguardShowing(false) - keyguardTransitionRepository.sendTransitionSteps( - from = KeyguardState.LOCKSCREEN, - to = KeyguardState.GONE, - this.testScheduler, - ) - testScheduler.runCurrent() - - // WHEN: Keyguard is shown again - keyguardRepository.setKeyguardShowing(true) - keyguardTransitionRepository.sendTransitionSteps( - from = KeyguardState.GONE, - to = KeyguardState.LOCKSCREEN, - this.testScheduler, - ) - testScheduler.runCurrent() - - // THEN: The notification is considered unseen and is not filtered out. - assertThat(unseenFilter.shouldFilterOut(entry, 0L)).isFalse() - } - } - - private fun runKeyguardCoordinatorTest( - testBlock: suspend KeyguardCoordinatorTestScope.() -> Unit - ) { - val testDispatcher = UnconfinedTestDispatcher() - val testScope = TestScope(testDispatcher) - val fakeSettings = - FakeSettings().apply { - putInt(Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, 1) - } - val seenNotificationsInteractor = - SeenNotificationsInteractor(ActiveNotificationListRepository()) - val keyguardCoordinator = - KeyguardCoordinator( - testDispatcher, - mock<DumpManager>(), - headsUpManager, - keyguardNotifVisibilityProvider, - keyguardRepository, - kosmos.keyguardTransitionInteractor, - KeyguardCoordinatorLogger(logcatLogBuffer()), - testScope.backgroundScope, - sectionHeaderVisibilityProvider, - fakeSettings, - seenNotificationsInteractor, - statusBarStateController, - ) - keyguardCoordinator.attach(notifPipeline) - testScope.runTest(dispatchTimeoutMs = 1.seconds.inWholeMilliseconds) { - KeyguardCoordinatorTestScope( - keyguardCoordinator, - testScope, - seenNotificationsInteractor, - fakeSettings, - ) - .testBlock() - } - } - - private inner class KeyguardCoordinatorTestScope( - private val keyguardCoordinator: KeyguardCoordinator, - private val scope: TestScope, - val seenNotificationsInteractor: SeenNotificationsInteractor, - private val fakeSettings: FakeSettings, - ) : CoroutineScope by scope { - val testScheduler: TestCoroutineScheduler - get() = scope.testScheduler - - val onStateChangeListener: Consumer<String> = withArgCaptor { - verify(keyguardNotifVisibilityProvider).addOnStateChangedListener(capture()) - } - - val unseenFilter: NotifFilter - get() = keyguardCoordinator.unseenNotifFilter - - val collectionListener: NotifCollectionListener = withArgCaptor { - verify(notifPipeline).addCollectionListener(capture()) - } - - val onHeadsUpChangedListener: OnHeadsUpChangedListener - get() = withArgCaptor { verify(headsUpManager).addListener(capture()) } - - val statusBarStateListener: StatusBarStateController.StateListener - get() = withArgCaptor { verify(statusBarStateController).addCallback(capture()) } - - var showOnlyUnseenNotifsOnKeyguardSetting: Boolean - get() = - fakeSettings.getIntForUser( - Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, - UserHandle.USER_CURRENT, - ) == 1 - set(value) { - fakeSettings.putIntForUser( - Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, - if (value) 1 else 2, - UserHandle.USER_CURRENT, - ) - } - } - - companion object { - @JvmStatic - @Parameters(name = "{0}") - fun getParams(): List<FlagsParameterization> { - return FlagsParameterization.allCombinationsOf().andSceneContainer() - } - } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java index a925ccfe174b..c9710370c2c2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java @@ -1197,6 +1197,26 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { @Test @EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME) + public void testGenerateHeadsUpAnimation_isSeenInShade_noAnimation() { + // GIVEN NSSL is ready for HUN animations + Consumer<Boolean> headsUpAnimatingAwayListener = mock(BooleanConsumer.class); + prepareStackScrollerForHunAnimations(headsUpAnimatingAwayListener); + + // Entry was seen in shade + NotificationEntry entry = mock(NotificationEntry.class); + when(entry.isSeenInShade()).thenReturn(true); + ExpandableNotificationRow row = mock(ExpandableNotificationRow.class); + when(row.getEntry()).thenReturn(entry); + + // WHEN we generate an add event + mStackScroller.generateHeadsUpAnimation(row, /* isHeadsUp = */ true); + + // THEN nothing happens + assertThat(mStackScroller.isAddOrRemoveAnimationPending()).isFalse(); + } + + @Test + @EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME) public void testOnChildAnimationsFinished_resetsheadsUpAnimatingAway() { // GIVEN NSSL is ready for HUN animations Consumer<Boolean> headsUpAnimatingAwayListener = mock(BooleanConsumer.class); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImplTest.java index fea0e72fe577..8dfbb37f8189 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImplTest.java @@ -118,8 +118,8 @@ public class LegacyNotificationIconAreaControllerImplTest extends SysuiTestCase } @Test + @DisableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void testAppearResetsTranslation() { - mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT); mController.setupAodIcons(mAodIcons); when(mDozeParameters.shouldControlScreenOff()).thenReturn(false); mController.appearAodIcons(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java index 49e3f04cb44e..31f93b402a75 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java @@ -1059,6 +1059,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { @Test @DisableSceneContainer + @DisableFlags(Flags.FLAG_SIM_PIN_RACE_CONDITION_ON_RESTART) public void testShowBouncerOrKeyguard_needsFullScreen() { when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn( KeyguardSecurityModel.SecurityMode.SimPin); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java index 01540e7584a3..58ad83546e01 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java @@ -536,7 +536,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { // WHEN there's *no* ongoing activity via new callback mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged( - /* hasOngoingActivity= */ false); + /* hasOngoingActivity= */ false, /* shouldAnimate= */ false); // THEN the old callback value is used, so the view is shown assertEquals(View.VISIBLE, @@ -548,7 +548,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { // WHEN there *is* an ongoing activity via new callback mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged( - /* hasOngoingActivity= */ true); + /* hasOngoingActivity= */ true, /* shouldAnimate= */ false); // THEN the old callback value is used, so the view is hidden assertEquals(View.GONE, @@ -565,7 +565,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { // listener, but I'm unable to get the fragment to get attached so that the binder starts // listening to flows. mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged( - /* hasOngoingActivity= */ false); + /* hasOngoingActivity= */ false, /* shouldAnimate= */ false); assertEquals(View.GONE, mFragment.getView().findViewById(R.id.ongoing_activity_chip).getVisibility()); @@ -577,7 +577,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { resumeAndGetFragment(); mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged( - /* hasOngoingActivity= */ true); + /* hasOngoingActivity= */ true, /* shouldAnimate= */ false); assertEquals(View.VISIBLE, mFragment.getView().findViewById(R.id.ongoing_activity_chip).getVisibility()); @@ -590,7 +590,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged( - /* hasOngoingActivity= */ true); + /* hasOngoingActivity= */ true, /* shouldAnimate= */ false); fragment.disable(DEFAULT_DISPLAY, StatusBarManager.DISABLE_NOTIFICATION_ICONS, 0, false); @@ -605,7 +605,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged( - /* hasOngoingActivity= */ true); + /* hasOngoingActivity= */ true, /* shouldAnimate= */ false); when(mHeadsUpAppearanceController.shouldBeVisible()).thenReturn(true); fragment.disable(DEFAULT_DISPLAY, 0, 0, false); @@ -621,14 +621,14 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { // Ongoing activity started mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged( - /* hasOngoingActivity= */ true); + /* hasOngoingActivity= */ true, /* shouldAnimate= */ false); assertEquals(View.VISIBLE, mFragment.getView().findViewById(R.id.ongoing_activity_chip).getVisibility()); // Ongoing activity ended mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged( - /* hasOngoingActivity= */ false); + /* hasOngoingActivity= */ false, /* shouldAnimate= */ false); assertEquals(View.GONE, mFragment.getView().findViewById(R.id.ongoing_activity_chip).getVisibility()); @@ -643,7 +643,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { // Ongoing call started mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged( - /* hasOngoingActivity= */ true); + /* hasOngoingActivity= */ true, /* shouldAnimate= */ false); // Notification area is hidden without delay assertEquals(0f, getNotificationAreaView().getAlpha(), 0.01); @@ -661,7 +661,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { // WHEN there's *no* ongoing activity via new callback mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged( - /* hasOngoingActivity= */ false); + /* hasOngoingActivity= */ false, /* shouldAnimate= */ false); // THEN the new callback value is used, so the view is hidden assertEquals(View.GONE, @@ -673,7 +673,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { // WHEN there *is* an ongoing activity via new callback mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged( - /* hasOngoingActivity= */ true); + /* hasOngoingActivity= */ true, /* shouldAnimate= */ false); // THEN the new callback value is used, so the view is shown assertEquals(View.VISIBLE, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt index 94159bcebf47..60750cf96e67 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt @@ -425,7 +425,7 @@ class CollapsedStatusBarViewModelImplTest : SysuiTestCase() { kosmos.screenRecordRepository.screenRecordState.value = ScreenRecordModel.DoingNothing - assertThat(latest).isEqualTo(OngoingActivityChipModel.Hidden) + assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java) kosmos.fakeMediaProjectionRepository.mediaProjectionState.value = MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeCollapsedStatusBarViewModel.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeCollapsedStatusBarViewModel.kt index d3f11253fc09..cefdf7e43fae 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeCollapsedStatusBarViewModel.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeCollapsedStatusBarViewModel.kt @@ -29,7 +29,7 @@ class FakeCollapsedStatusBarViewModel : CollapsedStatusBarViewModel { override val transitionFromLockscreenToDreamStartedEvent = MutableSharedFlow<Unit>() override val ongoingActivityChip: MutableStateFlow<OngoingActivityChipModel> = - MutableStateFlow(OngoingActivityChipModel.Hidden) + MutableStateFlow(OngoingActivityChipModel.Hidden()) override val isHomeStatusBarAllowedByScene = MutableStateFlow(false) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt index cc2ef53c6cdb..12cfdcfa8df5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt @@ -35,7 +35,6 @@ import com.android.systemui.statusbar.domain.interactor.keyguardStatusBarInterac import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository -import com.android.systemui.statusbar.notification.stack.data.repository.setNotifications import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor import com.android.systemui.statusbar.policy.BatteryController import com.android.systemui.statusbar.policy.batteryController diff --git a/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/FakeMotionEvent.kt b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/FakeMotionEvent.kt index f40282f0da69..a8271fec0963 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/FakeMotionEvent.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/FakeMotionEvent.kt @@ -21,10 +21,9 @@ import android.view.InputDevice.SOURCE_MOUSE import android.view.MotionEvent import android.view.MotionEvent.CLASSIFICATION_NONE import android.view.MotionEvent.TOOL_TYPE_FINGER -import java.lang.reflect.Method -import org.mockito.kotlin.doNothing +import org.mockito.AdditionalAnswers +import org.mockito.Mockito.mock import org.mockito.kotlin.doReturn -import org.mockito.kotlin.spy import org.mockito.kotlin.whenever fun motionEvent( @@ -40,31 +39,18 @@ fun motionEvent( val event = MotionEvent.obtain(/* downTime= */ 0, /* eventTime= */ 0, action, x, y, /* metaState= */ 0) event.source = source - val spy = - spy<MotionEvent>(event) { - on { getToolType(0) } doReturn toolType - on { getPointerCount() } doReturn pointerCount - axisValues.forEach { (key, value) -> on { getAxisValue(key) } doReturn value } - on { getClassification() } doReturn classification - } - ensureFinalizeIsNotCalledTwice(spy) - return spy -} - -private fun ensureFinalizeIsNotCalledTwice(spy: MotionEvent) { - // Spy in mockito will create copy of the spied object, copying all its field etc. Here it means - // we create copy of MotionEvent and its mNativePtr, so we have two separate objects of type - // MotionEvents with the same mNativePtr. That breaks because MotionEvent has custom finalize() - // method which goes to native code and tries to delete the reference from mNativePtr. It works - // first time but second time reference is already deleted and it breaks. That's why we have to - // avoid calling finalize twice - doNothing().whenever(spy).finalizeUsingReflection() -} - -private fun MotionEvent.finalizeUsingReflection() { - val finalizeMethod: Method = MotionEvent::class.java.getDeclaredMethod("finalize") - finalizeMethod.isAccessible = true - finalizeMethod.invoke(this) + // we need to use mock with delegation instead of spy because: + // 1. Spy will try to deallocate the same memory again when finalize() is called as it keep the + // same memory pointer to native MotionEvent + // 2. Even after workaround for issue above there still remains problem with destructor of + // native event trying to free the same chunk of native memory. I'm not sure why it happens but + // mock seems to fix the issue and because it delegates all calls seems safer overall + val delegate = mock(MotionEvent::class.java, AdditionalAnswers.delegatesTo<MotionEvent>(event)) + doReturn(toolType).whenever(delegate).getToolType(0) + doReturn(pointerCount).whenever(delegate).pointerCount + doReturn(classification).whenever(delegate).classification + axisValues.forEach { (key, value) -> doReturn(value).whenever(delegate).getAxisValue(key) } + return delegate } fun touchpadEvent( diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java index f7371487a7c5..3f5dc82ec5cb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java @@ -18,6 +18,8 @@ package com.android.systemui.volume; import static android.app.ActivityManager.LOCK_TASK_MODE_NONE; +import static com.google.common.truth.Truth.assertThat; + import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -37,16 +39,19 @@ import android.media.IAudioService; import android.media.session.MediaSession; import android.os.Handler; import android.os.Process; +import android.platform.test.annotations.EnableFlags; import android.testing.TestableLooper; import android.view.accessibility.AccessibilityManager; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.settingslib.flags.Flags; import com.android.systemui.SysuiTestCase; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dump.DumpManager; import com.android.systemui.keyguard.WakefulnessLifecycle; +import com.android.systemui.plugins.VolumeDialogController; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.util.RingerModeLiveData; @@ -54,7 +59,9 @@ import com.android.systemui.util.RingerModeTracker; import com.android.systemui.util.concurrency.FakeExecutor; import com.android.systemui.util.concurrency.FakeThreadFactory; import com.android.systemui.util.concurrency.ThreadFactory; +import com.android.systemui.util.kotlin.JavaAdapter; import com.android.systemui.util.time.FakeSystemClock; +import com.android.systemui.volume.domain.interactor.AudioSharingInteractor; import org.junit.Before; import org.junit.Test; @@ -63,6 +70,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.Objects; import java.util.concurrent.Executor; @RunWith(AndroidJUnit4.class) @@ -104,6 +112,10 @@ public class VolumeDialogControllerImplTest extends SysuiTestCase { private UserTracker mUserTracker; @Mock private DumpManager mDumpManager; + @Mock + private AudioSharingInteractor mAudioSharingInteractor; + @Mock + private JavaAdapter mJavaAdapter; @Before @@ -124,11 +136,26 @@ public class VolumeDialogControllerImplTest extends SysuiTestCase { mCallback = mock(VolumeDialogControllerImpl.C.class); mThreadFactory.setLooper(TestableLooper.get(this).getLooper()); - mVolumeController = new TestableVolumeDialogControllerImpl(mContext, - mBroadcastDispatcher, mRingerModeTracker, mThreadFactory, mAudioManager, - mNotificationManager, mVibrator, mIAudioService, mAccessibilityManager, - mPackageManager, mWakefullnessLifcycle, mKeyguardManager, - mActivityManager, mUserTracker, mDumpManager, mCallback); + mVolumeController = + new TestableVolumeDialogControllerImpl( + mContext, + mBroadcastDispatcher, + mRingerModeTracker, + mThreadFactory, + mAudioManager, + mNotificationManager, + mVibrator, + mIAudioService, + mAccessibilityManager, + mPackageManager, + mWakefullnessLifcycle, + mKeyguardManager, + mActivityManager, + mUserTracker, + mDumpManager, + mCallback, + mAudioSharingInteractor, + mJavaAdapter); mVolumeController.setEnableDialogs(true, true); } @@ -224,6 +251,41 @@ public class VolumeDialogControllerImplTest extends SysuiTestCase { verify(mUserTracker).addCallback(any(UserTracker.Callback.class), any(Executor.class)); } + @Test + @EnableFlags(Flags.FLAG_VOLUME_DIALOG_AUDIO_SHARING_FIX) + public void handleAudioSharingStreamVolumeChanges_updateState() { + ArgumentCaptor<VolumeDialogController.State> stateCaptor = + ArgumentCaptor.forClass(VolumeDialogController.State.class); + int broadcastStream = VolumeDialogControllerImpl.DYNAMIC_STREAM_BROADCAST; + + mVolumeController.handleAudioSharingStreamVolumeChanges(100); + + verify(mCallback).onStateChanged(stateCaptor.capture()); + assertThat(stateCaptor.getValue().states.contains(broadcastStream)).isTrue(); + assertThat(stateCaptor.getValue().states.get(broadcastStream).level).isEqualTo(100); + + mVolumeController.handleAudioSharingStreamVolumeChanges(200); + + verify(mCallback, times(2)).onStateChanged(stateCaptor.capture()); + assertThat(stateCaptor.getValue().states.contains(broadcastStream)).isTrue(); + assertThat(stateCaptor.getValue().states.get(broadcastStream).level).isEqualTo(200); + + mVolumeController.handleAudioSharingStreamVolumeChanges(null); + + verify(mCallback, times(3)).onStateChanged(stateCaptor.capture()); + assertThat(stateCaptor.getValue().states.contains(broadcastStream)).isFalse(); + } + + @Test + @EnableFlags(Flags.FLAG_VOLUME_DIALOG_AUDIO_SHARING_FIX) + public void testSetStreamVolume_setSecondaryDeviceVolume() { + mVolumeController.setStreamVolume( + VolumeDialogControllerImpl.DYNAMIC_STREAM_BROADCAST, /* level= */ 100); + Objects.requireNonNull(TestableLooper.get(this)).processAllMessages(); + + verify(mAudioSharingInteractor).setStreamVolume(100); + } + static class TestableVolumeDialogControllerImpl extends VolumeDialogControllerImpl { private final WakefulnessLifecycle.Observer mWakefullessLifecycleObserver; @@ -243,11 +305,27 @@ public class VolumeDialogControllerImplTest extends SysuiTestCase { ActivityManager activityManager, UserTracker userTracker, DumpManager dumpManager, - C callback) { - super(context, broadcastDispatcher, ringerModeTracker, theadFactory, audioManager, - notificationManager, optionalVibrator, iAudioService, accessibilityManager, - packageManager, wakefulnessLifecycle, keyguardManager, - activityManager, userTracker, dumpManager); + C callback, + AudioSharingInteractor audioSharingInteractor, + JavaAdapter javaAdapter) { + super( + context, + broadcastDispatcher, + ringerModeTracker, + theadFactory, + audioManager, + notificationManager, + optionalVibrator, + iAudioService, + accessibilityManager, + packageManager, + wakefulnessLifecycle, + keyguardManager, + activityManager, + userTracker, + dumpManager, + audioSharingInteractor, + javaAdapter); mCallbacks = callback; ArgumentCaptor<WakefulnessLifecycle.Observer> observerCaptor = diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java index cdfcca6c7065..b5cbf598bd05 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java @@ -23,6 +23,7 @@ import static android.media.AudioManager.RINGER_MODE_VIBRATE; import static com.android.systemui.Flags.FLAG_HAPTIC_VOLUME_SLIDER; import static com.android.systemui.volume.Events.DISMISS_REASON_UNKNOWN; import static com.android.systemui.volume.Events.SHOW_REASON_UNKNOWN; +import static com.android.systemui.volume.VolumeDialogControllerImpl.DYNAMIC_STREAM_BROADCAST; import static com.android.systemui.volume.VolumeDialogControllerImpl.STREAMS; import static junit.framework.Assert.assertEquals; @@ -72,6 +73,7 @@ import androidx.test.filters.SmallTest; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.testing.UiEventLoggerFake; +import com.android.settingslib.flags.Flags; import com.android.systemui.Prefs; import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.AnimatorTestRule; @@ -794,6 +796,38 @@ public class VolumeDialogImplTest extends SysuiTestCase { verify(mVolumeDialogInteractor, atLeastOnce()).onDialogDismissed(); // dismiss by timeout } + @Test + @EnableFlags(Flags.FLAG_VOLUME_DIALOG_AUDIO_SHARING_FIX) + public void testDynamicStreamForBroadcast_createRow() { + State state = createShellState(); + VolumeDialogController.StreamState ss = new VolumeDialogController.StreamState(); + ss.dynamic = true; + ss.levelMin = 0; + ss.levelMax = 255; + ss.level = 20; + ss.name = -1; + ss.remoteLabel = mContext.getString(R.string.audio_sharing_description); + state.states.append(DYNAMIC_STREAM_BROADCAST, ss); + + mDialog.onStateChangedH(state); + mTestableLooper.processAllMessages(); + + ViewGroup volumeDialogRows = mDialog.getDialogView().findViewById(R.id.volume_dialog_rows); + assumeNotNull(volumeDialogRows); + View broadcastRow = null; + final int rowCount = volumeDialogRows.getChildCount(); + // we don't make assumptions about the position of the dnd row + for (int i = 0; i < rowCount; i++) { + View volumeRow = volumeDialogRows.getChildAt(i); + if (volumeRow.getId() == DYNAMIC_STREAM_BROADCAST) { + broadcastRow = volumeRow; + break; + } + } + assertNotNull(broadcastRow); + assertEquals(broadcastRow.getVisibility(), View.VISIBLE); + } + /** * @return true if at least one volume row has the DND icon */ diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java index ac42319c7b25..60b5b5d39b9b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java @@ -86,6 +86,7 @@ import android.service.dreams.IDreamManager; import android.service.notification.NotificationListenerService; import android.service.notification.ZenModeConfig; import android.testing.TestableLooper; +import android.util.Log; import android.util.Pair; import android.util.SparseArray; import android.view.Display; @@ -183,6 +184,7 @@ import com.android.wm.shell.common.bubbles.BubbleBarLocation; import com.android.wm.shell.common.bubbles.BubbleBarUpdate; import com.android.wm.shell.draganddrop.DragAndDropController; import com.android.wm.shell.onehanded.OneHandedController; +import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; @@ -192,7 +194,6 @@ import com.android.wm.shell.transition.Transitions; import org.junit.After; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -216,6 +217,9 @@ import platform.test.runner.parameterized.Parameters; @RunWith(ParameterizedAndroidJunit4.class) @TestableLooper.RunWithLooper(setAsMainLooper = true) public class BubblesTest extends SysuiTestCase { + + private static final String TAG = "BubblesTest"; + @Mock private CommonNotifCollection mCommonNotifCollection; @Mock @@ -241,8 +245,6 @@ public class BubblesTest extends SysuiTestCase { @Mock private KeyguardBypassController mKeyguardBypassController; @Mock - private FloatingContentCoordinator mFloatingContentCoordinator; - @Mock private BubbleDataRepository mDataRepository; @Mock private NotificationShadeWindowView mNotificationShadeWindowView; @@ -372,6 +374,7 @@ public class BubblesTest extends SysuiTestCase { @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); + PhysicsAnimatorTestUtils.prepareForTest(); if (Transitions.ENABLE_SHELL_TRANSITIONS) { doReturn(true).when(mTransitions).isRegistered(); @@ -494,7 +497,7 @@ public class BubblesTest extends SysuiTestCase { mShellCommandHandler, mShellController, mBubbleData, - mFloatingContentCoordinator, + new FloatingContentCoordinator(), mDataRepository, mStatusBarService, mWindowManager, @@ -571,12 +574,32 @@ public class BubblesTest extends SysuiTestCase { } @After - public void tearDown() { + public void tearDown() throws Exception { ArrayList<Bubble> bubbles = new ArrayList<>(mBubbleData.getBubbles()); for (int i = 0; i < bubbles.size(); i++) { mBubbleController.removeBubble(bubbles.get(i).getKey(), Bubbles.DISMISS_NO_LONGER_BUBBLE); } + mTestableLooper.processAllMessages(); + + // check that no animations are running before finishing the test to make sure that the + // state gets cleaned up correctly between tests. + int retryCount = 0; + while (PhysicsAnimatorTestUtils.isAnyAnimationRunning() && retryCount <= 10) { + Log.d( + TAG, + String.format("waiting for animations to complete. attempt %d", retryCount)); + // post a message to the looper and wait for it to be processed + mTestableLooper.runWithLooper(() -> {}); + retryCount++; + } + mTestableLooper.processAllMessages(); + if (PhysicsAnimatorTestUtils.isAnyAnimationRunning()) { + Log.d(TAG, "finished waiting for animations to complete but animations are still " + + "running"); + } else { + Log.d(TAG, "no animations are running"); + } } @Test @@ -1853,7 +1876,6 @@ public class BubblesTest extends SysuiTestCase { any(Bubble.class), anyBoolean(), anyBoolean()); } - @Ignore("reason = b/351977103") @Test public void testShowStackEdu_isNotConversationBubble() { // Setup diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalSceneTransitionRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalSceneTransitionRepositoryKosmos.kt new file mode 100644 index 000000000000..2050437da38a --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalSceneTransitionRepositoryKosmos.kt @@ -0,0 +1,22 @@ +/* + * 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.data.repository + +import com.android.systemui.kosmos.Kosmos + +val Kosmos.communalSceneTransitionRepository: CommunalSceneTransitionRepository by + Kosmos.Fixture { CommunalSceneTransitionRepository() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalPrefsRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalPrefsRepository.kt index 1da1fb2ee52f..5e870b19681b 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalPrefsRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalPrefsRepository.kt @@ -25,20 +25,11 @@ import kotlinx.coroutines.flow.map /** Fake implementation of [CommunalPrefsRepository] */ class FakeCommunalPrefsRepository : CommunalPrefsRepository { private val _isCtaDismissed = MutableStateFlow<Set<UserInfo>>(emptySet()) - private val _isDisclaimerDismissed = MutableStateFlow<Set<UserInfo>>(emptySet()) override fun isCtaDismissed(user: UserInfo): Flow<Boolean> = _isCtaDismissed.map { it.contains(user) } - override fun isDisclaimerDismissed(user: UserInfo): Flow<Boolean> = - _isDisclaimerDismissed.map { it.contains(user) } - override suspend fun setCtaDismissed(user: UserInfo) { _isCtaDismissed.value = _isCtaDismissed.value.toMutableSet().apply { add(user) } } - - override suspend fun setDisclaimerDismissed(user: UserInfo) { - _isDisclaimerDismissed.value = - _isDisclaimerDismissed.value.toMutableSet().apply { add(user) } - } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalSceneRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalSceneRepository.kt index d280be216f5e..82454817ecbb 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalSceneRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalSceneRepository.kt @@ -6,7 +6,6 @@ import com.android.compose.animation.scene.TransitionKey import com.android.systemui.communal.shared.model.CommunalScenes import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -25,11 +24,10 @@ class FakeCommunalSceneRepository( ) : CommunalSceneRepository { override fun changeScene(toScene: SceneKey, transitionKey: TransitionKey?) = - snapToScene(toScene, 0) + snapToScene(toScene) - override fun snapToScene(toScene: SceneKey, delayMillis: Long) { + override fun snapToScene(toScene: SceneKey) { applicationScope.launch { - delay(delayMillis) currentScene.value = toScene _transitionState.value = flowOf(ObservableTransitionState.Idle(toScene)) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt index eb9278537db5..4ad046cc095e 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt @@ -31,6 +31,7 @@ import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope import com.android.systemui.log.logcatLogBuffer import com.android.systemui.plugins.activityStarter import com.android.systemui.scene.domain.interactor.sceneInteractor @@ -42,6 +43,7 @@ val Kosmos.communalInteractor by Fixture { CommunalInteractor( applicationScope = applicationCoroutineScope, bgDispatcher = testDispatcher, + bgScope = testScope.backgroundScope, broadcastDispatcher = broadcastDispatcher, communalSceneInteractor = communalSceneInteractor, widgetRepository = communalWidgetRepository, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorKosmos.kt new file mode 100644 index 000000000000..e6e59e1a523e --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorKosmos.kt @@ -0,0 +1,37 @@ +/* + * 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.domain.interactor + +import com.android.systemui.communal.data.repository.communalSceneTransitionRepository +import com.android.systemui.keyguard.domain.interactor.internalKeyguardTransitionInteractor +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor +import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope + +val Kosmos.communalSceneTransitionInteractor: CommunalSceneTransitionInteractor by + Kosmos.Fixture { + CommunalSceneTransitionInteractor( + applicationScope = applicationCoroutineScope, + transitionInteractor = keyguardTransitionInteractor, + internalTransitionInteractor = internalKeyguardTransitionInteractor, + settingsInteractor = communalSettingsInteractor, + sceneInteractor = communalSceneInteractor, + repository = communalSceneTransitionRepository, + keyguardInteractor = keyguardInteractor, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FakeDeviceEntryRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FakeDeviceEntryRepository.kt index 045bd5d286df..2dcd275f0103 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FakeDeviceEntryRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FakeDeviceEntryRepository.kt @@ -21,21 +21,32 @@ import dagger.Module import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow /** Fake implementation of [DeviceEntryRepository] */ @SysUISingleton class FakeDeviceEntryRepository @Inject constructor() : DeviceEntryRepository { - private var isLockscreenEnabled = true + + private val _isLockscreenEnabled = MutableStateFlow(true) + override val isLockscreenEnabled: StateFlow<Boolean> = _isLockscreenEnabled.asStateFlow() private val _isBypassEnabled = MutableStateFlow(false) override val isBypassEnabled: StateFlow<Boolean> = _isBypassEnabled + private var pendingLockscreenEnabled = _isLockscreenEnabled.value + override suspend fun isLockscreenEnabled(): Boolean { - return isLockscreenEnabled + _isLockscreenEnabled.value = pendingLockscreenEnabled + return isLockscreenEnabled.value } fun setLockscreenEnabled(isLockscreenEnabled: Boolean) { - this.isLockscreenEnabled = isLockscreenEnabled + _isLockscreenEnabled.value = isLockscreenEnabled + pendingLockscreenEnabled = _isLockscreenEnabled.value + } + + fun setPendingLockscreenEnabled(isLockscreenEnabled: Boolean) { + pendingLockscreenEnabled = isLockscreenEnabled } fun setBypassEnabled(isBypassEnabled: Boolean) { 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 f73f43dc53fe..edf4bcc238c0 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,7 +17,10 @@ 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(Instant.MIN)) } + Kosmos.Fixture { FakeContextualEducationRepository(fakeEduClock) } + +var Kosmos.fakeEduClock: Clock by Kosmos.Fixture { FakeEduClock(Instant.MIN) } 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 new file mode 100644 index 000000000000..5b2dc2b39e27 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractorKosmos.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.education.domain.interactor + +import com.android.systemui.education.data.repository.contextualEducationRepository +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testScope +import com.android.systemui.user.domain.interactor.selectedUserInteractor + +val Kosmos.contextualEducationInteractor by + Kosmos.Fixture { + ContextualEducationInteractor( + backgroundScope = testScope.backgroundScope, + repository = contextualEducationRepository, + selectedUserInteractor = selectedUserInteractor + ) + } 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 new file mode 100644 index 000000000000..8f84e0482b83 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.education.domain.interactor + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testScope + +var Kosmos.keyboardTouchpadEduStatsInteractor by + Kosmos.Fixture { + KeyboardTouchpadEduStatsInteractorImpl( + backgroundScope = testScope.backgroundScope, + contextualEducationInteractor = contextualEducationInteractor + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt index 126d85890531..4634a7fd009f 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt @@ -19,7 +19,7 @@ package com.android.systemui.keyguard.domain.interactor import android.service.dream.dreamManager import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.communal.domain.interactor.communalSceneInteractor -import com.android.systemui.deviceentry.data.repository.deviceEntryRepository +import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope @@ -41,7 +41,7 @@ var Kosmos.fromDozingTransitionInteractor by communalSceneInteractor = communalSceneInteractor, powerInteractor = powerInteractor, keyguardOcclusionInteractor = keyguardOcclusionInteractor, - deviceEntryRepository = deviceEntryRepository, + deviceEntryInteractor = deviceEntryInteractor, wakeToGoneInteractor = keyguardWakeDirectlyToGoneInteractor, dreamManager = dreamManager ) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelKosmos.kt index 3d85a4abbd68..c7dfd5cc93b9 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelKosmos.kt @@ -17,6 +17,8 @@ package com.android.systemui.statusbar.chips.casttootherdevice.ui.viewmodel import android.content.applicationContext +import com.android.systemui.animation.dialogTransitionAnimator +import com.android.systemui.animation.mockDialogTransitionAnimator import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.statusbar.chips.casttootherdevice.domain.interactor.mediaRouterChipInteractor @@ -34,6 +36,7 @@ val Kosmos.castToOtherDeviceChipViewModel: CastToOtherDeviceChipViewModel by mediaRouterChipInteractor = mediaRouterChipInteractor, systemClock = fakeSystemClock, endMediaProjectionDialogHelper = endMediaProjectionDialogHelper, + dialogTransitionAnimator = mockDialogTransitionAnimator, logger = statusBarChipsLogger, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperKosmos.kt index 1ed7a4702e2c..651a0f7639d8 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperKosmos.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.chips.mediaprojection.ui.view import android.content.packageManager +import com.android.systemui.animation.mockDialogTransitionAnimator import com.android.systemui.kosmos.Kosmos import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory @@ -24,6 +25,7 @@ val Kosmos.endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper by Kosmos.Fixture { EndMediaProjectionDialogHelper( dialogFactory = mockSystemUIDialogFactory, + dialogTransitionAnimator = mockDialogTransitionAnimator, packageManager = packageManager, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelKosmos.kt index e4bb1665a432..c2a6f7d91eb0 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelKosmos.kt @@ -17,10 +17,12 @@ package com.android.systemui.statusbar.chips.screenrecord.ui.viewmodel import android.content.applicationContext +import com.android.systemui.animation.mockDialogTransitionAnimator import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.statusbar.chips.mediaprojection.ui.view.endMediaProjectionDialogHelper import com.android.systemui.statusbar.chips.screenrecord.domain.interactor.screenRecordChipInteractor +import com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel.shareToAppChipViewModel import com.android.systemui.statusbar.chips.statusBarChipsLogger import com.android.systemui.util.time.fakeSystemClock @@ -30,7 +32,9 @@ val Kosmos.screenRecordChipViewModel: ScreenRecordChipViewModel by scope = applicationCoroutineScope, context = applicationContext, interactor = screenRecordChipInteractor, + shareToAppChipViewModel = shareToAppChipViewModel, endMediaProjectionDialogHelper = endMediaProjectionDialogHelper, + dialogTransitionAnimator = mockDialogTransitionAnimator, systemClock = fakeSystemClock, logger = statusBarChipsLogger, ) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelKosmos.kt index 8ed7f9684d86..0770009f9998 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelKosmos.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel import android.content.applicationContext +import com.android.systemui.animation.mockDialogTransitionAnimator import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.mediaProjectionChipInteractor @@ -32,6 +33,7 @@ val Kosmos.shareToAppChipViewModel: ShareToAppChipViewModel by mediaProjectionChipInteractor = mediaProjectionChipInteractor, systemClock = fakeSystemClock, endMediaProjectionDialogHelper = endMediaProjectionDialogHelper, + dialogTransitionAnimator = mockDialogTransitionAnimator, logger = statusBarChipsLogger, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/EntryUtil.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/EntryUtil.kt index da956ec67696..8b4de2bcc26f 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/EntryUtil.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/EntryUtil.kt @@ -22,13 +22,12 @@ package com.android.systemui.statusbar.notification.collection * The [modifier] function will be passed an instance of a NotificationEntryBuilder. Any * modifications made to the builder will be applied to the [entry]. */ -inline fun modifyEntry( - entry: NotificationEntry, +inline fun NotificationEntry.modifyEntry( crossinline modifier: NotificationEntryBuilder.() -> Unit ) { - val builder = NotificationEntryBuilder(entry) + val builder = NotificationEntryBuilder(this) modifier(builder) - builder.apply(entry) + builder.apply(this) } fun getAttachState(entry: ListEntry): ListAttachState { diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorKosmos.kt new file mode 100644 index 000000000000..77d97bb7cbe9 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorKosmos.kt @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.collection.coordinator + +import com.android.systemui.dump.dumpManager +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.plugins.statusbar.statusBarStateController +import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.statusbar.notification.domain.interactor.seenNotificationsInteractor +import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor +import com.android.systemui.util.settings.fakeSettings + +var Kosmos.lockScreenMinimalismCoordinator by + Kosmos.Fixture { + LockScreenMinimalismCoordinator( + bgDispatcher = testDispatcher, + dumpManager = dumpManager, + headsUpInteractor = headsUpNotificationInteractor, + logger = lockScreenMinimalismCoordinatorLogger, + scope = testScope.backgroundScope, + secureSettings = fakeSettings, + seenNotificationsInteractor = seenNotificationsInteractor, + statusBarStateController = statusBarStateController, + shadeInteractor = shadeInteractor, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLoggerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLoggerKosmos.kt new file mode 100644 index 000000000000..77aeb44e15eb --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLoggerKosmos.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.collection.coordinator + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.log.logcatLogBuffer + +val Kosmos.lockScreenMinimalismCoordinatorLogger by + Kosmos.Fixture { LockScreenMinimalismCoordinatorLogger(logcatLogBuffer()) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationRepositoryKosmos.kt index 492e87bbcac4..7e8f1a9115ea 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationRepositoryKosmos.kt @@ -22,14 +22,19 @@ import com.android.systemui.statusbar.notification.data.repository.HeadsUpReposi import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map val Kosmos.headsUpNotificationRepository by Fixture { FakeHeadsUpNotificationRepository() } class FakeHeadsUpNotificationRepository : HeadsUpRepository { override val isHeadsUpAnimatingAway: MutableStateFlow<Boolean> = MutableStateFlow(false) - override val topHeadsUpRow: Flow<HeadsUpRowRepository?> = MutableStateFlow(null) - override val activeHeadsUpRows: MutableStateFlow<Set<HeadsUpRowRepository>> = - MutableStateFlow(emptySet()) + + val orderedHeadsUpRows = MutableStateFlow(emptyList<HeadsUpRowRepository>()) + override val topHeadsUpRow: Flow<HeadsUpRowRepository?> = + orderedHeadsUpRows.map { it.firstOrNull() }.distinctUntilChanged() + override val activeHeadsUpRows: Flow<Set<HeadsUpRowRepository>> = + orderedHeadsUpRows.map { it.toSet() }.distinctUntilChanged() override fun setHeadsUpAnimatingAway(animatingAway: Boolean) { isHeadsUpAnimatingAway.value = animatingAway @@ -38,4 +43,12 @@ class FakeHeadsUpNotificationRepository : HeadsUpRepository { override fun snooze() { // do nothing } + + fun setNotifications(notifications: List<HeadsUpRowRepository>) { + this.orderedHeadsUpRows.value = notifications.toList() + } + + fun setNotifications(vararg notifications: HeadsUpRowRepository) { + this.orderedHeadsUpRows.value = notifications.toList() + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeDeviceProvisioningRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeDeviceProvisioningRepository.kt index 9247e889f8f9..e3176f1783e3 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeDeviceProvisioningRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeDeviceProvisioningRepository.kt @@ -26,9 +26,14 @@ import kotlinx.coroutines.flow.MutableStateFlow class FakeDeviceProvisioningRepository @Inject constructor() : DeviceProvisioningRepository { private val _isDeviceProvisioned = MutableStateFlow(true) override val isDeviceProvisioned: Flow<Boolean> = _isDeviceProvisioned + fun setDeviceProvisioned(isProvisioned: Boolean) { _isDeviceProvisioned.value = isProvisioned } + + override fun isDeviceProvisioned(): Boolean { + return _isDeviceProvisioned.value + } } @Module diff --git a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail01_Test.java b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail01_Test.java new file mode 100644 index 000000000000..db95fad2a3ad --- /dev/null +++ b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail01_Test.java @@ -0,0 +1,51 @@ +/* + * 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.ravenwoodtest.coretest.methodvalidation; + +import android.platform.test.ravenwood.RavenwoodRule; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.rules.RuleChain; +import org.junit.runner.RunWith; + +/** + * RavenwoodRule has a validator to ensure "test-looking" methods have valid JUnit annotations. + * This class contains tests for this validator. + */ +@RunWith(AndroidJUnit4.class) +public class RavenwoodTestMethodValidation_Fail01_Test { + private ExpectedException mThrown = ExpectedException.none(); + private final RavenwoodRule mRavenwood = new RavenwoodRule(); + + @Rule + public final RuleChain chain = RuleChain.outerRule(mThrown).around(mRavenwood); + + public RavenwoodTestMethodValidation_Fail01_Test() { + mThrown.expectMessage("Method setUp() doesn't have @Before"); + } + + @SuppressWarnings("JUnit4SetUpNotRun") + public void setUp() { + } + + @Test + public void testEmpty() { + } +} diff --git a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail02_Test.java b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail02_Test.java new file mode 100644 index 000000000000..ddc66c73a7c0 --- /dev/null +++ b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail02_Test.java @@ -0,0 +1,51 @@ +/* + * 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.ravenwoodtest.coretest.methodvalidation; + +import android.platform.test.ravenwood.RavenwoodRule; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.rules.RuleChain; +import org.junit.runner.RunWith; + +/** + * RavenwoodRule has a validator to ensure "test-looking" methods have valid JUnit annotations. + * This class contains tests for this validator. + */ +@RunWith(AndroidJUnit4.class) +public class RavenwoodTestMethodValidation_Fail02_Test { + private ExpectedException mThrown = ExpectedException.none(); + private final RavenwoodRule mRavenwood = new RavenwoodRule(); + + @Rule + public final RuleChain chain = RuleChain.outerRule(mThrown).around(mRavenwood); + + public RavenwoodTestMethodValidation_Fail02_Test() { + mThrown.expectMessage("Method tearDown() doesn't have @After"); + } + + @SuppressWarnings("JUnit4TearDownNotRun") + public void tearDown() { + } + + @Test + public void testEmpty() { + } +} diff --git a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail03_Test.java b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail03_Test.java new file mode 100644 index 000000000000..ec8e907dcdb3 --- /dev/null +++ b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail03_Test.java @@ -0,0 +1,51 @@ +/* + * 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.ravenwoodtest.coretest.methodvalidation; + +import android.platform.test.ravenwood.RavenwoodRule; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.rules.RuleChain; +import org.junit.runner.RunWith; + +/** + * RavenwoodRule has a validator to ensure "test-looking" methods have valid JUnit annotations. + * This class contains tests for this validator. + */ +@RunWith(AndroidJUnit4.class) +public class RavenwoodTestMethodValidation_Fail03_Test { + private ExpectedException mThrown = ExpectedException.none(); + private final RavenwoodRule mRavenwood = new RavenwoodRule(); + + @Rule + public final RuleChain chain = RuleChain.outerRule(mThrown).around(mRavenwood); + + public RavenwoodTestMethodValidation_Fail03_Test() { + mThrown.expectMessage("Method testFoo() doesn't have @Test"); + } + + @SuppressWarnings("JUnit4TestNotRun") + public void testFoo() { + } + + @Test + public void testEmpty() { + } +} diff --git a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_OkTest.java b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_OkTest.java new file mode 100644 index 000000000000..d952d07b3817 --- /dev/null +++ b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_OkTest.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ravenwoodtest.coretest.methodvalidation; + +import android.platform.test.ravenwood.RavenwoodRule; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * RavenwoodRule has a validator to ensure "test-looking" methods have valid JUnit annotations. + * This class contains tests for this validator. + */ +@RunWith(AndroidJUnit4.class) +public class RavenwoodTestMethodValidation_OkTest { + @Rule + public final RavenwoodRule mRavenwood = new RavenwoodRule(); + + @Before + public void setUp() { + } + + @Before + public void testSetUp() { + } + + @After + public void tearDown() { + } + + @After + public void testTearDown() { + } + + @Test + public void testEmpty() { + } +} diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java index 49e793fcbddf..4357f2b8660a 100644 --- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java +++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java @@ -33,14 +33,17 @@ import com.android.internal.os.RuntimeInit; import com.android.server.LocalServices; import org.junit.After; +import org.junit.AfterClass; import org.junit.Assert; import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.Description; import org.junit.runner.RunWith; import org.junit.runners.model.Statement; import java.io.PrintStream; +import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; @@ -230,6 +233,18 @@ public class RavenwoodRuleImpl { } } + /** + * @return if a method has any of annotations. + */ + private static boolean hasAnyAnnotations(Method m, Class<? extends Annotation>... annotations) { + for (var anno : annotations) { + if (m.getAnnotation(anno) != null) { + return true; + } + } + return false; + } + private static void validateTestAnnotations(Statement base, Description description, boolean enableOptionalValidation) { final var testClass = description.getTestClass(); @@ -239,13 +254,14 @@ public class RavenwoodRuleImpl { boolean hasErrors = false; for (Method m : collectMethods(testClass)) { if (Modifier.isPublic(m.getModifiers()) && m.getName().startsWith("test")) { - if (m.getAnnotation(Test.class) == null) { + if (!hasAnyAnnotations(m, Test.class, Before.class, After.class, + BeforeClass.class, AfterClass.class)) { message.append("\nMethod " + m.getName() + "() doesn't have @Test"); hasErrors = true; } } if ("setUp".equals(m.getName())) { - if (m.getAnnotation(Before.class) == null) { + if (!hasAnyAnnotations(m, Before.class)) { message.append("\nMethod " + m.getName() + "() doesn't have @Before"); hasErrors = true; } @@ -255,7 +271,7 @@ public class RavenwoodRuleImpl { } } if ("tearDown".equals(m.getName())) { - if (m.getAnnotation(After.class) == null) { + if (!hasAnyAnnotations(m, After.class)) { message.append("\nMethod " + m.getName() + "() doesn't have @After"); hasErrors = true; } diff --git a/services/accessibility/accessibility.aconfig b/services/accessibility/accessibility.aconfig index b4efae3a05e4..8e2e0ad76d15 100644 --- a/services/accessibility/accessibility.aconfig +++ b/services/accessibility/accessibility.aconfig @@ -114,6 +114,13 @@ flag { } flag { + name: "enable_magnification_follows_mouse" + namespace: "accessibility" + description: "Whether to enable mouse following for fullscreen magnification" + bug: "335494097" +} + +flag { name: "fix_drag_pointer_when_ending_drag" namespace: "accessibility" description: "Send the correct pointer id when transitioning from dragging to delegating states." diff --git a/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java b/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java index d7da2f0052d3..a5ec2ba2f267 100644 --- a/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java +++ b/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java @@ -804,7 +804,15 @@ public final class PresentationStatsEventLogger { + event.mSuggestionPresentedLastTimestampMs + " event.mFocusedVirtualAutofillId=" + event.mFocusedVirtualAutofillId + " event.mFieldFirstLength=" + event.mFieldFirstLength - + " event.mFieldLastLength=" + event.mFieldLastLength); + + " event.mFieldLastLength=" + event.mFieldLastLength + + " event.mViewFailedPriorToRefillCount=" + event.mViewFailedPriorToRefillCount + + " event.mViewFilledSuccessfullyOnRefillCount=" + + event.mViewFilledSuccessfullyOnRefillCount + + " event.mViewFailedOnRefillCount=" + event.mViewFailedOnRefillCount + + " event.notExpiringResponseDuringAuthCount=" + + event.mFixExpireResponseDuringAuthCount + + " event.notifyViewEnteredIgnoredDuringAuthCount=" + + event.mNotifyViewEnteredIgnoredDuringAuthCount); } // TODO(b/234185326): Distinguish empty responses from other no presentation reasons. @@ -859,7 +867,12 @@ public final class PresentationStatsEventLogger { event.mSuggestionPresentedLastTimestampMs, event.mFocusedVirtualAutofillId, event.mFieldFirstLength, - event.mFieldLastLength); + event.mFieldLastLength, + event.mViewFailedPriorToRefillCount, + event.mViewFilledSuccessfullyOnRefillCount, + event.mViewFailedOnRefillCount, + event.mFixExpireResponseDuringAuthCount, + event.mNotifyViewEnteredIgnoredDuringAuthCount); mEventInternal = Optional.empty(); } @@ -912,6 +925,12 @@ public final class PresentationStatsEventLogger { // uninitialized doesn't help much, as this would be non-zero only if callback is received. int mViewFillSuccessCount = 0; int mViewFilledButUnexpectedCount = 0; + int mViewFailedPriorToRefillCount = 0; + int mViewFailedOnRefillCount = 0; + int mViewFilledSuccessfullyOnRefillCount = 0; + + int mFixExpireResponseDuringAuthCount = 0; + int mNotifyViewEnteredIgnoredDuringAuthCount = 0; ArraySet<AutofillId> mAutofillIdsAttemptedAutofill; ArraySet<AutofillId> mAlreadyFilledAutofillIds = new ArraySet<>(); diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java index 7d9d660af536..ee7d0aef2189 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java @@ -28,9 +28,9 @@ import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_BLOCKED_ import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CAMERA; import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CLIPBOARD; import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_RECENTS; -import static android.content.pm.PackageManager.ACTION_REQUEST_PERMISSIONS; import static android.companion.virtualdevice.flags.Flags.virtualCameraServiceDiscovery; import static android.companion.virtualdevice.flags.Flags.intentInterceptionActionMatchingFix; +import static android.content.pm.PackageManager.ACTION_REQUEST_PERMISSIONS; import android.annotation.EnforcePermission; import android.annotation.NonNull; @@ -561,8 +561,8 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub private void sendPendingIntent(int displayId, PendingIntent pendingIntent) throws PendingIntent.CanceledException { final ActivityOptions options = ActivityOptions.makeBasic().setLaunchDisplayId(displayId); - options.setPendingIntentBackgroundActivityLaunchAllowed(true); - options.setPendingIntentBackgroundActivityLaunchAllowedByPermission(true); + options.setPendingIntentBackgroundActivityStartMode( + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS); pendingIntent.send( mContext, /* code= */ 0, diff --git a/services/core/java/com/android/server/am/AppErrors.java b/services/core/java/com/android/server/am/AppErrors.java index 061bcd740f6b..ee7033e4a437 100644 --- a/services/core/java/com/android/server/am/AppErrors.java +++ b/services/core/java/com/android/server/am/AppErrors.java @@ -47,6 +47,7 @@ import android.os.Message; import android.os.Process; import android.os.SystemClock; import android.os.UserHandle; +import android.os.UserManager; import android.provider.Settings; import android.util.ArrayMap; import android.util.ArraySet; @@ -63,6 +64,8 @@ import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto; import com.android.server.LocalServices; import com.android.server.PackageWatchdog; +import com.android.server.pm.UserManagerInternal; +import com.android.server.pm.UserManagerService; import com.android.server.usage.AppStandbyInternal; import com.android.server.wm.WindowProcessController; @@ -868,9 +871,6 @@ class AppErrors { private boolean handleAppCrashLSPB(ProcessRecord app, String reason, String shortMsg, String longMsg, String stackTrace, AppErrorDialog.Data data) { final long now = SystemClock.uptimeMillis(); - final boolean showBackground = Settings.Secure.getIntForUser(mContext.getContentResolver(), - Settings.Secure.ANR_SHOW_BACKGROUND, 0, - mService.mUserController.getCurrentUserId()) != 0; Long crashTime; Long crashTimePersistent; @@ -881,6 +881,8 @@ class AppErrors { final boolean persistent = app.isPersistent(); final WindowProcessController proc = app.getWindowProcessController(); final ProcessErrorStateRecord errState = app.mErrorState; + final boolean showBackground = Settings.Secure.getIntForUser(mContext.getContentResolver(), + Settings.Secure.ANR_SHOW_BACKGROUND, 0, getVisibleUserId(userId)) != 0; if (!app.isolated) { crashTime = mProcessCrashTimes.get(processName, uid); @@ -1000,9 +1002,6 @@ class AppErrors { void handleShowAppErrorUi(Message msg) { AppErrorDialog.Data data = (AppErrorDialog.Data) msg.obj; - boolean showBackground = Settings.Secure.getIntForUser(mContext.getContentResolver(), - Settings.Secure.ANR_SHOW_BACKGROUND, 0, - mService.mUserController.getCurrentUserId()) != 0; final int userId; synchronized (mProcLock) { @@ -1027,7 +1026,11 @@ class AppErrors { for (int profileId : mService.mUserController.getCurrentProfileIds()) { isBackground &= (userId != profileId); } - if (isBackground && !showBackground) { + int visibleUserId = getVisibleUserId(userId); + boolean isVisibleUser = isVisibleBackgroundUser(visibleUserId); + boolean showBackground = Settings.Secure.getIntForUser(mContext.getContentResolver(), + Settings.Secure.ANR_SHOW_BACKGROUND, 0, visibleUserId) != 0; + if (isBackground && !showBackground && !isVisibleUser) { Slog.w(TAG, "Skipping crash dialog of " + proc + ": background"); if (res != null) { res.set(AppErrorDialog.BACKGROUND_USER); @@ -1054,7 +1057,7 @@ class AppErrors { final long now = SystemClock.uptimeMillis(); final boolean shouldThottle = crashShowErrorTime != null && now < crashShowErrorTime + ActivityManagerConstants.MIN_CRASH_INTERVAL; - if ((mService.mAtmInternal.canShowErrorDialogs() || showBackground) + if ((mService.mAtmInternal.canShowErrorDialogs(visibleUserId) || showBackground) && !crashSilenced && !shouldThottle && (showFirstCrash || showFirstCrashDevOption || data.repeating)) { Slog.i(TAG, "Showing crash dialog for package " + packageName + " u" + userId); @@ -1103,10 +1106,10 @@ class AppErrors { return; } + int visibleUserId = getVisibleUserId(proc.userId); boolean showBackground = Settings.Secure.getIntForUser(mContext.getContentResolver(), - Settings.Secure.ANR_SHOW_BACKGROUND, 0, - mService.mUserController.getCurrentUserId()) != 0; - if (mService.mAtmInternal.canShowErrorDialogs() || showBackground) { + Settings.Secure.ANR_SHOW_BACKGROUND, 0, visibleUserId) != 0; + if (mService.mAtmInternal.canShowErrorDialogs(visibleUserId) || showBackground) { AnrController anrController = errState.getDialogController().getAnrController(); if (anrController == null) { errState.getDialogController().showAnrDialogs(data); @@ -1163,6 +1166,43 @@ class AppErrors { } /** + * Returns the user ID of the visible user associated with the error occurrence. + * + * <p>For most cases it will return the current foreground user ID, but on devices that + * {@link UserManager#isVisibleBackgroundUsersEnabled() support visible background users}, + * it will return the given app user ID passed as parameter. + * + * @param appUserId The user ID of the app where the error occurred. + * @return The ID of the visible user associated with the error. + */ + private int getVisibleUserId(int appUserId) { + if (!UserManager.isVisibleBackgroundUsersEnabled()) { + return mService.mUserController.getCurrentUserId(); + } + return appUserId; + } + + /** + * Checks if the given user is a visible background user, which is a full, background user + * assigned to secondary displays on the devices that have + * {@link UserManager#isVisibleBackgroundUsersEnabled() + * config_multiuserVisibleBackgroundUsers enabled} (for example, passenger users on + * automotive builds, using the display associated with their seats). + * + * @see UserManager#isUserVisible() + */ + private boolean isVisibleBackgroundUser(int userId) { + if (!UserManager.isVisibleBackgroundUsersEnabled()) { + return false; + } + boolean isForeground = mService.mUserController.getCurrentUserId() == userId; + boolean isProfile = UserManagerService.getInstance().isProfile(userId); + boolean isVisible = LocalServices.getService(UserManagerInternal.class) + .isUserVisible(userId); + return isVisible && !isForeground && !isProfile; + } + + /** * Information about a process that is currently marked as bad. */ static final class BadProcessInfo { diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java index 092ee16f3342..8df4e7702be8 100644 --- a/services/core/java/com/android/server/am/BatteryStatsService.java +++ b/services/core/java/com/android/server/am/BatteryStatsService.java @@ -672,6 +672,12 @@ public final class BatteryStatsService extends IBatteryStats.Stub BatteryConsumer.POWER_COMPONENT_FLASHLIGHT, Flags.streamlinedMiscBatteryStats()); + mStats.setPowerStatsCollectorEnabled(BatteryConsumer.POWER_COMPONENT_GNSS, + Flags.streamlinedMiscBatteryStats()); + mBatteryUsageStatsProvider.setPowerStatsExporterEnabled( + BatteryConsumer.POWER_COMPONENT_GNSS, + Flags.streamlinedMiscBatteryStats()); + mStats.setPowerStatsCollectorEnabled(BatteryConsumer.POWER_COMPONENT_CAMERA, Flags.streamlinedMiscBatteryStats()); mBatteryUsageStatsProvider.setPowerStatsExporterEnabled( @@ -1098,18 +1104,20 @@ public final class BatteryStatsService extends IBatteryStats.Stub final StatsManager statsManager = mContext.getSystemService(StatsManager.class); final StatsPullAtomCallbackImpl pullAtomCallback = new StatsPullAtomCallbackImpl(); - statsManager.setPullAtomCallback( - FrameworkStatsLog.BATTERY_USAGE_STATS_SINCE_RESET, - null, // use default PullAtomMetadata values - DIRECT_EXECUTOR, pullAtomCallback); - statsManager.setPullAtomCallback( - FrameworkStatsLog.BATTERY_USAGE_STATS_SINCE_RESET_USING_POWER_PROFILE_MODEL, - null, // use default PullAtomMetadata values - DIRECT_EXECUTOR, pullAtomCallback); - statsManager.setPullAtomCallback( - FrameworkStatsLog.BATTERY_USAGE_STATS_BEFORE_RESET, - null, // use default PullAtomMetadata values - DIRECT_EXECUTOR, pullAtomCallback); + if (!Flags.disableCompositeBatteryUsageStatsAtoms()) { + statsManager.setPullAtomCallback( + FrameworkStatsLog.BATTERY_USAGE_STATS_SINCE_RESET, + null, // use default PullAtomMetadata values + DIRECT_EXECUTOR, pullAtomCallback); + statsManager.setPullAtomCallback( + FrameworkStatsLog.BATTERY_USAGE_STATS_SINCE_RESET_USING_POWER_PROFILE_MODEL, + null, // use default PullAtomMetadata values + DIRECT_EXECUTOR, pullAtomCallback); + statsManager.setPullAtomCallback( + FrameworkStatsLog.BATTERY_USAGE_STATS_BEFORE_RESET, + null, // use default PullAtomMetadata values + DIRECT_EXECUTOR, pullAtomCallback); + } if (Flags.addBatteryUsageStatsSliceAtom()) { statsManager.setPullAtomCallback( FrameworkStatsLog.BATTERY_USAGE_STATS_PER_UID, @@ -1126,6 +1134,10 @@ public final class BatteryStatsService extends IBatteryStats.Stub final BatteryUsageStats bus; switch (atomTag) { case FrameworkStatsLog.BATTERY_USAGE_STATS_SINCE_RESET: { + if (Flags.disableCompositeBatteryUsageStatsAtoms()) { + return StatsManager.PULL_SKIP; + } + @SuppressLint("MissingPermission") final double minConsumedPowerThreshold = DeviceConfig.getFloat(DEVICE_CONFIG_NAMESPACE, @@ -1142,6 +1154,10 @@ public final class BatteryStatsService extends IBatteryStats.Stub break; } case FrameworkStatsLog.BATTERY_USAGE_STATS_SINCE_RESET_USING_POWER_PROFILE_MODEL: + if (Flags.disableCompositeBatteryUsageStatsAtoms()) { + return StatsManager.PULL_SKIP; + } + final BatteryUsageStatsQuery queryPowerProfile = new BatteryUsageStatsQuery.Builder() .setMaxStatsAgeMs(0) @@ -1153,6 +1169,10 @@ public final class BatteryStatsService extends IBatteryStats.Stub bus = getBatteryUsageStats(List.of(queryPowerProfile)).get(0); break; case FrameworkStatsLog.BATTERY_USAGE_STATS_BEFORE_RESET: { + if (Flags.disableCompositeBatteryUsageStatsAtoms()) { + return StatsManager.PULL_SKIP; + } + final long sessionStart = getLastBatteryUsageStatsBeforeResetAtomPullTimestamp(); final long sessionEnd; @@ -3252,6 +3272,9 @@ public final class BatteryStatsService extends IBatteryStats.Stub .setMaxStatsAgeMs(0) .includeProcessStateData() .includePowerModels(); + if (Flags.batteryUsageStatsByPowerAndScreenState()) { + builder.includeScreenStateData().includePowerStateData(); + } if (model == BatteryConsumer.POWER_MODEL_POWER_PROFILE) { builder.powerProfileModeledOnly(); } @@ -3270,7 +3293,7 @@ public final class BatteryStatsService extends IBatteryStats.Stub if (proto) { batteryUsageStats.dumpToProto(fd); } else { - batteryUsageStats.dump(pw, ""); + batteryUsageStats.dump(pw, " "); } } diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java index b058bd8eb3d6..504c54aefc44 100644 --- a/services/core/java/com/android/server/am/OomAdjuster.java +++ b/services/core/java/com/android/server/am/OomAdjuster.java @@ -1708,6 +1708,11 @@ public class OomAdjuster { // priority for this non-top split. schedGroup = SCHED_GROUP_TOP_APP; mAdjType = "resumed-split-screen-activity"; + } else if ((flags + & WindowProcessController.ACTIVITY_STATE_FLAG_PERCEPTIBLE_FREEFORM) != 0) { + // The recently used non-top visible freeform app. + schedGroup = SCHED_GROUP_TOP_APP; + mAdjType = "perceptible-freeform-activity"; } foregroundActivities = true; mHasVisibleActivities = true; diff --git a/services/core/java/com/android/server/app/GameManagerService.java b/services/core/java/com/android/server/app/GameManagerService.java index f6df60f736e8..e4c65bd2147d 100644 --- a/services/core/java/com/android/server/app/GameManagerService.java +++ b/services/core/java/com/android/server/app/GameManagerService.java @@ -269,7 +269,8 @@ public final class GameManagerService extends IGameManagerService.Stub { @Override public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, String[] args, ShellCallback callback, ResultReceiver result) { - new GameManagerShellCommand().exec(this, in, out, err, args, callback, result); + new GameManagerShellCommand(mPackageManager).exec(this, in, out, err, args, callback, + result); } @Override diff --git a/services/core/java/com/android/server/app/GameManagerShellCommand.java b/services/core/java/com/android/server/app/GameManagerShellCommand.java index ab57c4fe837e..d3b4312bf4a3 100644 --- a/services/core/java/com/android/server/app/GameManagerShellCommand.java +++ b/services/core/java/com/android/server/app/GameManagerShellCommand.java @@ -16,10 +16,13 @@ package com.android.server.app; +import android.annotation.NonNull; import android.app.ActivityManager; import android.app.GameManager; import android.app.IGameManagerService; import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; import android.os.RemoteException; import android.os.ServiceManager; import android.os.ServiceManager.ServiceNotFoundException; @@ -47,7 +50,10 @@ public class GameManagerShellCommand extends ShellCommand { private static final String UNSUPPORTED_MODE_NUM = String.valueOf( GameManager.GAME_MODE_UNSUPPORTED); - public GameManagerShellCommand() { + private PackageManager mPackageManager; + + public GameManagerShellCommand(@NonNull PackageManager packageManager) { + mPackageManager = packageManager; } @Override @@ -91,9 +97,29 @@ public class GameManagerShellCommand extends ShellCommand { return -1; } + private boolean isPackageGame(String packageName, int userId, PrintWriter pw) { + try { + final ApplicationInfo applicationInfo = mPackageManager + .getApplicationInfoAsUser(packageName, PackageManager.MATCH_ALL, userId); + boolean isGame = applicationInfo.category == ApplicationInfo.CATEGORY_GAME; + if (!isGame) { + pw.println("Package " + packageName + " is not of game type, to use the game " + + "mode commands, it must specify game category in the manifest as " + + "android:appCategory=\"game\""); + } + return isGame; + } catch (PackageManager.NameNotFoundException e) { + pw.println("Package " + packageName + " is not found for user " + userId); + return false; + } + } + private int runListGameModes(PrintWriter pw) throws ServiceNotFoundException, RemoteException { final String packageName = getNextArgRequired(); final int userId = ActivityManager.getCurrentUser(); + if (!isPackageGame(packageName, userId, pw)) { + return -1; + } final GameManagerService gameManagerService = (GameManagerService) ServiceManager.getService(Context.GAME_SERVICE); final String currentMode = gameModeIntToString( @@ -110,12 +136,15 @@ public class GameManagerShellCommand extends ShellCommand { private int runListGameModeConfigs(PrintWriter pw) throws ServiceNotFoundException, RemoteException { final String packageName = getNextArgRequired(); - + final int userId = ActivityManager.getCurrentUser(); + if (!isPackageGame(packageName, userId, pw)) { + return -1; + } final GameManagerService gameManagerService = (GameManagerService) ServiceManager.getService(Context.GAME_SERVICE); final String listStr = gameManagerService.getInterventionList(packageName, - ActivityManager.getCurrentUser()); + userId); if (listStr == null) { pw.println("No interventions found for " + packageName); @@ -131,15 +160,17 @@ public class GameManagerShellCommand extends ShellCommand { if (option != null && option.equals("--user")) { userIdStr = getNextArgRequired(); } - final String gameMode = getNextArgRequired(); final String packageName = getNextArgRequired(); + int userId = userIdStr != null ? Integer.parseInt(userIdStr) + : ActivityManager.getCurrentUser(); + if (!isPackageGame(packageName, userId, pw)) { + return -1; + } final IGameManagerService service = IGameManagerService.Stub.asInterface( ServiceManager.getServiceOrThrow(Context.GAME_SERVICE)); boolean batteryModeSupported = false; boolean perfModeSupported = false; - int userId = userIdStr != null ? Integer.parseInt(userIdStr) - : ActivityManager.getCurrentUser(); int[] modes = service.getAvailableGameModes(packageName, userId); for (int mode : modes) { if (mode == GameManager.GAME_MODE_PERFORMANCE) { @@ -262,6 +293,9 @@ public class GameManagerShellCommand extends ShellCommand { int userId = userIdStr != null ? Integer.parseInt(userIdStr) : ActivityManager.getCurrentUser(); + if (!isPackageGame(packageName, userId, pw)) { + return -1; + } final GameManagerService gameManagerService = (GameManagerService) ServiceManager.getService(Context.GAME_SERVICE); @@ -308,13 +342,14 @@ public class GameManagerShellCommand extends ShellCommand { } final String packageName = getNextArgRequired(); - - final GameManagerService gameManagerService = (GameManagerService) - ServiceManager.getService(Context.GAME_SERVICE); - int userId = userIdStr != null ? Integer.parseInt(userIdStr) : ActivityManager.getCurrentUser(); + if (!isPackageGame(packageName, userId, pw)) { + return -1; + } + final GameManagerService gameManagerService = (GameManagerService) + ServiceManager.getService(Context.GAME_SERVICE); if (gameMode == null) { gameManagerService.resetGameModeConfigOverride(packageName, userId, -1); return 0; diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java index 326ed69fa1dc..25b9228d3b37 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java +++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java @@ -1871,7 +1871,10 @@ public class AudioDeviceBroker { synchronized (mBluetoothAudioStateLock) { reapplyAudioHalBluetoothState(); } - mBtHelper.onAudioServerDiedRestoreA2dp(); + final int forceForMedia = getBluetoothA2dpEnabled() + ? AudioSystem.FORCE_NONE : AudioSystem.FORCE_NO_BT_A2DP; + setForceUse_Async( + AudioSystem.FOR_MEDIA, forceForMedia, "MSG_RESTORE_DEVICES"); updateCommunicationRoute("MSG_RESTORE_DEVICES"); } } diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java index 02aa6f52ba8a..ca69f31adb35 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java +++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java @@ -1757,6 +1757,15 @@ public class AudioDeviceInventory { if (AudioService.DEBUG_DEVICES) { Slog.i(TAG, "deviceInfo:" + di + " is(already)Connected:" + isConnected); } + // Do not report an error in case of redundant connect or disconnect request + // as this can cause a state mismatch between BtHelper and AudioDeviceInventory + if (connect == isConnected) { + Log.i(TAG, "handleDeviceConnection() deviceInfo=" + di + " is already " + + (connect ? "" : "dis") + "connected"); + mmi.set(MediaMetrics.Property.STATE, connect + ? MediaMetrics.Value.CONNECT : MediaMetrics.Value.DISCONNECT).record(); + return true; + } if (connect && !isConnected) { final int res; if (isForTesting) { diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index fab0a56af2a8..fe3bbb094284 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -739,6 +739,8 @@ public class AudioService extends IAudioService.Stub // Broadcast receiver for device connections intent broadcasts private final BroadcastReceiver mReceiver = new AudioServiceBroadcastReceiver(); + private final Executor mAudioServerLifecycleExecutor; + private IMediaProjectionManager mProjectionService; // to validate projection token /** Interface for UserManagerService. */ @@ -1059,7 +1061,8 @@ public class AudioService extends IAudioService.Stub audioserverPermissions() ? initializeAudioServerPermissionProvider( context, audioPolicyFacade, audioserverLifecycleExecutor) : - null + null, + audioserverLifecycleExecutor ); } @@ -1145,13 +1148,16 @@ public class AudioService extends IAudioService.Stub * {@link AudioSystemThread} is created as the messaging thread instead. * @param appOps {@link AppOpsManager} system service * @param enforcer Used for permission enforcing + * @param permissionProvider Used to push permissions to audioserver + * @param audioserverLifecycleExecutor Used for tasks managing audioserver lifecycle */ @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG) public AudioService(Context context, AudioSystemAdapter audioSystem, SystemServerAdapter systemServer, SettingsAdapter settings, AudioVolumeGroupHelperBase audioVolumeGroupHelper, AudioPolicyFacade audioPolicy, @Nullable Looper looper, AppOpsManager appOps, @NonNull PermissionEnforcer enforcer, - /* @NonNull */ AudioServerPermissionProvider permissionProvider) { + /* @NonNull */ AudioServerPermissionProvider permissionProvider, + Executor audioserverLifecycleExecutor) { super(enforcer); sLifecycleLogger.enqueue(new EventLogger.StringEvent("AudioService()")); mContext = context; @@ -1159,6 +1165,7 @@ public class AudioService extends IAudioService.Stub mAppOps = appOps; mPermissionProvider = permissionProvider; + mAudioServerLifecycleExecutor = audioserverLifecycleExecutor; mAudioSystem = audioSystem; mSystemServer = systemServer; @@ -1170,6 +1177,34 @@ public class AudioService extends IAudioService.Stub mBroadcastHandlerThread = new HandlerThread("AudioService Broadcast"); mBroadcastHandlerThread.start(); + // Listen to permission invalidations for the PermissionProvider + if (audioserverPermissions()) { + final Handler broadcastHandler = mBroadcastHandlerThread.getThreadHandler(); + mAudioSystem.listenForSystemPropertyChange(PermissionManager.CACHE_KEY_PACKAGE_INFO, + new Runnable() { + // Roughly chosen to be long enough to suppress the autocork behavior + // of the permission cache (50ms), and longer than the task could reasonably + // take, even with many packages and users, while not introducing visible + // permission leaks - since the app needs to restart, and trigger an action + // which requires permissions from audioserver before this delay. + // For RECORD_AUDIO, we are additionally protected by appops. + final long UPDATE_DELAY_MS = 110; + final AtomicLong scheduledUpdateTimestamp = new AtomicLong(0); + @Override + public void run() { + var currentTime = SystemClock.uptimeMillis(); + if (currentTime > scheduledUpdateTimestamp.get()) { + scheduledUpdateTimestamp.set(currentTime + UPDATE_DELAY_MS); + broadcastHandler.postAtTime( () -> + mAudioServerLifecycleExecutor.execute(mPermissionProvider + ::onPermissionStateChanged), + currentTime + UPDATE_DELAY_MS + ); + } + } + }); + } + mDeviceBroker = new AudioDeviceBroker(mContext, this, mAudioSystem); mIsSingleVolume = AudioSystem.isSingleVolume(context); @@ -9918,9 +9953,9 @@ public class AudioService extends IAudioService.Stub int a2dpDev = AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP; synchronized (mCachedAbsVolDrivingStreamsLock) { mCachedAbsVolDrivingStreams.compute(a2dpDev, (dev, stream) -> { - if (stream != null && !mAvrcpAbsVolSupported) { + if (!mAvrcpAbsVolSupported) { mAudioSystem.setDeviceAbsoluteVolumeEnabled(a2dpDev, /*address=*/ - "", /*enabled*/false, AudioSystem.DEVICE_NONE); + "", /*enabled*/false, AudioSystem.STREAM_DEFAULT); return null; } // For A2DP and AVRCP we need to set the driving stream based on the @@ -11974,29 +12009,6 @@ public class AudioService extends IAudioService.Stub provider.onServiceStart(audioPolicy.getPermissionController()); }); - // Set up event listeners - // Must be kept in sync with PermissionManager - Runnable cacheSysPropHandler = new Runnable() { - private AtomicReference<SystemProperties.Handle> mHandle = new AtomicReference(); - private AtomicLong mNonce = new AtomicLong(); - @Override - public void run() { - if (mHandle.get() == null) { - // Cache the handle - mHandle.compareAndSet(null, SystemProperties.find( - PermissionManager.CACHE_KEY_PACKAGE_INFO)); - } - long nonce; - SystemProperties.Handle ref; - if ((ref = mHandle.get()) != null && (nonce = ref.getLong(0)) != 0 && - mNonce.getAndSet(nonce) != nonce) { - audioserverExecutor.execute(() -> provider.onPermissionStateChanged()); - } - } - }; - - SystemProperties.addChangeCallback(cacheSysPropHandler); - IntentFilter packageUpdateFilter = new IntentFilter(); packageUpdateFilter.addAction(ACTION_PACKAGE_ADDED); packageUpdateFilter.addAction(ACTION_PACKAGE_REMOVED); diff --git a/services/core/java/com/android/server/audio/AudioSystemAdapter.java b/services/core/java/com/android/server/audio/AudioSystemAdapter.java index 7f4bc74bd59e..d083c68c4c2c 100644 --- a/services/core/java/com/android/server/audio/AudioSystemAdapter.java +++ b/services/core/java/com/android/server/audio/AudioSystemAdapter.java @@ -748,6 +748,10 @@ public class AudioSystemAdapter implements AudioSystem.RoutingUpdateCallback, return AudioSystem.setMasterMute(mute); } + public void listenForSystemPropertyChange(String systemPropertyName, Runnable callback) { + AudioSystem.listenForSystemPropertyChange(systemPropertyName, callback); + } + /** * Part of AudioService dump * @param pw diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java index 57bffa7e5b40..ce92dfbcc1c8 100644 --- a/services/core/java/com/android/server/audio/BtHelper.java +++ b/services/core/java/com/android/server/audio/BtHelper.java @@ -83,39 +83,53 @@ public class BtHelper { } // BluetoothHeadset API to control SCO connection + @GuardedBy("BtHelper.this") private @Nullable BluetoothHeadset mBluetoothHeadset; // Bluetooth headset device + @GuardedBy("mDeviceBroker.mDeviceStateLock") private @Nullable BluetoothDevice mBluetoothHeadsetDevice; + + @GuardedBy("mDeviceBroker.mDeviceStateLock") private final Map<BluetoothDevice, AudioDeviceAttributes> mResolvedScoAudioDevices = new HashMap<>(); + @GuardedBy("BtHelper.this") private @Nullable BluetoothHearingAid mHearingAid = null; + @GuardedBy("BtHelper.this") private @Nullable BluetoothLeAudio mLeAudio = null; + @GuardedBy("BtHelper.this") private @Nullable BluetoothLeAudioCodecConfig mLeAudioCodecConfig; // Reference to BluetoothA2dp to query for AbsoluteVolume. + @GuardedBy("BtHelper.this") private @Nullable BluetoothA2dp mA2dp = null; + @GuardedBy("BtHelper.this") private @Nullable BluetoothCodecConfig mA2dpCodecConfig; + @GuardedBy("BtHelper.this") private @AudioSystem.AudioFormatNativeEnumForBtCodec int mLeAudioBroadcastCodec = AudioSystem.AUDIO_FORMAT_DEFAULT; // If absolute volume is supported in AVRCP device + @GuardedBy("mDeviceBroker.mDeviceStateLock") private boolean mAvrcpAbsVolSupported = false; // Current connection state indicated by bluetooth headset + @GuardedBy("mDeviceBroker.mDeviceStateLock") private int mScoConnectionState; // Indicate if SCO audio connection is currently active and if the initiator is // audio service (internal) or bluetooth headset (external) + @GuardedBy("mDeviceBroker.mDeviceStateLock") private int mScoAudioState; // Indicates the mode used for SCO audio connection. The mode is virtual call if the request // originated from an app targeting an API version before JB MR2 and raw audio after that. + @GuardedBy("mDeviceBroker.mDeviceStateLock") private int mScoAudioMode; // SCO audio state is not active @@ -210,7 +224,7 @@ public class BtHelper { //---------------------------------------------------------------------- // Interface for AudioDeviceBroker - // Called locked by ADeviceBroker.mSetModeLock -> AudioDeviceBroker.mDeviceStateLock + @GuardedBy("mDeviceBroker.mDeviceStateLock") /*package*/ synchronized void onSystemReady() { mScoConnectionState = android.media.AudioManager.SCO_AUDIO_STATE_ERROR; resetBluetoothSco(); @@ -238,17 +252,13 @@ public class BtHelper { } } - /*package*/ synchronized void onAudioServerDiedRestoreA2dp() { - final int forMed = mDeviceBroker.getBluetoothA2dpEnabled() - ? AudioSystem.FORCE_NONE : AudioSystem.FORCE_NO_BT_A2DP; - mDeviceBroker.setForceUse_Async(AudioSystem.FOR_MEDIA, forMed, "onAudioServerDied()"); - } - - /*package*/ synchronized void setAvrcpAbsoluteVolumeSupported(boolean supported) { + @GuardedBy("mDeviceBroker.mDeviceStateLock") + /*package*/ void setAvrcpAbsoluteVolumeSupported(boolean supported) { mAvrcpAbsVolSupported = supported; Log.i(TAG, "setAvrcpAbsoluteVolumeSupported supported=" + supported); } + @GuardedBy("mDeviceBroker.mDeviceStateLock") /*package*/ synchronized void setAvrcpAbsoluteVolumeIndex(int index) { if (mA2dp == null) { if (AudioService.DEBUG_VOL) { @@ -371,7 +381,7 @@ public class BtHelper { return codecAndChanged; } - // Called locked by ADeviceBroker.mSetModeLock -> AudioDeviceBroker.mDeviceStateLock + @GuardedBy("mDeviceBroker.mDeviceStateLock") /*package*/ synchronized void onReceiveBtEvent(Intent intent) { final String action = intent.getAction(); @@ -393,12 +403,12 @@ public class BtHelper { } /** - * Exclusively called from AudioDeviceBroker (with mSetModeLock held) + * Exclusively called from AudioDeviceBroker (with mDeviceStateLock held) * when handling MSG_L_RECEIVED_BT_EVENT in {@link #onReceiveBtEvent(Intent)} * as part of the serialization of the communication route selection */ - @GuardedBy("BtHelper.this") - private void onScoAudioStateChanged(int state) { + @GuardedBy("mDeviceBroker.mDeviceStateLock") + private synchronized void onScoAudioStateChanged(int state) { boolean broadcast = false; int scoAudioState = AudioManager.SCO_AUDIO_STATE_ERROR; Log.i(TAG, "onScoAudioStateChanged state: " + state @@ -414,12 +424,14 @@ public class BtHelper { broadcast = true; } if (!mDeviceBroker.isScoManagedByAudio()) { - mDeviceBroker.setBluetoothScoOn(true, "BtHelper.onScoAudioStateChanged"); + mDeviceBroker.setBluetoothScoOn( + true, "BtHelper.onScoAudioStateChanged, state: " + state); } break; case BluetoothHeadset.STATE_AUDIO_DISCONNECTED: if (!mDeviceBroker.isScoManagedByAudio()) { - mDeviceBroker.setBluetoothScoOn(false, "BtHelper.onScoAudioStateChanged"); + mDeviceBroker.setBluetoothScoOn( + false, "BtHelper.onScoAudioStateChanged, state: " + state); } scoAudioState = AudioManager.SCO_AUDIO_STATE_DISCONNECTED; // There are two cases where we want to immediately reconnect audio: @@ -466,6 +478,7 @@ public class BtHelper { * * @return false if SCO isn't connected */ + @GuardedBy("mDeviceBroker.mDeviceStateLock") /*package*/ synchronized boolean isBluetoothScoOn() { if (mBluetoothHeadset == null || mBluetoothHeadsetDevice == null) { return false; @@ -479,19 +492,20 @@ public class BtHelper { return false; } - /*package*/ synchronized boolean isBluetoothScoRequestedInternally() { + @GuardedBy("mDeviceBroker.mDeviceStateLock") + /*package*/ boolean isBluetoothScoRequestedInternally() { return mScoAudioState == SCO_STATE_ACTIVE_INTERNAL || mScoAudioState == SCO_STATE_ACTIVATE_REQ; } - // Called locked by ADeviceBroker.mSetModeLock -> AudioDeviceBroker.mDeviceStateLock + @GuardedBy("mDeviceBroker.mDeviceStateLock") /*package*/ synchronized boolean startBluetoothSco(int scoAudioMode, @NonNull String eventSource) { AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(eventSource)); return requestScoState(BluetoothHeadset.STATE_AUDIO_CONNECTED, scoAudioMode); } - // Called locked by ADeviceBroker.mSetModeLock -> AudioDeviceBroker.mDeviceStateLock + @GuardedBy("mDeviceBroker.mDeviceStateLock") /*package*/ synchronized boolean stopBluetoothSco(@NonNull String eventSource) { AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(eventSource)); return requestScoState(BluetoothHeadset.STATE_AUDIO_DISCONNECTED, SCO_MODE_VIRTUAL_CALL); @@ -551,7 +565,8 @@ public class BtHelper { } } - /*package*/ synchronized void onBroadcastScoConnectionState(int state) { + @GuardedBy("mDeviceBroker.mDeviceStateLock") + /*package*/ void onBroadcastScoConnectionState(int state) { if (state == mScoConnectionState) { return; } @@ -563,8 +578,8 @@ public class BtHelper { mScoConnectionState = state; } - // Called locked by ADeviceBroker.mSetModeLock -> AudioDeviceBroker.mDeviceStateLock - /*package*/ synchronized void resetBluetoothSco() { + @GuardedBy("mDeviceBroker.mDeviceStateLock") + /*package*/ void resetBluetoothSco() { mScoAudioState = SCO_STATE_INACTIVE; broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_DISCONNECTED); mDeviceBroker.clearA2dpSuspended(false /* internalOnly */); @@ -572,7 +587,7 @@ public class BtHelper { mDeviceBroker.setBluetoothScoOn(false, "resetBluetoothSco"); } - // Called locked by ADeviceBroker.mSetModeLock -> AudioDeviceBroker.mDeviceStateLock + @GuardedBy("mDeviceBroker.mDeviceStateLock") /*package*/ synchronized void onBtProfileDisconnected(int profile) { AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( "BT profile " + BluetoothProfile.getProfileName(profile) @@ -634,9 +649,10 @@ public class BtHelper { } } + @GuardedBy("BtHelper.this") MyLeAudioCallback mLeAudioCallback = null; - // Called locked by ADeviceBroker.mSetModeLock -> AudioDeviceBroker.mDeviceStateLock + @GuardedBy("mDeviceBroker.mDeviceStateLock") /*package*/ synchronized void onBtProfileConnected(int profile, BluetoothProfile proxy) { AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( "BT profile " + BluetoothProfile.getProfileName(profile) + " connected to proxy " @@ -773,8 +789,8 @@ public class BtHelper { } } - // Called locked by ADeviceBroker.mSetModeLock -> AudioDeviceBroker.mDeviceStateLock - private void onHeadsetProfileConnected(@NonNull BluetoothHeadset headset) { + @GuardedBy("mDeviceBroker.mDeviceStateLock") + private synchronized void onHeadsetProfileConnected(@NonNull BluetoothHeadset headset) { // Discard timeout message mDeviceBroker.handleCancelFailureToConnectToBtHeadsetService(); mBluetoothHeadset = headset; @@ -830,6 +846,7 @@ public class BtHelper { mDeviceBroker.postBroadcastScoConnectionState(state); } + @GuardedBy("mDeviceBroker.mDeviceStateLock") @Nullable AudioDeviceAttributes getHeadsetAudioDevice() { if (mBluetoothHeadsetDevice == null) { return null; @@ -837,6 +854,7 @@ public class BtHelper { return getHeadsetAudioDevice(mBluetoothHeadsetDevice); } + @GuardedBy("mDeviceBroker.mDeviceStateLock") private @NonNull AudioDeviceAttributes getHeadsetAudioDevice(BluetoothDevice btDevice) { AudioDeviceAttributes deviceAttr = mResolvedScoAudioDevices.get(btDevice); if (deviceAttr != null) { @@ -876,30 +894,44 @@ public class BtHelper { return new AudioDeviceAttributes(nativeType, address, name); } + @GuardedBy("mDeviceBroker.mDeviceStateLock") private boolean handleBtScoActiveDeviceChange(BluetoothDevice btDevice, boolean isActive) { if (btDevice == null) { return true; } - int inDevice = AudioSystem.DEVICE_IN_BLUETOOTH_SCO_HEADSET; - AudioDeviceAttributes audioDevice = btHeadsetDeviceToAudioDevice(btDevice); boolean result = false; + AudioDeviceAttributes audioDevice = null; // Only used if isActive is true + String address = btDevice.getAddress(); + String name = getName(btDevice); + // Handle output device if (isActive) { - result |= mDeviceBroker.handleDeviceConnection(audioDevice, isActive, btDevice); + audioDevice = btHeadsetDeviceToAudioDevice(btDevice); + result = mDeviceBroker.handleDeviceConnection( + audioDevice, true /*connect*/, btDevice); } else { - int[] outDeviceTypes = { + AudioDeviceAttributes ada = mResolvedScoAudioDevices.get(btDevice); + if (ada != null) { + result = mDeviceBroker.handleDeviceConnection( + ada, false /*connect*/, btDevice); + } else { + // Disconnect all possible audio device types if the disconnected device type is + // unknown + int[] outDeviceTypes = { AudioSystem.DEVICE_OUT_BLUETOOTH_SCO, AudioSystem.DEVICE_OUT_BLUETOOTH_SCO_HEADSET, AudioSystem.DEVICE_OUT_BLUETOOTH_SCO_CARKIT - }; - for (int outDeviceType : outDeviceTypes) { - result |= mDeviceBroker.handleDeviceConnection(new AudioDeviceAttributes( - outDeviceType, audioDevice.getAddress(), audioDevice.getName()), - isActive, btDevice); + }; + for (int outDeviceType : outDeviceTypes) { + result |= mDeviceBroker.handleDeviceConnection(new AudioDeviceAttributes( + outDeviceType, address, name), false /*connect*/, btDevice); + } } } + // Handle input device + int inDevice = AudioSystem.DEVICE_IN_BLUETOOTH_SCO_HEADSET; // handleDeviceConnection() && result to make sure the method get executed result = mDeviceBroker.handleDeviceConnection(new AudioDeviceAttributes( - inDevice, audioDevice.getAddress(), audioDevice.getName()), + inDevice, address, name), isActive, btDevice) && result; if (result) { if (isActive) { @@ -916,8 +948,8 @@ public class BtHelper { return btDevice == null ? "(null)" : btDevice.getAnonymizedAddress(); } - // Called locked by ADeviceBroker.mSetModeLock -> AudioDeviceBroker.mDeviceStateLock - /*package */ synchronized void onSetBtScoActiveDevice(BluetoothDevice btDevice) { + @GuardedBy("mDeviceBroker.mDeviceStateLock") + /*package */ void onSetBtScoActiveDevice(BluetoothDevice btDevice) { Log.i(TAG, "onSetBtScoActiveDevice: " + getAnonymizedAddress(mBluetoothHeadsetDevice) + " -> " + getAnonymizedAddress(btDevice)); final BluetoothDevice previousActiveDevice = mBluetoothHeadsetDevice; @@ -987,10 +1019,8 @@ public class BtHelper { //---------------------------------------------------------------------- - // @GuardedBy("mDeviceBroker.mSetModeLock") - // @GuardedBy("AudioDeviceBroker.this.mDeviceStateLock") - @GuardedBy("BtHelper.this") - private boolean requestScoState(int state, int scoAudioMode) { + @GuardedBy("mDeviceBroker.mDeviceStateLock") + private synchronized boolean requestScoState(int state, int scoAudioMode) { checkScoAudioState(); if (state == BluetoothHeadset.STATE_AUDIO_CONNECTED) { // Make sure that the state transitions to CONNECTING even if we cannot initiate @@ -1154,13 +1184,14 @@ public class BtHelper { } } - private void checkScoAudioState() { + @GuardedBy("mDeviceBroker.mDeviceStateLock") + private synchronized void checkScoAudioState() { try { if (mBluetoothHeadset != null && mBluetoothHeadsetDevice != null && mScoAudioState == SCO_STATE_INACTIVE && mBluetoothHeadset.getAudioState(mBluetoothHeadsetDevice) - != BluetoothHeadset.STATE_AUDIO_DISCONNECTED) { + != BluetoothHeadset.STATE_AUDIO_DISCONNECTED) { mScoAudioState = SCO_STATE_ACTIVE_EXTERNAL; } } catch (Exception e) { @@ -1184,7 +1215,7 @@ public class BtHelper { return result; } - /*package*/ int getLeAudioDeviceGroupId(BluetoothDevice device) { + /*package*/ synchronized int getLeAudioDeviceGroupId(BluetoothDevice device) { if (mLeAudio == null || device == null) { return BluetoothLeAudio.GROUP_ID_INVALID; } @@ -1197,7 +1228,7 @@ public class BtHelper { * @return A List of Pair(String main_address, String identity_address). Note that the * addresses returned by BluetoothDevice can be null. */ - /*package*/ List<Pair<String, String>> getLeAudioGroupAddresses(int groupId) { + /*package*/ synchronized List<Pair<String, String>> getLeAudioGroupAddresses(int groupId) { List<Pair<String, String>> addresses = new ArrayList<>(); BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); if (adapter == null || mLeAudio == null) { diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java index 7a055d1d7e5c..76b4263c4b89 100644 --- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java +++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java @@ -1192,6 +1192,18 @@ public class DisplayDeviceConfig { */ public float getHdrBrightnessFromSdr(float brightness, float maxDesiredHdrSdrRatio) { Spline sdrToHdrSpline = mHbmData != null ? mHbmData.sdrToHdrRatioSpline : null; + return getHdrBrightnessFromSdr(brightness, maxDesiredHdrSdrRatio, sdrToHdrSpline); + } + + /** + * Calculate the HDR brightness for the specified SDR brightenss, restricted by the + * maxDesiredHdrSdrRatio (the ratio between the HDR luminance and SDR luminance) and specific + * sdrToHdrSpline + * + * @return the HDR brightness or BRIGHTNESS_INVALID when no mapping exists. + */ + public float getHdrBrightnessFromSdr(float brightness, float maxDesiredHdrSdrRatio, + @Nullable Spline sdrToHdrSpline) { if (sdrToHdrSpline == null) { return PowerManager.BRIGHTNESS_INVALID; } diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index b3a6c1c1e20a..2cec869c290e 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -3849,9 +3849,10 @@ public final class DisplayManagerService extends SystemService { // Ignore redundant events. Further optimization is possible by merging adjacent events. Pair<Integer, Integer> last = mDisplayEvents.get(mDisplayEvents.size() - 1); if (last.first == displayId && last.second == event) { - Slog.d(TAG, - "Ignore redundant display event " + displayId + "/" + event + " to " - + mCallbackRecord.mUid + "/" + mCallbackRecord.mPid); + if (DEBUG) { + Slog.d(TAG, "Ignore redundant display event " + displayId + "/" + event + " to " + + mCallbackRecord.mUid + "/" + mCallbackRecord.mPid); + } return; } diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java index 5c1e783c0f52..1177be212222 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController.java +++ b/services/core/java/com/android/server/display/DisplayPowerController.java @@ -129,9 +129,9 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call private static final String SCREEN_ON_BLOCKED_TRACE_NAME = "Screen on blocked"; private static final String SCREEN_OFF_BLOCKED_TRACE_NAME = "Screen off blocked"; - private static final String TAG = "DisplayPowerController2"; + private static final String TAG = "DisplayPowerController"; // To enable these logs, run: - // 'adb shell setprop persist.log.tag.DisplayPowerController2 DEBUG && adb reboot' + // 'adb shell setprop persist.log.tag.DisplayPowerController DEBUG && adb reboot' private static final boolean DEBUG = DebugUtils.isDebuggable(TAG); private static final String SCREEN_ON_BLOCKED_BY_DISPLAYOFFLOAD_TRACE_NAME = "Screen on blocked by displayoffload"; @@ -263,6 +263,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call // The unique ID of the primary display device currently tied to this logical display private String mUniqueDisplayId; + private String mPhysicalDisplayName; // Tracker for brightness changes. @Nullable @@ -371,10 +372,6 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call // If the last recorded screen state was dozing or not. private boolean mDozing; - private boolean mAppliedDimming; - - private boolean mAppliedThrottling; - // Reason for which the brightness was last changed. See {@link BrightnessReason} for more // information. // At the time of this writing, this value is changed within updatePowerState() only, which is @@ -483,7 +480,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call // DPCs following the brightness of this DPC. This is used in concurrent displays mode - there // is one lead display, the additional displays follow the brightness value of the lead display. @GuardedBy("mLock") - private SparseArray<DisplayPowerControllerInterface> mDisplayBrightnessFollowers = + private final SparseArray<DisplayPowerControllerInterface> mDisplayBrightnessFollowers = new SparseArray(); private boolean mBootCompleted; @@ -508,14 +505,15 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call mClock = mInjector.getClock(); mLogicalDisplay = logicalDisplay; mDisplayId = mLogicalDisplay.getDisplayIdLocked(); + mDisplayDevice = mLogicalDisplay.getPrimaryDisplayDeviceLocked(); + IBinder displayToken = mDisplayDevice.getDisplayTokenLocked(); + DisplayDeviceInfo displayDeviceInfo = mDisplayDevice.getDisplayDeviceInfoLocked(); mSensorManager = sensorManager; mHandler = new DisplayControllerHandler(handler.getLooper()); - mDisplayDeviceConfig = logicalDisplay.getPrimaryDisplayDeviceLocked() - .getDisplayDeviceConfig(); + mDisplayDeviceConfig = mDisplayDevice.getDisplayDeviceConfig(); mIsEnabled = logicalDisplay.isEnabledLocked(); mIsInTransition = logicalDisplay.isInTransitionLocked(); - mIsDisplayInternal = logicalDisplay.getPrimaryDisplayDeviceLocked() - .getDisplayDeviceInfoLocked().type == Display.TYPE_INTERNAL; + mIsDisplayInternal = displayDeviceInfo.type == Display.TYPE_INTERNAL; mWakelockController = mInjector.getWakelockController(mDisplayId, callbacks); mDisplayPowerProximityStateController = mInjector.getDisplayPowerProximityStateController( mWakelockController, mDisplayDeviceConfig, mHandler.getLooper(), @@ -524,9 +522,10 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call mTag = TAG + "[" + mDisplayId + "]"; mThermalBrightnessThrottlingDataId = logicalDisplay.getDisplayInfoLocked().thermalBrightnessThrottlingDataId; - mDisplayDevice = mLogicalDisplay.getPrimaryDisplayDeviceLocked(); - mUniqueDisplayId = logicalDisplay.getPrimaryDisplayDeviceLocked().getUniqueId(); + + mUniqueDisplayId = mDisplayDevice.getUniqueId(); mDisplayStatsId = mUniqueDisplayId.hashCode(); + mPhysicalDisplayName = mDisplayDevice.getNameLocked(); mLastBrightnessEvent = new BrightnessEvent(mDisplayId); mTempBrightnessEvent = new BrightnessEvent(mDisplayId); @@ -544,8 +543,6 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call mBrightnessTracker = brightnessTracker; mOnBrightnessChangeRunnable = onBrightnessChangeRunnable; - PowerManager pm = context.getSystemService(PowerManager.class); - final Resources resources = context.getResources(); // DOZE AND DIM SETTINGS @@ -573,8 +570,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call mBrightnessRangeController = mInjector.getBrightnessRangeController(hbmController, modeChangeCallback, mDisplayDeviceConfig, mHandler, flags, - mDisplayDevice.getDisplayTokenLocked(), - mDisplayDevice.getDisplayDeviceInfoLocked()); + displayToken, displayDeviceInfo); mDisplayBrightnessController = new DisplayBrightnessController(context, null, @@ -588,8 +584,8 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call mUniqueDisplayId, mThermalBrightnessThrottlingDataId, logicalDisplay.getPowerThrottlingDataIdLocked(), - mDisplayDeviceConfig, - mDisplayId), mContext, flags, mSensorManager); + mDisplayDeviceConfig, displayDeviceInfo.width, displayDeviceInfo.height, + displayToken, mDisplayId), mContext, flags, mSensorManager); // Seed the cached brightness saveBrightnessInfo(getScreenBrightnessSetting()); mAutomaticBrightnessStrategy = @@ -840,6 +836,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call } final String uniqueId = device.getUniqueId(); + final String displayName = device.getNameLocked(); final DisplayDeviceConfig config = device.getDisplayDeviceConfig(); final IBinder token = device.getDisplayTokenLocked(); final DisplayDeviceInfo info = device.getDisplayDeviceInfoLocked(); @@ -866,6 +863,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call changed = true; mDisplayDevice = device; mUniqueDisplayId = uniqueId; + mPhysicalDisplayName = displayName; mDisplayStatsId = mUniqueDisplayId.hashCode(); mDisplayDeviceConfig = config; mThermalBrightnessThrottlingDataId = thermalBrightnessThrottlingDataId; @@ -895,7 +893,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call mBrightnessClamperController.onDisplayChanged( new BrightnessClamperController.DisplayDeviceData(uniqueId, thermalBrightnessThrottlingDataId, powerThrottlingDataId, - config, mDisplayId)); + config, info.width, info.height, token, mDisplayId)); if (changed) { updatePowerState(); @@ -1552,10 +1550,9 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call // unthrottled (unclamped/ideal) and throttled brightness levels for subsequent operations. // Note throttling effectively changes the allowed brightness range, so, similarly to HBM, // we broadcast this change through setting. - final float unthrottledBrightnessState = brightnessState; + final float unthrottledBrightnessState = rawBrightnessState; DisplayBrightnessState clampedState = mBrightnessClamperController.clamp(mPowerRequest, brightnessState, slowChange, /* displayState= */ state); - brightnessState = clampedState.getBrightness(); slowChange = clampedState.isSlowChange(); // faster rate wins, at this point customAnimationRate == -1, strategy does not control @@ -1744,11 +1741,23 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call // brightness cap, RBC state, etc. mTempBrightnessEvent.setTime(System.currentTimeMillis()); mTempBrightnessEvent.setBrightness(brightnessState); + mTempBrightnessEvent.setNits( + mDisplayBrightnessController.convertToAdjustedNits(brightnessState)); + final float hbmMax = mBrightnessRangeController.getCurrentBrightnessMax(); + final float clampedMax = Math.min(clampedState.getMaxBrightness(), hbmMax); + final float brightnessOnAvailableScale = MathUtils.constrainedMap(0.0f, 1.0f, + clampedState.getMinBrightness(), clampedMax, + brightnessState); + mTempBrightnessEvent.setPercent(Math.round( + 1000.0f * com.android.internal.display.BrightnessUtils.convertLinearToGamma( + brightnessOnAvailableScale) / 10)); // rounded to one dp + mTempBrightnessEvent.setUnclampedBrightness(unthrottledBrightnessState); mTempBrightnessEvent.setPhysicalDisplayId(mUniqueDisplayId); + mTempBrightnessEvent.setPhysicalDisplayName(mPhysicalDisplayName); mTempBrightnessEvent.setDisplayState(state); mTempBrightnessEvent.setDisplayPolicy(mPowerRequest.policy); mTempBrightnessEvent.setReason(mBrightnessReason); - mTempBrightnessEvent.setHbmMax(mBrightnessRangeController.getCurrentBrightnessMax()); + mTempBrightnessEvent.setHbmMax(hbmMax); mTempBrightnessEvent.setHbmMode(mBrightnessRangeController.getHighBrightnessMode()); mTempBrightnessEvent.setFlags(mTempBrightnessEvent.getFlags() | (mIsRbcActive ? BrightnessEvent.FLAG_RBC : 0) @@ -2648,8 +2657,6 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call pw.println("Display Power Controller Thread State:"); pw.println(" mPowerRequest=" + mPowerRequest); pw.println(" mBrightnessReason=" + mBrightnessReason); - pw.println(" mAppliedDimming=" + mAppliedDimming); - pw.println(" mAppliedThrottling=" + mAppliedThrottling); pw.println(" mDozing=" + mDozing); pw.println(" mSkipRampState=" + skipRampStateToString(mSkipRampState)); pw.println(" mScreenOnBlockStartRealTime=" + mScreenOnBlockStartRealTime); diff --git a/services/core/java/com/android/server/display/brightness/BrightnessEvent.java b/services/core/java/com/android/server/display/brightness/BrightnessEvent.java index 82b401a7cc83..5cc603c5018c 100644 --- a/services/core/java/com/android/server/display/brightness/BrightnessEvent.java +++ b/services/core/java/com/android/server/display/brightness/BrightnessEvent.java @@ -20,6 +20,8 @@ import static android.hardware.display.DisplayManagerInternal.DisplayPowerReques import static android.hardware.display.DisplayManagerInternal.DisplayPowerRequest.policyToString; import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DEFAULT; +import static com.android.server.display.BrightnessMappingStrategy.INVALID_LUX; +import static com.android.server.display.BrightnessMappingStrategy.INVALID_NITS; import static com.android.server.display.config.DisplayBrightnessMappingConfig.autoBrightnessModeToString; import android.hardware.display.BrightnessInfo; @@ -48,13 +50,17 @@ public final class BrightnessEvent { private BrightnessReason mReason = new BrightnessReason(); private int mDisplayId; private String mPhysicalDisplayId; + private String mPhysicalDisplayName; private int mDisplayState; private int mDisplayPolicy; private long mTime; private float mLux; + private float mNits; + private float mPercent; private float mPreThresholdLux; private float mInitialBrightness; private float mBrightness; + private float mUnclampedBrightness; private float mRecommendedBrightness; private float mPreThresholdBrightness; private int mHbmMode; @@ -88,15 +94,19 @@ public final class BrightnessEvent { mReason.set(that.getReason()); mDisplayId = that.getDisplayId(); mPhysicalDisplayId = that.getPhysicalDisplayId(); + mPhysicalDisplayName = that.getPhysicalDisplayName(); mDisplayState = that.mDisplayState; mDisplayPolicy = that.mDisplayPolicy; mTime = that.getTime(); // Lux values mLux = that.getLux(); mPreThresholdLux = that.getPreThresholdLux(); + mNits = that.getNits(); + mPercent = that.getPercent(); // Brightness values mInitialBrightness = that.getInitialBrightness(); mBrightness = that.getBrightness(); + mUnclampedBrightness = that.getUnclampedBrightness(); mRecommendedBrightness = that.getRecommendedBrightness(); mPreThresholdBrightness = that.getPreThresholdBrightness(); // Different brightness modulations @@ -121,14 +131,18 @@ public final class BrightnessEvent { mReason = new BrightnessReason(); mTime = SystemClock.uptimeMillis(); mPhysicalDisplayId = ""; + mPhysicalDisplayName = ""; mDisplayState = Display.STATE_UNKNOWN; mDisplayPolicy = POLICY_OFF; // Lux values - mLux = 0; + mLux = INVALID_LUX; mPreThresholdLux = 0; + mNits = INVALID_NITS; + mPercent = -1f; // Brightness values mInitialBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT; mBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT; + mUnclampedBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT; mRecommendedBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT; mPreThresholdBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT; // Different brightness modulations @@ -160,13 +174,18 @@ public final class BrightnessEvent { return mReason.equals(that.mReason) && mDisplayId == that.mDisplayId && mPhysicalDisplayId.equals(that.mPhysicalDisplayId) + && mPhysicalDisplayName.equals(that.mPhysicalDisplayName) && mDisplayState == that.mDisplayState && mDisplayPolicy == that.mDisplayPolicy && Float.floatToRawIntBits(mLux) == Float.floatToRawIntBits(that.mLux) && Float.floatToRawIntBits(mPreThresholdLux) == Float.floatToRawIntBits(that.mPreThresholdLux) + && Float.floatToRawIntBits(mNits) == Float.floatToRawIntBits(that.mNits) + && Float.floatToRawIntBits(mPercent) == Float.floatToRawIntBits(that.mPercent) && Float.floatToRawIntBits(mBrightness) == Float.floatToRawIntBits(that.mBrightness) + && Float.floatToRawIntBits(mUnclampedBrightness) + == Float.floatToRawIntBits(that.mUnclampedBrightness) && Float.floatToRawIntBits(mRecommendedBrightness) == Float.floatToRawIntBits(that.mRecommendedBrightness) && Float.floatToRawIntBits(mPreThresholdBrightness) @@ -195,27 +214,34 @@ public final class BrightnessEvent { public String toString(boolean includeTime) { return (includeTime ? FORMAT.format(new Date(mTime)) + " - " : "") + "BrightnessEvent: " - + "disp=" + mDisplayId - + ", physDisp=" + mPhysicalDisplayId - + ", displayState=" + Display.stateToString(mDisplayState) - + ", displayPolicy=" + policyToString(mDisplayPolicy) - + ", brt=" + mBrightness + ((mFlags & FLAG_USER_SET) != 0 ? "(user_set)" : "") + + "brt=" + mBrightness + ((mFlags & FLAG_USER_SET) != 0 ? "(user_set)" : "") + " (" + + mPercent + "%)" + + ", nits= " + mNits + + ", lux=" + mLux + + ", reason=" + mReason.toString(mAdjustmentFlags) + + ", strat=" + mDisplayBrightnessStrategyName + + ", state=" + Display.stateToString(mDisplayState) + + ", policy=" + policyToString(mDisplayPolicy) + + ", flags=" + flagsToString() + // Autobrightness + ", initBrt=" + mInitialBrightness + ", rcmdBrt=" + mRecommendedBrightness + ", preBrt=" + mPreThresholdBrightness - + ", lux=" + mLux + ", preLux=" + mPreThresholdLux + + ", wasShortTermModelActive=" + mWasShortTermModelActive + + ", autoBrightness=" + mAutomaticBrightnessEnabled + " (" + + autoBrightnessModeToString(mAutoBrightnessMode) + ")" + // Throttling info + + ", unclampedBrt=" + mUnclampedBrightness + ", hbmMax=" + mHbmMax + ", hbmMode=" + BrightnessInfo.hbmToString(mHbmMode) - + ", rbcStrength=" + mRbcStrength + ", thrmMax=" + mThermalMax + // Modifiers + + ", rbcStrength=" + mRbcStrength + ", powerFactor=" + mPowerFactor - + ", wasShortTermModelActive=" + mWasShortTermModelActive - + ", flags=" + flagsToString() - + ", reason=" + mReason.toString(mAdjustmentFlags) - + ", autoBrightness=" + mAutomaticBrightnessEnabled - + ", strategy=" + mDisplayBrightnessStrategyName - + ", autoBrightnessMode=" + autoBrightnessModeToString(mAutoBrightnessMode); + // Meta + + ", physDisp=" + mPhysicalDisplayName + "(" + mPhysicalDisplayId + ")" + + ", logicalId=" + mDisplayId; } @Override @@ -255,6 +281,14 @@ public final class BrightnessEvent { this.mPhysicalDisplayId = mPhysicalDisplayId; } + public String getPhysicalDisplayName() { + return mPhysicalDisplayName; + } + + public void setPhysicalDisplayName(String mPhysicalDisplayName) { + this.mPhysicalDisplayName = mPhysicalDisplayName; + } + public void setDisplayState(int state) { mDisplayState = state; } @@ -295,6 +329,29 @@ public final class BrightnessEvent { this.mBrightness = brightness; } + public float getUnclampedBrightness() { + return mUnclampedBrightness; + } + + public void setUnclampedBrightness(float unclampedBrightness) { + this.mUnclampedBrightness = unclampedBrightness; + } + + public void setPercent(float percent) { + this.mPercent = percent; + } + public float getPercent() { + return mPercent; + } + + public void setNits(float nits) { + this.mNits = nits; + } + + public float getNits() { + return mNits; + } + public float getRecommendedBrightness() { return mRecommendedBrightness; } diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java index 88d2c007cf37..afab7438bf16 100644 --- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java +++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java @@ -28,13 +28,16 @@ import android.hardware.display.BrightnessInfo; import android.hardware.display.DisplayManagerInternal; import android.os.Handler; import android.os.HandlerExecutor; +import android.os.IBinder; import android.os.PowerManager; import android.provider.DeviceConfig; import android.provider.DeviceConfigInterface; import android.util.IndentingPrintWriter; import android.util.Slog; +import android.util.Spline; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.display.BrightnessSynchronizer; import com.android.server.display.DisplayBrightnessState; import com.android.server.display.DisplayDeviceConfig; import com.android.server.display.DisplayDeviceConfig.PowerThrottlingConfigData; @@ -65,6 +68,11 @@ public class BrightnessClamperController { private final List<BrightnessClamper<? super DisplayDeviceData>> mClampers; private final List<BrightnessStateModifier> mModifiers; + + private final List<DisplayDeviceDataListener> mDisplayDeviceDataListeners = new ArrayList<>(); + private final List<StatefulModifier> mStatefulModifiers = new ArrayList<>(); + private ModifiersAggregatedState mModifiersAggregatedState = new ModifiersAggregatedState(); + private final DeviceConfig.OnPropertiesChangedListener mOnPropertiesChangedListener; private float mBrightnessCap = PowerManager.BRIGHTNESS_MAX; @@ -110,7 +118,16 @@ public class BrightnessClamperController { mClampers = injector.getClampers(handler, clamperChangeListenerInternal, data, flags, context); mModifiers = injector.getModifiers(flags, context, handler, clamperChangeListener, - data.mDisplayDeviceConfig); + data); + + mModifiers.forEach(m -> { + if (m instanceof DisplayDeviceDataListener l) { + mDisplayDeviceDataListeners.add(l); + } + if (m instanceof StatefulModifier s) { + mStatefulModifiers.add(s); + } + }); mOnPropertiesChangedListener = properties -> mClampers.forEach(BrightnessClamper::onDeviceConfigChanged); mLightSensorController.configure(data.getAmbientLightSensor(), data.getDisplayId()); @@ -123,6 +140,7 @@ public class BrightnessClamperController { public void onDisplayChanged(DisplayDeviceData data) { mLightSensorController.configure(data.getAmbientLightSensor(), data.getDisplayId()); mClampers.forEach(clamper -> clamper.onDisplayChanged(data)); + mDisplayDeviceDataListeners.forEach(l -> l.onDisplayChanged(data)); adjustLightSensorSubscription(); } @@ -234,14 +252,27 @@ public class BrightnessClamperController { customAnimationRate = minClamper.getCustomAnimationRate(); } + ModifiersAggregatedState newAggregatedState = new ModifiersAggregatedState(); + mStatefulModifiers.forEach((clamper) -> clamper.applyStateChange(newAggregatedState)); + if (mBrightnessCap != brightnessCap || mClamperType != clamperType - || mCustomAnimationRate != customAnimationRate) { + || mCustomAnimationRate != customAnimationRate + || needToNotifyExternalListener(mModifiersAggregatedState, newAggregatedState)) { mBrightnessCap = brightnessCap; mClamperType = clamperType; mCustomAnimationRate = customAnimationRate; mClamperChangeListenerExternal.onChanged(); } + mModifiersAggregatedState = newAggregatedState; + } + + private boolean needToNotifyExternalListener(ModifiersAggregatedState state1, + ModifiersAggregatedState state2) { + return !BrightnessSynchronizer.floatEquals(state1.mMaxDesiredHdrRatio, + state2.mMaxDesiredHdrRatio) + || state1.mSdrHdrRatioSpline != state2.mSdrHdrRatioSpline + || state1.mHdrHbmEnabled != state2.mHdrHbmEnabled; } private void start() { @@ -295,17 +326,16 @@ public class BrightnessClamperController { List<BrightnessStateModifier> getModifiers(DisplayManagerFlags flags, Context context, Handler handler, ClamperChangeListener listener, - DisplayDeviceConfig displayDeviceConfig) { + DisplayDeviceData data) { List<BrightnessStateModifier> modifiers = new ArrayList<>(); modifiers.add(new DisplayDimModifier(context)); modifiers.add(new BrightnessLowPowerModeModifier()); - if (flags.isEvenDimmerEnabled() && displayDeviceConfig != null - && displayDeviceConfig.isEvenDimmerAvailable()) { + if (flags.isEvenDimmerEnabled() && data.mDisplayDeviceConfig.isEvenDimmerAvailable()) { modifiers.add(new BrightnessLowLuxModifier(handler, listener, context, - displayDeviceConfig)); + data.mDisplayDeviceConfig)); } if (flags.useNewHdrBrightnessModifier()) { - modifiers.add(new HdrBrightnessModifier()); + modifiers.add(new HdrBrightnessModifier(handler, listener, data)); } return modifiers; } @@ -319,7 +349,14 @@ public class BrightnessClamperController { } /** - * Config Data for clampers + * Modifier should implement this interface in order to receive display change updates + */ + interface DisplayDeviceDataListener { + void onDisplayChanged(DisplayDeviceData displayData); + } + + /** + * Config Data for clampers/modifiers */ public static class DisplayDeviceData implements BrightnessThermalClamper.ThermalData, BrightnessPowerClamper.PowerData, @@ -331,23 +368,34 @@ public class BrightnessClamperController { @NonNull private final String mPowerThrottlingDataId; @NonNull - private final DisplayDeviceConfig mDisplayDeviceConfig; + final DisplayDeviceConfig mDisplayDeviceConfig; + + final int mWidth; - private final int mDisplayId; + final int mHeight; + + final IBinder mDisplayToken; + + final int mDisplayId; public DisplayDeviceData(@NonNull String uniqueDisplayId, @NonNull String thermalThrottlingDataId, @NonNull String powerThrottlingDataId, @NonNull DisplayDeviceConfig displayDeviceConfig, + int width, + int height, + IBinder displayToken, int displayId) { mUniqueDisplayId = uniqueDisplayId; mThermalThrottlingDataId = thermalThrottlingDataId; mPowerThrottlingDataId = powerThrottlingDataId; mDisplayDeviceConfig = displayDeviceConfig; + mWidth = width; + mHeight = height; + mDisplayToken = displayToken; mDisplayId = displayId; } - @NonNull @Override public String getUniqueDisplayId() { @@ -406,4 +454,24 @@ public class BrightnessClamperController { return mDisplayId; } } + + /** + * Stateful modifier should implement this interface and modify aggregatedState. + * AggregatedState is used by Controller to determine if updatePowerState call is needed + * to correctly adjust brightness + */ + interface StatefulModifier { + void applyStateChange(ModifiersAggregatedState aggregatedState); + } + + /** + * StatefulModifiers contribute to AggregatedState, that is used to decide if brightness + * adjustement is needed + */ + public static class ModifiersAggregatedState { + float mMaxDesiredHdrRatio = HdrBrightnessModifier.DEFAULT_MAX_HDR_SDR_RATIO; + @Nullable + Spline mSdrHdrRatioSpline = null; + boolean mHdrHbmEnabled = false; + } } diff --git a/services/core/java/com/android/server/display/brightness/clamper/HdrBrightnessModifier.java b/services/core/java/com/android/server/display/brightness/clamper/HdrBrightnessModifier.java index a829866ae6fc..2ee70fd88919 100644 --- a/services/core/java/com/android/server/display/brightness/clamper/HdrBrightnessModifier.java +++ b/services/core/java/com/android/server/display/brightness/clamper/HdrBrightnessModifier.java @@ -16,17 +16,85 @@ package com.android.server.display.brightness.clamper; +import android.annotation.Nullable; +import android.annotation.SuppressLint; import android.hardware.display.DisplayManagerInternal; +import android.os.Handler; +import android.os.IBinder; +import android.view.SurfaceControlHdrLayerInfoListener; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.display.BrightnessSynchronizer; import com.android.server.display.DisplayBrightnessState; +import com.android.server.display.DisplayDeviceConfig; +import com.android.server.display.config.HdrBrightnessData; import java.io.PrintWriter; -public class HdrBrightnessModifier implements BrightnessStateModifier { +public class HdrBrightnessModifier implements BrightnessStateModifier, + BrightnessClamperController.DisplayDeviceDataListener, + BrightnessClamperController.StatefulModifier { + + static final float DEFAULT_MAX_HDR_SDR_RATIO = 1.0f; + private static final float DEFAULT_HDR_LAYER_SIZE = -1.0f; + + private final SurfaceControlHdrLayerInfoListener mHdrListener = + new SurfaceControlHdrLayerInfoListener() { + @Override + public void onHdrInfoChanged(IBinder displayToken, int numberOfHdrLayers, int maxW, + int maxH, int flags, float maxDesiredHdrSdrRatio) { + boolean hdrLayerPresent = numberOfHdrLayers > 0; + mHandler.post(() -> HdrBrightnessModifier.this.onHdrInfoChanged( + hdrLayerPresent ? (float) (maxW * maxH) : DEFAULT_HDR_LAYER_SIZE, + hdrLayerPresent ? maxDesiredHdrSdrRatio : DEFAULT_MAX_HDR_SDR_RATIO)); + } + }; + + private final Handler mHandler; + private final BrightnessClamperController.ClamperChangeListener mClamperChangeListener; + private final Injector mInjector; + + private IBinder mRegisteredDisplayToken; + + private float mScreenSize; + private float mHdrLayerSize = DEFAULT_HDR_LAYER_SIZE; + private HdrBrightnessData mHdrBrightnessData; + private DisplayDeviceConfig mDisplayDeviceConfig; + private float mMaxDesiredHdrRatio = DEFAULT_MAX_HDR_SDR_RATIO; + private Mode mMode = Mode.NO_HDR; + + HdrBrightnessModifier(Handler handler, + BrightnessClamperController.ClamperChangeListener clamperChangeListener, + BrightnessClamperController.DisplayDeviceData displayData) { + this(handler, clamperChangeListener, new Injector(), displayData); + } + + @VisibleForTesting + HdrBrightnessModifier(Handler handler, + BrightnessClamperController.ClamperChangeListener clamperChangeListener, + Injector injector, + BrightnessClamperController.DisplayDeviceData displayData) { + mHandler = handler; + mClamperChangeListener = clamperChangeListener; + mInjector = injector; + onDisplayChanged(displayData); + } + + // Called in DisplayControllerHandler @Override public void apply(DisplayManagerInternal.DisplayPowerRequest request, DisplayBrightnessState.Builder stateBuilder) { - // noop + if (mHdrBrightnessData == null) { // no hdr data + return; + } + if (mMode == Mode.NO_HDR) { + return; + } + + float hdrBrightness = mDisplayDeviceConfig.getHdrBrightnessFromSdr( + stateBuilder.getBrightness(), mMaxDesiredHdrRatio, + mHdrBrightnessData.sdrToHdrRatioSpline); + stateBuilder.setHdrBrightness(hdrBrightness); } @Override @@ -34,11 +102,13 @@ public class HdrBrightnessModifier implements BrightnessStateModifier { // noop } + // Called in DisplayControllerHandler @Override public void stop() { - // noop + unregisterHdrListener(); } + @Override public boolean shouldListenToLightSensor() { return false; @@ -48,4 +118,117 @@ public class HdrBrightnessModifier implements BrightnessStateModifier { public void setAmbientLux(float lux) { // noop } + + @Override + public void onDisplayChanged(BrightnessClamperController.DisplayDeviceData displayData) { + mHandler.post(() -> onDisplayChanged(displayData.mDisplayToken, displayData.mWidth, + displayData.mHeight, displayData.mDisplayDeviceConfig)); + } + + // Called in DisplayControllerHandler + @Override + public void applyStateChange( + BrightnessClamperController.ModifiersAggregatedState aggregatedState) { + if (mMode != Mode.NO_HDR) { + aggregatedState.mMaxDesiredHdrRatio = mMaxDesiredHdrRatio; + aggregatedState.mSdrHdrRatioSpline = mHdrBrightnessData.sdrToHdrRatioSpline; + aggregatedState.mHdrHbmEnabled = (mMode == Mode.HBM_HDR); + } + } + + // Called in DisplayControllerHandler + private void onDisplayChanged(IBinder displayToken, int width, int height, + DisplayDeviceConfig config) { + mDisplayDeviceConfig = config; + mScreenSize = (float) width * height; + HdrBrightnessData data = config.getHdrBrightnessData(); + if (data == null) { + unregisterHdrListener(); + } else { + registerHdrListener(displayToken); + } + recalculate(data, mMaxDesiredHdrRatio); + } + + // Called in DisplayControllerHandler + private void recalculate(@Nullable HdrBrightnessData data, float maxDesiredHdrRatio) { + Mode newMode = recalculateMode(data); + // if HDR mode changed, notify changed + boolean needToNotifyChange = mMode != newMode; + // If HDR mode is active, we need to check if other HDR params are changed + if (mMode != HdrBrightnessModifier.Mode.NO_HDR) { + if (!BrightnessSynchronizer.floatEquals(mMaxDesiredHdrRatio, maxDesiredHdrRatio) + || data != mHdrBrightnessData) { + needToNotifyChange = true; + } + } + + mMode = newMode; + mHdrBrightnessData = data; + mMaxDesiredHdrRatio = maxDesiredHdrRatio; + + if (needToNotifyChange) { + mClamperChangeListener.onChanged(); + } + } + + // Called in DisplayControllerHandler + private Mode recalculateMode(@Nullable HdrBrightnessData data) { + // no config + if (data == null) { + return Mode.NO_HDR; + } + // HDR layer < minHdr % for Nbm + if (mHdrLayerSize < mScreenSize * data.minimumHdrPercentOfScreenForNbm) { + return Mode.NO_HDR; + } + // HDR layer < minHdr % for Hbm, and HDR layer >= that minHdr % for Nbm + if (mHdrLayerSize < mScreenSize * data.minimumHdrPercentOfScreenForHbm) { + return Mode.NBM_HDR; + } + // HDR layer > that minHdr % for Hbm + return Mode.HBM_HDR; + } + + // Called in DisplayControllerHandler + private void onHdrInfoChanged(float hdrLayerSize, float maxDesiredHdrSdrRatio) { + mHdrLayerSize = hdrLayerSize; + recalculate(mHdrBrightnessData, maxDesiredHdrSdrRatio); + } + + // Called in DisplayControllerHandler + private void registerHdrListener(IBinder displayToken) { + if (mRegisteredDisplayToken == displayToken) { + return; + } + unregisterHdrListener(); + if (displayToken != null) { + mInjector.registerHdrListener(mHdrListener, displayToken); + mRegisteredDisplayToken = displayToken; + } + } + + // Called in DisplayControllerHandler + private void unregisterHdrListener() { + if (mRegisteredDisplayToken != null) { + mInjector.unregisterHdrListener(mHdrListener, mRegisteredDisplayToken); + mRegisteredDisplayToken = null; + mHdrLayerSize = DEFAULT_HDR_LAYER_SIZE; + } + } + + private enum Mode { + NO_HDR, NBM_HDR, HBM_HDR + } + + @SuppressLint("MissingPermission") + static class Injector { + void registerHdrListener(SurfaceControlHdrLayerInfoListener listener, IBinder token) { + listener.register(token); + } + + void unregisterHdrListener(SurfaceControlHdrLayerInfoListener listener, IBinder token) { + listener.unregister(token); + } + } } diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index 1f46af8b741d..bb2efa166800 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -2224,12 +2224,6 @@ public class InputManagerService extends IInputManager.Stub // Native callback. @SuppressWarnings("unused") - private void notifyConfigurationChanged(long whenNanos) { - mWindowManagerCallbacks.notifyConfigurationChanged(); - } - - // Native callback. - @SuppressWarnings("unused") private void notifyInputDevicesChanged(InputDevice[] inputDevices) { synchronized (mInputDevicesLock) { if (!mInputDevicesChangedPending) { @@ -2240,6 +2234,9 @@ public class InputManagerService extends IInputManager.Stub mInputDevices = inputDevices; } + // Input device change can possibly change configuration, so notify window manager to update + // its configuration. + mWindowManagerCallbacks.notifyConfigurationChanged(); } // Native callback. diff --git a/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMapRepository.java b/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMapRepository.java index 8ca045834981..99f4747227ae 100644 --- a/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMapRepository.java +++ b/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMapRepository.java @@ -20,9 +20,9 @@ import android.annotation.AnyThread; import android.annotation.NonNull; import android.annotation.UserIdInt; import android.annotation.WorkerThread; -import android.os.Handler; import android.os.Process; import android.util.IntArray; +import android.util.Slog; import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; @@ -36,7 +36,10 @@ import java.util.concurrent.locks.ReentrantLock; * persistent storages. */ final class AdditionalSubtypeMapRepository { - @GuardedBy("ImfLock.class") + private static final String TAG = "AdditionalSubtypeMapRepository"; + + // TODO(b/352594784): Should we user other lock primitives? + @GuardedBy("sPerUserMap") @NonNull private static final SparseArray<AdditionalSubtypeMap> sPerUserMap = new SparseArray<>(); @@ -192,29 +195,77 @@ final class AdditionalSubtypeMapRepository { private AdditionalSubtypeMapRepository() { } + /** + * Returns {@link AdditionalSubtypeMap} for the given user. + * + * <p>This method is expected be called after {@link #ensureInitializedAndGet(int)}. Otherwise + * {@link AdditionalSubtypeMap#EMPTY_MAP} will be returned.</p> + * + * @param userId the user to be queried about + * @return {@link AdditionalSubtypeMap} for the given user + */ + @AnyThread @NonNull - @GuardedBy("ImfLock.class") static AdditionalSubtypeMap get(@UserIdInt int userId) { - final AdditionalSubtypeMap map = sPerUserMap.get(userId); - if (map != null) { - return map; + final AdditionalSubtypeMap map; + synchronized (sPerUserMap) { + map = sPerUserMap.get(userId); + } + if (map == null) { + Slog.e(TAG, "get(userId=" + userId + ") is called before loadInitialDataAndGet()." + + " Returning an empty map"); + return AdditionalSubtypeMap.EMPTY_MAP; + } + return map; + } + + /** + * Ensures that {@link AdditionalSubtypeMap} is initialized for the given user. Load it from + * the persistent storage if {@link #putAndSave(int, AdditionalSubtypeMap, InputMethodMap)} has + * not been called yet. + * + * @param userId the user to be initialized + * @return {@link AdditionalSubtypeMap} that is associated with the given user. If + * {@link #putAndSave(int, AdditionalSubtypeMap, InputMethodMap)} is already called + * then the given {@link AdditionalSubtypeMap}. + */ + @AnyThread + @NonNull + static AdditionalSubtypeMap ensureInitializedAndGet(@UserIdInt int userId) { + final var map = AdditionalSubtypeUtils.load(userId); + synchronized (sPerUserMap) { + final AdditionalSubtypeMap previous = sPerUserMap.get(userId); + // If putAndSave() has already been called, then use it. + if (previous != null) { + return previous; + } + sPerUserMap.put(userId, map); } - final AdditionalSubtypeMap newMap = AdditionalSubtypeUtils.load(userId); - sPerUserMap.put(userId, newMap); - return newMap; + return map; } - @GuardedBy("ImfLock.class") + /** + * Puts {@link AdditionalSubtypeMap} for the given user then schedule an I/O task to save it + * to the storage. + * + * @param userId the user for the given {@link AdditionalSubtypeMap} is to be saved + * @param map {@link AdditionalSubtypeMap} to be saved + * @param inputMethodMap {@link InputMethodMap} to be used while saving the data + */ + @AnyThread static void putAndSave(@UserIdInt int userId, @NonNull AdditionalSubtypeMap map, @NonNull InputMethodMap inputMethodMap) { - final AdditionalSubtypeMap previous = sPerUserMap.get(userId); - if (previous == map) { - return; + synchronized (sPerUserMap) { + final AdditionalSubtypeMap previous = sPerUserMap.get(userId); + if (previous == map) { + return; + } + sPerUserMap.put(userId, map); + sWriter.scheduleWriteTask(userId, map, inputMethodMap); } - sPerUserMap.put(userId, map); - sWriter.scheduleWriteTask(userId, map, inputMethodMap); } + @AnyThread static void startWriterThread() { sWriter.startThread(); } @@ -225,12 +276,10 @@ final class AdditionalSubtypeMapRepository { } @AnyThread - static void remove(@UserIdInt int userId, @NonNull Handler ioHandler) { - sWriter.onUserRemoved(userId); - ioHandler.post(() -> { - synchronized (ImfLock.class) { - sPerUserMap.remove(userId); - } - }); + static void remove(@UserIdInt int userId) { + synchronized (sPerUserMap) { + sWriter.onUserRemoved(userId); + sPerUserMap.remove(userId); + } } } diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java index f61ca61c1e04..c82e5be7c643 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java @@ -16,6 +16,8 @@ package com.android.server.inputmethod; +import static com.android.server.inputmethod.InputMethodUtils.NOT_A_SUBTYPE_ID; + import static java.lang.annotation.RetentionPolicy.SOURCE; import android.annotation.NonNull; @@ -110,7 +112,7 @@ public abstract class InputMethodManagerInternal { InlineSuggestionsRequestInfo requestInfo, InlineSuggestionsRequestCallback cb); /** - * Force switch to the enabled input method by {@code imeId} for current user. If the input + * 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. * * @param imeId the input method ID to be switched to @@ -119,7 +121,25 @@ public abstract class InputMethodManagerInternal { * method by {@code imeId}; {@code false} the input method with {@code imeId} is not available * to be switched. */ - public abstract boolean switchToInputMethod(String imeId, @UserIdInt int userId); + public boolean switchToInputMethod(@NonNull String imeId, @UserIdInt int userId) { + return switchToInputMethod(imeId, NOT_A_SUBTYPE_ID, 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. + * + * @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 + * @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, + @UserIdInt int userId); /** * Force enable or disable the input method associated with {@code imeId} for given user. If @@ -211,6 +231,15 @@ public abstract class InputMethodManagerInternal { public abstract void updateImeWindowStatus(boolean disableImeIcon, int displayId); /** + * Updates and reports whether the IME switcher button should be shown, regardless whether + * SystemUI or the IME is responsible for drawing it and the corresponding navigation bar. + * + * @param displayId the display for which to update the IME switcher button visibility. + * @param userId the user for which to update the IME switcher button visibility. + */ + public abstract void updateShouldShowImeSwitcher(int displayId, @UserIdInt int userId); + + /** * Finish stylus handwriting by calling {@link InputMethodService#finishStylusHandwriting()} if * there is an ongoing handwriting session. */ @@ -290,7 +319,8 @@ public abstract class InputMethodManagerInternal { } @Override - public boolean switchToInputMethod(String imeId, @UserIdInt int userId) { + public boolean switchToInputMethod(@NonNull String imeId, int subtypeId, + @UserIdInt int userId) { return false; } @@ -335,6 +365,10 @@ public abstract class InputMethodManagerInternal { } @Override + public void updateShouldShowImeSwitcher(int displayId, @UserIdInt int userId) { + } + + @Override public void onSessionForAccessibilityCreated(int accessibilityConnectionId, IAccessibilityInputMethodSession session, @UserIdInt int userId) { } diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 85af7ab8a10f..a9e9dcfa6b2f 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -22,8 +22,6 @@ import static android.os.IServiceManager.DUMP_FLAG_PRIORITY_NORMAL; import static android.os.IServiceManager.DUMP_FLAG_PROTO; import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; import static android.os.UserManager.USER_TYPE_SYSTEM_HEADLESS; -import static android.provider.Settings.Secure.STYLUS_HANDWRITING_DEFAULT_VALUE; -import static android.provider.Settings.Secure.STYLUS_HANDWRITING_ENABLED; import static android.server.inputmethod.InputMethodManagerServiceProto.BACK_DISPOSITION; import static android.server.inputmethod.InputMethodManagerServiceProto.BOUND_TO_METHOD; import static android.server.inputmethod.InputMethodManagerServiceProto.CONCURRENT_MULTI_USER_MODE_ENABLED; @@ -51,6 +49,7 @@ 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.InputMethodBindingController.TIME_TO_RECONNECT; import static com.android.server.inputmethod.InputMethodSubtypeSwitchingController.MODE_AUTO; @@ -181,6 +180,7 @@ import com.android.server.SystemService; import com.android.server.companion.virtual.VirtualDeviceManagerInternal; import com.android.server.input.InputManagerInternal; import com.android.server.inputmethod.InputMethodManagerInternal.InputMethodListListener; +import com.android.server.inputmethod.InputMethodMenuControllerNew.MenuItem; import com.android.server.inputmethod.InputMethodSubtypeSwitchingController.ImeSubtypeListItem; import com.android.server.pm.UserManagerInternal; import com.android.server.statusbar.StatusBarManagerInternal; @@ -218,6 +218,13 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. static final String TAG = "InputMethodManagerService"; public static final String PROTO_ARG = "--proto"; + /** + * Timeout in milliseconds in {@link #systemRunning()} to make sure that users are initialized + * in {@link Lifecycle#initializeUsersAsync(int[])}. + */ + @DurationMillisLong + private static final long SYSTEM_READY_USER_INIT_TIMEOUT = 3000; + @Retention(SOURCE) @IntDef({ShellCommandResult.SUCCESS, ShellCommandResult.FAILURE}) private @interface ShellCommandResult { @@ -360,6 +367,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. private final UserManagerInternal mUserManagerInternal; @MultiUserUnawareField private final InputMethodMenuController mMenuController; + private final InputMethodMenuControllerNew mMenuControllerNew; @GuardedBy("ImfLock.class") @MultiUserUnawareField @@ -566,7 +574,9 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } switch (key) { case Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD: { - mMenuController.updateKeyboardFromSettingsLocked(); + if (!Flags.imeSwitcherRevamp()) { + mMenuController.updateKeyboardFromSettingsLocked(); + } break; } case Settings.Secure.ACCESSIBILITY_SOFT_KEYBOARD_MODE: { @@ -586,7 +596,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } break; } - case STYLUS_HANDWRITING_ENABLED: { + case Settings.Secure.STYLUS_HANDWRITING_ENABLED: { InputMethodManager.invalidateLocalStylusHandwritingAvailabilityCaches(); InputMethodManager .invalidateLocalConnectionlessStylusHandwritingAvailabilityCaches(); @@ -631,7 +641,15 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } } } - mMenuController.hideInputMethodMenu(); + if (Flags.imeSwitcherRevamp()) { + synchronized (ImfLock.class) { + final var bindingController = getInputMethodBindingController(senderUserId); + mMenuControllerNew.hide(bindingController.getCurTokenDisplayId(), + senderUserId); + } + } else { + mMenuController.hideInputMethodMenu(); + } } else { Slog.w(TAG, "Unexpected intent " + intent); } @@ -683,24 +701,9 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. super(true); } - @GuardedBy("ImfLock.class") - private boolean isChangingPackagesOfCurrentUserLocked() { - final int userId = getChangingUserId(); - final boolean retval = userId == mCurrentUserId; - if (DEBUG) { - if (!retval) { - Slog.d(TAG, "--- ignore this call back from a background user: " + userId); - } - } - return retval; - } - @Override public boolean onHandleForceStop(Intent intent, String[] packages, int uid, boolean doit) { synchronized (ImfLock.class) { - if (!isChangingPackagesOfCurrentUserLocked()) { - return false; - } final int userId = getChangingUserId(); final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); String curInputMethodId = settings.getSelectedInputMethod(); @@ -1023,7 +1026,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. // Called directly from UserManagerService. Do not block the calling thread. final int userId = user.id; SecureSettingsWrapper.onUserRemoved(userId); - AdditionalSubtypeMapRepository.remove(userId, mService.mIoHandler); + AdditionalSubtypeMapRepository.remove(userId); InputMethodSettingsRepository.remove(userId); mService.mUserDataRepository.remove(userId); } @@ -1054,39 +1057,31 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @AnyThread private void initializeUsersAsync(@UserIdInt int[] userIds) { + Slog.d(TAG, "Schedule initialization for users=" + Arrays.toString(userIds)); mService.mIoHandler.post(() -> { final var service = mService; final var context = service.mContext; final var userManagerInternal = service.mUserManagerInternal; - // We first create InputMethodMap for each user without loading AdditionalSubtypes. - final int numUsers = userIds.length; - final InputMethodMap[] rawMethodMaps = new InputMethodMap[numUsers]; - for (int i = 0; i < numUsers; ++i) { - final int userId = userIds[i]; - rawMethodMaps[i] = InputMethodManagerService.queryInputMethodServicesInternal( - context, userId, AdditionalSubtypeMap.EMPTY_MAP, + for (int userId : userIds) { + Slog.d(TAG, "Start initialization for user=" + userId); + final var additionalSubtypeMap = + AdditionalSubtypeMapRepository.ensureInitializedAndGet(userId); + final var settings = InputMethodManagerService.queryInputMethodServicesInternal( + context, userId, additionalSubtypeMap, DirectBootAwareness.AUTO).getMethodMap(); + InputMethodSettingsRepository.put(userId, + InputMethodSettings.create(settings, userId)); + final int profileParentId = userManagerInternal.getProfileParentId(userId); final boolean value = InputMethodDrawsNavBarResourceMonitor.evaluate(context, profileParentId); final var userData = mService.getUserData(userId); userData.mImeDrawsNavBar.set(value); - } - // Then create full InputMethodMap for each user. Note that - // AdditionalSubtypeMapRepository#get() and InputMethodSettingsRepository#put() - // need to be called with ImfLock held (b/352387655). - // TODO(b/343601565): Avoid ImfLock after fixing b/352387655. - synchronized (ImfLock.class) { - for (int i = 0; i < numUsers; ++i) { - final int userId = userIds[i]; - final var map = AdditionalSubtypeMapRepository.get(userId); - final var methodMap = rawMethodMaps[i].applyAdditionalSubtypes(map); - final var settings = InputMethodSettings.create(methodMap, userId); - InputMethodSettingsRepository.put(userId, settings); - } + userData.mBackgroundLoadLatch.countDown(); + Slog.d(TAG, "Complete initialization for user=" + userId); } }); } @@ -1103,13 +1098,9 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. final InputMethodSettings newSettings = queryInputMethodServicesInternal(mContext, userId, AdditionalSubtypeMapRepository.get(userId), DirectBootAwareness.AUTO); InputMethodSettingsRepository.put(userId, newSettings); - if (mCurrentUserId == userId) { - // We need to rebuild IMEs. - postInputMethodSettingUpdatedLocked(false /* resetDefaultEnabledIme */, userId); - updateInputMethodsFromSettingsLocked(true /* enabledChanged */, userId); - } else if (mConcurrentMultiUserModeEnabled) { - initializeVisibleBackgroundUserLocked(userId); - } + // We need to rebuild IMEs. + postInputMethodSettingUpdatedLocked(false /* resetDefaultEnabledIme */, userId); + updateInputMethodsFromSettingsLocked(true /* enabledChanged */, userId); } } @@ -1171,6 +1162,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. : bindingControllerFactory); mMenuController = new InputMethodMenuController(this); + mMenuControllerNew = Flags.imeSwitcherRevamp() + ? new InputMethodMenuControllerNew() : null; mVisibilityStateComputer = new ImeVisibilityStateComputer(this); mVisibilityApplier = new DefaultImeVisibilityApplier(this); @@ -1331,10 +1324,42 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } } + private void waitForUserInitialization() { + final int[] userIds = mUserManagerInternal.getUserIds(); + final long deadlineNanos = SystemClock.elapsedRealtimeNanos() + + TimeUnit.MILLISECONDS.toNanos(SYSTEM_READY_USER_INIT_TIMEOUT); + boolean interrupted = false; + try { + for (int userId : userIds) { + final var latch = getUserData(userId).mBackgroundLoadLatch; + boolean awaitResult; + while (true) { + try { + final long remainingNanos = + Math.max(deadlineNanos - SystemClock.elapsedRealtimeNanos(), 0); + awaitResult = latch.await(remainingNanos, TimeUnit.NANOSECONDS); + break; + } catch (InterruptedException ignored) { + interrupted = true; + } + } + if (!awaitResult) { + Slog.w(TAG, "Timed out for user#" + userId + " to be initialized"); + } + } + } finally { + if (interrupted) { + Thread.currentThread().interrupt(); + } + } + } + /** * TODO(b/32343335): The entire systemRunning() method needs to be revisited. */ public void systemRunning() { + waitForUserInitialization(); + synchronized (ImfLock.class) { if (DEBUG) { Slog.d(TAG, "--- systemReady"); @@ -1381,16 +1406,13 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. final String defaultImiId = SecureSettingsWrapper.getString( Settings.Secure.DEFAULT_INPUT_METHOD, null, currentUserId); final boolean imeSelectedOnBoot = !TextUtils.isEmpty(defaultImiId); - final InputMethodSettings newSettings = queryInputMethodServicesInternal(mContext, - currentUserId, AdditionalSubtypeMapRepository.get(currentUserId), - DirectBootAwareness.AUTO); - InputMethodSettingsRepository.put(currentUserId, newSettings); + final var settings = InputMethodSettingsRepository.get(currentUserId); postInputMethodSettingUpdatedLocked( !imeSelectedOnBoot /* resetDefaultEnabledIme */, currentUserId); updateFromSettingsLocked(true, currentUserId); InputMethodUtils.setNonSelectedSystemImesDisabledUntilUsed( getPackageManagerForUser(mContext, currentUserId), - newSettings.getEnabledInputMethodList()); + settings.getEnabledInputMethodList()); AdditionalSubtypeMapRepository.startWriterThread(); @@ -1410,9 +1432,9 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. (windowToken, imeVisible) -> { if (Flags.refactorInsetsController()) { if (imeVisible) { - showSoftInputInternal(windowToken); + showCurrentInputInternal(windowToken); } else { - hideSoftInputInternal(windowToken); + hideCurrentInputInternal(windowToken); } } }); @@ -1586,8 +1608,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. // If user is a profile, use preference of it`s parent profile. final int profileParentUserId = mUserManagerInternal.getProfileParentId(userId); if (Settings.Secure.getIntForUser(context.getContentResolver(), - STYLUS_HANDWRITING_ENABLED, STYLUS_HANDWRITING_DEFAULT_VALUE, - profileParentUserId) == 0) { + Settings.Secure.STYLUS_HANDWRITING_ENABLED, + Settings.Secure.STYLUS_HANDWRITING_DEFAULT_VALUE, profileParentUserId) == 0) { return false; } return true; @@ -1782,7 +1804,11 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. ImeTracker.PHASE_SERVER_WAIT_IME); userData.mCurStatsToken = null; // TODO: Make mMenuController multi-user aware - mMenuController.hideInputMethodMenuLocked(); + if (Flags.imeSwitcherRevamp()) { + mMenuControllerNew.hide(bindingController.getCurTokenDisplayId(), userId); + } else { + mMenuController.hideInputMethodMenuLocked(); + } } } @@ -1884,7 +1910,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. if (Flags.refactorInsetsController()) { if (isShowRequestedForCurrentWindow(userId) && userData.mImeBindingState != null && userData.mImeBindingState.mFocusedWindow != null) { - showSoftInputInternal(userData.mImeBindingState.mFocusedWindow); + showCurrentInputInternal(userData.mImeBindingState.mFocusedWindow); } } else { if (isShowRequestedForCurrentWindow(userId)) { @@ -2599,7 +2625,12 @@ 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. - if (mMenuController.getSwitchingDialogLocked() != null) return false; + final boolean switcherMenuShowing = Flags.imeSwitcherRevamp() + ? mMenuControllerNew.isShowing() + : mMenuController.getSwitchingDialogLocked() != null; + if (switcherMenuShowing) { + return false; + } // When we are switching IMEs, the IME switcher button should be hidden. final var bindingController = getInputMethodBindingController(userId); if (!Objects.equals(bindingController.getCurId(), @@ -2614,7 +2645,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. || (visibility & InputMethodService.IME_INVISIBLE) != 0) { return false; } - if (mWindowManagerInternal.isHardKeyboardAvailable()) { + 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 @@ -2625,6 +2656,15 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); + 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()); + if (curImi != null && curImi.createImeLanguageSettingsActivityIntent() != null) { + return true; + } + } + return hasMultipleSubtypesForSwitcher(false /* nonAuxOnly */, settings); } @@ -2794,7 +2834,10 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } final var curId = bindingController.getCurId(); // TODO(b/305849394): Make mMenuController multi-user aware. - if (mMenuController.getSwitchingDialogLocked() != null + final boolean switcherMenuShowing = Flags.imeSwitcherRevamp() + ? mMenuControllerNew.isShowing() + : mMenuController.getSwitchingDialogLocked() != null; + if (switcherMenuShowing || !Objects.equals(curId, bindingController.getSelectedMethodId())) { // When the IME switcher dialog is shown, or we are switching IMEs, // the back button should be in the default state (as if the IME is not shown). @@ -2813,7 +2856,9 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @GuardedBy("ImfLock.class") void updateFromSettingsLocked(boolean enabledMayChange, @UserIdInt int userId) { updateInputMethodsFromSettingsLocked(enabledMayChange, userId); - mMenuController.updateKeyboardFromSettingsLocked(); + if (!Flags.imeSwitcherRevamp()) { + mMenuController.updateKeyboardFromSettingsLocked(); + } } /** @@ -3097,8 +3142,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } } - boolean showSoftInputInternal(IBinder windowToken) { - Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.showSoftInputInternal"); + boolean showCurrentInputInternal(IBinder windowToken) { + Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.showCurrentInputInternal"); ImeTracing.getInstance().triggerManagerServiceDump( "InputMethodManagerService#showSoftInput", mDumper); synchronized (ImfLock.class) { @@ -3117,8 +3162,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } } - boolean hideSoftInputInternal(IBinder windowToken) { - Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.hideSoftInputInternal"); + boolean hideCurrentInputInternal(IBinder windowToken) { + Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.hideCurrentInputInternal"); ImeTracing.getInstance().triggerManagerServiceDump( "InputMethodManagerService#hideSoftInput", mDumper); synchronized (ImfLock.class) { @@ -3979,10 +4024,70 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @IInputMethodManagerImpl.PermissionVerified(Manifest.permission.TEST_INPUT_METHOD) public boolean isInputMethodPickerShownForTest() { synchronized (ImfLock.class) { - return mMenuController.isisInputMethodPickerShownForTestLocked(); + return Flags.imeSwitcherRevamp() + ? mMenuControllerNew.isShowing() + : mMenuController.isisInputMethodPickerShownForTestLocked(); } } + /** + * 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. + * @return the list of menu items, and the index of the selected item, + * or {@code -1} if no item is selected. + */ + @GuardedBy("ImfLock.class") + @NonNull + private Pair<List<MenuItem>, Integer> getInputMethodPickerItems( + @NonNull List<ImeSubtypeListItem> items, @Nullable String selectedImeId, + int selectedSubtypeId, @UserIdInt int userId) { + final var bindingController = getInputMethodBindingController(userId); + final var settings = InputMethodSettingsRepository.get(userId); + + if (selectedSubtypeId == NOT_A_SUBTYPE_ID) { + // 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( + curImi, curSubtype.hashCode()); + } + } + + // No item is selected by default. When we have a list of explicitly enabled + // subtypes, the implicit subtype is no longer listed. If the implicit one + // is still selected, no items will be shown as selected. + int selectedIndex = -1; + String prevImeId = null; + final var menuItems = new ArrayList<MenuItem>(); + for (int i = 0; i < items.size(); i++) { + final var item = items.get(i); + final var imeId = item.mImi.getId(); + if (imeId.equals(selectedImeId)) { + final int subtypeId = item.mSubtypeId; + // Check if this is the selected IME-subtype pair. + if ((subtypeId == 0 && selectedSubtypeId == NOT_A_SUBTYPE_ID) + || subtypeId == NOT_A_SUBTYPE_ID + || subtypeId == selectedSubtypeId) { + 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)); + } + + return new Pair<>(menuItems, selectedIndex); + } + @BinderThread private void onImeSwitchButtonClickFromClient(@NonNull IBinder token, int displayId, @NonNull UserData userData) { @@ -4625,7 +4730,10 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. proto.write(IS_INTERACTIVE, mIsInteractive); proto.write(BACK_DISPOSITION, bindingController.getBackDisposition()); proto.write(IME_WINDOW_VISIBILITY, bindingController.getImeWindowVis()); - proto.write(SHOW_IME_WITH_HARD_KEYBOARD, mMenuController.getShowImeWithHardKeyboard()); + if (!Flags.imeSwitcherRevamp()) { + proto.write(SHOW_IME_WITH_HARD_KEYBOARD, + mMenuController.getShowImeWithHardKeyboard()); + } proto.write(CONCURRENT_MULTI_USER_MODE_ENABLED, mConcurrentMultiUserModeEnabled); proto.end(token); } @@ -4670,8 +4778,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. final IBinder requestToken = mVisibilityStateComputer.getWindowTokenFrom( windowToken, userId); mVisibilityApplier.applyImeVisibility(requestToken, statsToken, - setVisible ? ImeVisibilityStateComputer.STATE_SHOW_IME - : ImeVisibilityStateComputer.STATE_HIDE_IME, + setVisible ? STATE_SHOW_IME : STATE_HIDE_IME, SoftInputShowHideReason.NOT_SET /* ignore reason */, userId); } } finally { @@ -4931,8 +5038,9 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. synchronized (ImfLock.class) { final InputMethodSettings settings = InputMethodSettingsRepository.get(mCurrentUserId); + final int userId = settings.getUserId(); final boolean isScreenLocked = mWindowManagerInternal.isKeyguardLocked() - && mWindowManagerInternal.isKeyguardSecure(settings.getUserId()); + && mWindowManagerInternal.isKeyguardSecure(userId); final String lastInputMethodId = settings.getSelectedInputMethod(); int lastInputMethodSubtypeId = settings.getSelectedInputMethodSubtypeId(lastInputMethodId); @@ -4945,12 +5053,35 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. Slog.w(TAG, "Show switching menu failed, imList is empty," + " showAuxSubtypes: " + showAuxSubtypes + " isScreenLocked: " + isScreenLocked - + " userId: " + settings.getUserId()); + + " userId: " + userId); return false; } - mMenuController.showInputMethodMenuLocked(showAuxSubtypes, displayId, - lastInputMethodId, lastInputMethodSubtypeId, imList); + if (Flags.imeSwitcherRevamp()) { + if (DEBUG) { + Slog.v(TAG, "Show IME switcher menu," + + " showAuxSubtypes=" + showAuxSubtypes + + " displayId=" + displayId + + " preferredInputMethodId=" + lastInputMethodId + + " preferredInputMethodSubtypeId=" + lastInputMethodSubtypeId); + } + + final var itemsAndIndex = getInputMethodPickerItems(imList, + lastInputMethodId, lastInputMethodSubtypeId, 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); + } + + mMenuControllerNew.show(menuItems, selectedIndex, displayId, userId); + } else { + mMenuController.showInputMethodMenuLocked(showAuxSubtypes, displayId, + lastInputMethodId, lastInputMethodSubtypeId, imList); + } } return true; @@ -5021,7 +5152,9 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. // -------------------------------------------------------------- case MSG_HARD_KEYBOARD_SWITCH_CHANGED: - mMenuController.handleHardKeyboardStatusChange(msg.arg1 == 1); + if (!Flags.imeSwitcherRevamp()) { + mMenuController.handleHardKeyboardStatusChange(msg.arg1 == 1); + } synchronized (ImfLock.class) { sendOnNavButtonFlagsChangedToAllImesLocked(); } @@ -5591,7 +5724,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } @GuardedBy("ImfLock.class") - private boolean switchToInputMethodLocked(String imeId, @UserIdInt int userId) { + private boolean switchToInputMethodLocked(@NonNull String imeId, int subtypeId, + @UserIdInt int userId) { final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); if (mConcurrentMultiUserModeEnabled || userId == mCurrentUserId) { if (!settings.getMethodMap().containsKey(imeId) @@ -5599,7 +5733,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. .contains(settings.getMethodMap().get(imeId))) { return false; // IME is not found or not enabled. } - setInputMethodLocked(imeId, NOT_A_SUBTYPE_ID, userId); + setInputMethodLocked(imeId, subtypeId, userId); return true; } if (!settings.getMethodMap().containsKey(imeId) @@ -5608,6 +5742,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. return false; // IME is not found or not enabled. } settings.putSelectedInputMethod(imeId); + // For non-current user, only reset subtypeId (instead of setting the given one). settings.putSelectedSubtype(NOT_A_SUBTYPE_ID); return true; } @@ -5753,9 +5888,10 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } @Override - public boolean switchToInputMethod(String imeId, @UserIdInt int userId) { + public boolean switchToInputMethod(@NonNull String imeId, int subtypeId, + @UserIdInt int userId) { synchronized (ImfLock.class) { - return switchToInputMethodLocked(imeId, userId); + return switchToInputMethodLocked(imeId, subtypeId, userId); } } @@ -5852,7 +5988,12 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. // input target changed, in case seeing the dialog dismiss flickering during // the next focused window starting the input connection. if (mLastImeTargetWindow != userData.mImeBindingState.mFocusedWindow) { - mMenuController.hideInputMethodMenuLocked(); + if (Flags.imeSwitcherRevamp()) { + final var bindingController = getInputMethodBindingController(userId); + mMenuControllerNew.hide(bindingController.getCurTokenDisplayId(), userId); + } else { + mMenuController.hideInputMethodMenuLocked(); + } } } } @@ -5871,6 +6012,15 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } @Override + public void updateShouldShowImeSwitcher(int displayId, @UserIdInt int userId) { + synchronized (ImfLock.class) { + updateSystemUiLocked(userId); + final var userData = getUserData(userId); + sendOnNavButtonFlagsChangedLocked(userData); + } + } + + @Override public void onSessionForAccessibilityCreated(int accessibilityConnectionId, IAccessibilityInputMethodSession session, @UserIdInt int userId) { synchronized (ImfLock.class) { @@ -6114,7 +6264,6 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. boolean isCritical) { IInputMethodInvoker method; ClientState client; - ClientState focusedWindowClient; final Printer p = new PrintWriterPrinter(pw); @@ -6192,6 +6341,10 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. }; mUserDataRepository.forAllUserData(userDataDump); + if (Flags.imeSwitcherRevamp()) { + p.println(" menuControllerNew:"); + mMenuControllerNew.dump(p, " "); + } p.println(" mCurToken=" + bindingController.getCurToken()); p.println(" mCurTokenDisplayId=" + bindingController.getCurTokenDisplayId()); p.println(" mCurHostInputToken=" + bindingController.getCurHostInputToken()); @@ -6638,7 +6791,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. continue; } boolean failedToSelectUnknownIme = !switchToInputMethodLocked(imeId, - userId); + NOT_A_SUBTYPE_ID, userId); if (failedToSelectUnknownIme) { error.print("Unknown input method "); error.print(imeId); diff --git a/services/core/java/com/android/server/inputmethod/InputMethodMenuControllerNew.java b/services/core/java/com/android/server/inputmethod/InputMethodMenuControllerNew.java new file mode 100644 index 000000000000..045414bde82e --- /dev/null +++ b/services/core/java/com/android/server/inputmethod/InputMethodMenuControllerNew.java @@ -0,0 +1,356 @@ +/* + * 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.inputmethod; + + +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 android.annotation.IntRange; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.RequiresPermission; +import android.annotation.UserIdInt; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.UserHandle; +import android.text.TextUtils; +import android.util.Printer; +import android.util.Slog; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; +import android.view.inputmethod.InputMethodInfo; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.TextView; + +import com.android.internal.widget.RecyclerView; + +import java.util.List; + +/** + * Controller for showing and hiding the Input Method Switcher Menu. + */ +final class InputMethodMenuControllerNew { + + private static final String TAG = InputMethodMenuControllerNew.class.getSimpleName(); + + /** + * The horizontal offset from the menu to the edge of the screen corresponding + * to {@link Gravity#END}. + */ + private static final int HORIZONTAL_OFFSET = 16; + + /** The title of the window, used for debugging. */ + private static final String WINDOW_TITLE = "IME Switcher Menu"; + + private final InputMethodDialogWindowContext mDialogWindowContext = + new InputMethodDialogWindowContext(); + + @Nullable + private AlertDialog mDialog; + + @Nullable + private List<MenuItem> mMenuItems; + + /** + * Shows the Input Method Switcher Menu, with a list of IMEs and their subtypes. + * + * @param items the list of menu items. + * @param selectedIndex the index of the menu item that is selected. + * If no other IMEs are enabled, this index will be out of reach. + * @param displayId the ID of the display where the menu was requested. + * @param userId the ID of the user that requested the menu. + */ + @RequiresPermission(allOf = {INTERACT_ACROSS_USERS, HIDE_OVERLAY_WINDOWS}) + void show(@NonNull List<MenuItem> items, int selectedIndex, int displayId, + @UserIdInt int userId) { + // Hide the menu in case it was already showing. + hide(displayId, userId); + + final Context dialogWindowContext = mDialogWindowContext.get(displayId); + final var builder = new AlertDialog.Builder(dialogWindowContext, + com.android.internal.R.style.Theme_DeviceDefault_InputMethodSwitcherDialog); + final var inflater = LayoutInflater.from(builder.getContext()); + + // Create the content view. + final View contentView = inflater + .inflate(com.android.internal.R.layout.input_method_switch_dialog_new, null); + contentView.setAccessibilityPaneTitle( + dialogWindowContext.getText(com.android.internal.R.string.select_input_method)); + builder.setView(contentView); + + final DialogInterface.OnClickListener onClickListener = (dialog, which) -> { + if (which != selectedIndex) { + final var item = items.get(which); + InputMethodManagerInternal.get() + .switchToInputMethod(item.mImi.getId(), item.mSubtypeId, userId); + } + hide(displayId, userId); + }; + + final var selectedImi = selectedIndex >= 0 ? items.get(selectedIndex).mImi : null; + final var languageSettingsIntent = selectedImi != null + ? selectedImi.createImeLanguageSettingsActivityIntent() : null; + final boolean hasLanguageSettingsButton = languageSettingsIntent != null; + if (hasLanguageSettingsButton) { + final View buttonBar = contentView + .requireViewById(com.android.internal.R.id.button_bar); + buttonBar.setVisibility(View.VISIBLE); + + languageSettingsIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + final Button languageSettingsButton = contentView + .requireViewById(com.android.internal.R.id.button1); + languageSettingsButton.setVisibility(View.VISIBLE); + languageSettingsButton.setOnClickListener(v -> { + v.getContext().startActivityAsUser(languageSettingsIntent, UserHandle.of(userId)); + hide(displayId, userId); + }); + } + + // Create the current IME subtypes list. + final RecyclerView recyclerView = contentView + .requireViewById(com.android.internal.R.id.list); + recyclerView.setAdapter(new Adapter(items, selectedIndex, inflater, onClickListener)); + // Scroll to the currently selected IME. + recyclerView.scrollToPosition(selectedIndex); + // Indicate that the list can be scrolled. + recyclerView.setScrollIndicators( + hasLanguageSettingsButton ? View.SCROLL_INDICATOR_BOTTOM : 0); + + builder.setOnCancelListener(dialog -> hide(displayId, userId)); + mMenuItems = items; + mDialog = builder.create(); + mDialog.setCanceledOnTouchOutside(true); + final Window w = mDialog.getWindow(); + w.setHideOverlayWindows(true); + final WindowManager.LayoutParams attrs = w.getAttributes(); + // Use an alternate token for the dialog for that window manager can group the token + // with other IME windows based on type vs. grouping based on whichever token happens + // to get selected by the system later on. + attrs.token = dialogWindowContext.getWindowContextToken(); + attrs.gravity = Gravity.getAbsoluteGravity(Gravity.BOTTOM | Gravity.END, + dialogWindowContext.getResources().getConfiguration().getLayoutDirection()); + attrs.x = HORIZONTAL_OFFSET; + attrs.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; + attrs.type = WindowManager.LayoutParams.TYPE_INPUT_METHOD_DIALOG; + // Used for debugging only, not user visible. + attrs.setTitle(WINDOW_TITLE); + w.setAttributes(attrs); + + mDialog.show(); + InputMethodManagerInternal.get().updateShouldShowImeSwitcher(displayId, userId); + } + + /** + * Hides the Input Method Switcher Menu. + * + * @param displayId the ID of the display from where the menu should be hidden. + * @param userId the ID of the user for which the menu should be hidden. + */ + void hide(int displayId, @UserIdInt int userId) { + if (DEBUG) Slog.v(TAG, "Hide IME switcher menu."); + + mMenuItems = null; + // Cannot use dialog.isShowing() here, as the cancel listener flow already resets mShowing. + if (mDialog != null) { + mDialog.dismiss(); + mDialog = null; + + InputMethodManagerInternal.get().updateShouldShowImeSwitcher(displayId, userId); + } + } + + /** + * Returns whether the Input Method Switcher Menu is showing. + */ + boolean isShowing() { + return mDialog != null && mDialog.isShowing(); + } + + void dump(@NonNull Printer pw, @NonNull String prefix) { + final boolean showing = isShowing(); + pw.println(prefix + " isShowing: " + showing); + + if (showing) { + pw.println(prefix + " menuItems: " + mMenuItems); + } + } + + /** + * Item to be shown in the Input Method Switcher Menu, containing an input method and + * optionally an input method subtype. + */ + static class MenuItem { + + /** The name of the input method. */ + @NonNull + private final CharSequence mImeName; + + /** + * The name of the input method subtype, or {@code null} if this item doesn't have a + * subtype. + */ + @Nullable + private final CharSequence mSubtypeName; + + /** The info of the input method. */ + @NonNull + private final InputMethodInfo mImi; + + /** + * 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. + */ + @IntRange(from = NOT_A_SUBTYPE_ID) + private final int mSubtypeId; + + /** Whether this item has a group header (only the first item of each input method). */ + private final boolean mHasHeader; + + /** + * Whether this item should has a group divider (same as {@link #mHasHeader}, + * excluding the first IME). + */ + 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) { + mImeName = imeName; + mSubtypeName = subtypeName; + mImi = imi; + mSubtypeId = subtypeId; + mHasHeader = hasHeader; + mHasDivider = hasDivider; + } + + @Override + public String toString() { + return "MenuItem{" + + "mImeName=" + mImeName + + " mSubtypeName=" + mSubtypeName + + " mSubtypeId=" + mSubtypeId + + " mHasHeader=" + mHasHeader + + " mHasDivider=" + mHasDivider + + "}"; + } + } + + private static class Adapter extends RecyclerView.Adapter<Adapter.ViewHolder> { + + /** The list of items to show. */ + @NonNull + private final List<MenuItem> mItems; + /** The index of the selected item. */ + private final int mSelectedIndex; + @NonNull + private final LayoutInflater mInflater; + @NonNull + private final DialogInterface.OnClickListener mOnClickListener; + + Adapter(@NonNull List<MenuItem> items, int selectedIndex, + @NonNull LayoutInflater inflater, + @NonNull DialogInterface.OnClickListener onClickListener) { + mItems = items; + mSelectedIndex = selectedIndex; + mInflater = inflater; + mOnClickListener = onClickListener; + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + final View view = mInflater.inflate( + com.android.internal.R.layout.input_method_switch_item_new, parent, false); + + return new ViewHolder(view, mOnClickListener); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + holder.bind(mItems.get(position), position == mSelectedIndex /* isSelected */); + } + + @Override + public int getItemCount() { + return mItems.size(); + } + + private static class ViewHolder extends RecyclerView.ViewHolder { + + /** The container of the item. */ + @NonNull + private final View mContainer; + /** The name of the item. */ + @NonNull + private final TextView mName; + /** Indicator for the selected status of the item. */ + @NonNull + private final ImageView mCheckmark; + /** The group header optionally drawn above the item. */ + @NonNull + private final TextView mHeader; + /** The group divider optionally drawn above the item. */ + @NonNull + private final View mDivider; + + private ViewHolder(@NonNull View itemView, + @NonNull DialogInterface.OnClickListener onClickListener) { + super(itemView); + + mContainer = itemView.requireViewById(com.android.internal.R.id.list_item); + mName = itemView.requireViewById(com.android.internal.R.id.text); + mCheckmark = itemView.requireViewById(com.android.internal.R.id.image); + mHeader = itemView.requireViewById(com.android.internal.R.id.header_text); + mDivider = itemView.requireViewById(com.android.internal.R.id.divider); + + mContainer.setOnClickListener((v) -> + onClickListener.onClick(null /* dialog */, getAdapterPosition())); + } + + /** + * Binds the given item to the current view. + * + * @param item the item to bind. + * @param isSelected whether this is selected. + */ + private void bind(@NonNull MenuItem item, boolean isSelected) { + // Use the IME name for subtypes with an empty subtype name. + final var name = TextUtils.isEmpty(item.mSubtypeName) + ? item.mImeName : item.mSubtypeName; + mContainer.setActivated(isSelected); + // Activated is the correct state, but we also set selected for accessibility info. + mContainer.setSelected(isSelected); + mName.setSelected(isSelected); + mName.setText(name); + mCheckmark.setVisibility(isSelected ? View.VISIBLE : View.GONE); + mHeader.setText(item.mImeName); + mHeader.setVisibility(item.mHasHeader ? View.VISIBLE : View.GONE); + mDivider.setVisibility(item.mHasDivider ? View.VISIBLE : View.GONE); + } + } + } +} diff --git a/services/core/java/com/android/server/inputmethod/InputMethodSettingsRepository.java b/services/core/java/com/android/server/inputmethod/InputMethodSettingsRepository.java index 50ba36450bda..1b840362a8cf 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodSettingsRepository.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodSettingsRepository.java @@ -24,7 +24,8 @@ import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; final class InputMethodSettingsRepository { - @GuardedBy("ImfLock.class") + // TODO(b/352594784): Should we user other lock primitives? + @GuardedBy("sPerUserMap") @NonNull private static final SparseArray<InputMethodSettings> sPerUserMap = new SparseArray<>(); @@ -35,23 +36,28 @@ final class InputMethodSettingsRepository { } @NonNull - @GuardedBy("ImfLock.class") + @AnyThread static InputMethodSettings get(@UserIdInt int userId) { - final InputMethodSettings obj = sPerUserMap.get(userId); + final InputMethodSettings obj; + synchronized (sPerUserMap) { + obj = sPerUserMap.get(userId); + } if (obj != null) { return obj; } return InputMethodSettings.createEmptyMap(userId); } - @GuardedBy("ImfLock.class") + @AnyThread static void put(@UserIdInt int userId, @NonNull InputMethodSettings obj) { - sPerUserMap.put(userId, obj); + synchronized (sPerUserMap) { + sPerUserMap.put(userId, obj); + } } @AnyThread static void remove(@UserIdInt int userId) { - synchronized (ImfLock.class) { + synchronized (sPerUserMap) { sPerUserMap.remove(userId); } } diff --git a/services/core/java/com/android/server/inputmethod/UserData.java b/services/core/java/com/android/server/inputmethod/UserData.java index ec5c9e6a3550..be5732174b1d 100644 --- a/services/core/java/com/android/server/inputmethod/UserData.java +++ b/services/core/java/com/android/server/inputmethod/UserData.java @@ -28,6 +28,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.inputmethod.IRemoteAccessibilityInputConnection; import com.android.internal.inputmethod.IRemoteInputConnection; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicBoolean; /** Placeholder for all IMMS user specific fields */ @@ -35,6 +36,13 @@ final class UserData { @UserIdInt final int mUserId; + /** + * Tells whether {@link InputMethodManagerService.Lifecycle#initializeUsersAsync(int[])} is + * completed for this user or not. + */ + @NonNull + final CountDownLatch mBackgroundLoadLatch = new CountDownLatch(1); + @NonNull final InputMethodBindingController mBindingController; diff --git a/services/core/java/com/android/server/location/LocationManagerService.java b/services/core/java/com/android/server/location/LocationManagerService.java index 6e991b4db2b1..2e167efc4d81 100644 --- a/services/core/java/com/android/server/location/LocationManagerService.java +++ b/services/core/java/com/android/server/location/LocationManagerService.java @@ -376,6 +376,11 @@ public class LocationManagerService extends ILocationManager.Stub implements mContext.getContentResolver(), Settings.Global.LOCATION_ENABLE_STATIONARY_THROTTLE, defaultStationaryThrottlingSetting) != 0; + if (Flags.disableStationaryThrottling() && !( + Flags.keepGnssStationaryThrottling() && enableStationaryThrottling + && GPS_PROVIDER.equals(manager.getName()))) { + enableStationaryThrottling = false; + } if (enableStationaryThrottling) { realProvider = new StationaryThrottlingLocationProvider(manager.getName(), mInjector, realProvider); diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubService.java b/services/core/java/com/android/server/location/contexthub/ContextHubService.java index ed451ff0c194..7de10452191e 100644 --- a/services/core/java/com/android/server/location/contexthub/ContextHubService.java +++ b/services/core/java/com/android/server/location/contexthub/ContextHubService.java @@ -333,8 +333,14 @@ public class ContextHubService extends IContextHubService.Stub { return new IContextHubClientCallback.Stub() { private void finishCallback() { try { - IContextHubClient client = mDefaultClientMap.get(contextHubId); - client.callbackFinished(); + if (mDefaultClientMap != null && mDefaultClientMap.containsKey(contextHubId)) { + IContextHubClient client = mDefaultClientMap.get(contextHubId); + client.callbackFinished(); + } else { + Log.e(TAG, "Default client not found for hub (ID = " + contextHubId + "): " + + mDefaultClientMap == null ? "map was null" + : "map did not contain the hub"); + } } catch (RemoteException e) { Log.e( TAG, diff --git a/services/core/java/com/android/server/locksettings/LockSettingsShellCommand.java b/services/core/java/com/android/server/locksettings/LockSettingsShellCommand.java index 17f2fcc5b9d8..bb35b378866b 100644 --- a/services/core/java/com/android/server/locksettings/LockSettingsShellCommand.java +++ b/services/core/java/com/android/server/locksettings/LockSettingsShellCommand.java @@ -340,6 +340,11 @@ class LockSettingsShellCommand extends ShellCommand { getOutPrintWriter().println("Profile uses unified challenge"); return false; } + if (mOld.isEmpty()) { + getOutPrintWriter().println( + "User has a lock credential, but old credential was not provided"); + return false; + } try { final boolean result = mLockPatternUtils.checkCredential(getOldCredential(), diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java index ba7d3b8c76d2..d9f36223c6dd 100644 --- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java +++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java @@ -251,6 +251,10 @@ class MediaRouter2ServiceImpl { systemRoutes = providerInfo.getRoutes(); } else { systemRoutes = Collections.emptyList(); + Slog.e( + TAG, + "Returning empty system routes list because " + + "system provider has null providerInfo."); } } else { systemRoutes = new ArrayList<>(); diff --git a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java index d9e22c5a270f..53b67969e91a 100644 --- a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java +++ b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java @@ -4118,7 +4118,7 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { fout.increaseIndent(); for (int i = 0; i < mSubscriptionPlans.size(); i++) { final int subId = mSubscriptionPlans.keyAt(i); - fout.println("Subscriber ID " + subId + ":"); + fout.println("Subscription ID " + subId + ":"); fout.increaseIndent(); final SubscriptionPlan[] plans = mSubscriptionPlans.valueAt(i); if (!ArrayUtils.isEmpty(plans)) { diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java index c078409f468c..b12a917eede9 100644 --- a/services/core/java/com/android/server/notification/ZenModeHelper.java +++ b/services/core/java/com/android/server/notification/ZenModeHelper.java @@ -1159,11 +1159,18 @@ public class ZenModeHelper { rule.conditionId = azr.getConditionId(); modified = true; } - boolean shouldPreserveCondition = Flags.modesApi() && Flags.modesUi() - && !isNew && origin == UPDATE_ORIGIN_USER - && rule.enabled == azr.isEnabled() - && rule.conditionId != null && rule.condition != null - && rule.conditionId.equals(rule.condition.id); + // This can be removed when {@link Flags#modesUi} is fully ramped up + final boolean isWatch = + mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH); + boolean shouldPreserveCondition = + Flags.modesApi() + && (Flags.modesUi() || isWatch) + && !isNew + && origin == UPDATE_ORIGIN_USER + && rule.enabled == azr.isEnabled() + && rule.conditionId != null + && rule.condition != null + && rule.conditionId.equals(rule.condition.id); if (!shouldPreserveCondition) { // Do not update 'modified'. If only this changes we treat it as a no-op updateAZR. rule.condition = null; diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsDumpHelperImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsDumpHelperImpl.java index ad146afe16e7..fb54c5de260a 100644 --- a/services/core/java/com/android/server/power/stats/BatteryStatsDumpHelperImpl.java +++ b/services/core/java/com/android/server/power/stats/BatteryStatsDumpHelperImpl.java @@ -20,6 +20,8 @@ import android.os.BatteryStats; import android.os.BatteryUsageStats; import android.os.BatteryUsageStatsQuery; +import com.android.server.power.optimization.Flags; + public class BatteryStatsDumpHelperImpl implements BatteryStats.BatteryStatsDumpHelper { private final BatteryUsageStatsProvider mBatteryUsageStatsProvider; @@ -33,6 +35,9 @@ public class BatteryStatsDumpHelperImpl implements BatteryStats.BatteryStatsDump .setMaxStatsAgeMs(0); if (detailed) { builder.includePowerModels().includeProcessStateData().includeVirtualUids(); + if (Flags.batteryUsageStatsByPowerAndScreenState()) { + builder.includePowerStateData().includeScreenStateData(); + } } return mBatteryUsageStatsProvider.getBatteryUsageStats((BatteryStatsImpl) batteryStats, builder.build()); diff --git a/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java b/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java index 8127b8217bb0..ac6896696de6 100644 --- a/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java +++ b/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java @@ -201,7 +201,8 @@ public class BatteryUsageStatsProvider { batteryUsageStatsBuilder = new BatteryUsageStats.Builder( stats.getCustomEnergyConsumerNames(), includePowerModels, - includeProcessStateData, minConsumedPowerThreshold); + includeProcessStateData, query.isScreenStateDataNeeded(), + query.isPowerStateDataNeeded(), minConsumedPowerThreshold); // TODO(b/188068523): use a monotonic clock to ensure resilience of order and duration // of batteryUsageStats sessions to wall-clock adjustments @@ -348,6 +349,7 @@ public class BatteryUsageStatsProvider { final String[] customEnergyConsumerNames = stats.getCustomEnergyConsumerNames(); final BatteryUsageStats.Builder builder = new BatteryUsageStats.Builder( customEnergyConsumerNames, includePowerModels, includeProcessStateData, + query.isScreenStateDataNeeded(), query.isPowerStateDataNeeded(), minConsumedPowerThreshold); if (mPowerStatsStore == null) { Log.e(TAG, "PowerStatsStore is unavailable"); @@ -408,7 +410,6 @@ public class BatteryUsageStatsProvider { + " does not include process state data"); continue; } - builder.add(snapshot); } } diff --git a/services/core/java/com/android/server/power/stats/PowerStatsExporter.java b/services/core/java/com/android/server/power/stats/PowerStatsExporter.java index 549a97ea49cd..0f1349287a0f 100644 --- a/services/core/java/com/android/server/power/stats/PowerStatsExporter.java +++ b/services/core/java/com/android/server/power/stats/PowerStatsExporter.java @@ -129,17 +129,55 @@ public class PowerStatsExporter { if (descriptor == null) { return; } + boolean isCustomComponent = + descriptor.powerComponentId >= BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID; PowerStatsLayout layout = new PowerStatsLayout(); layout.fromExtras(descriptor.extras); long[] deviceStats = new long[descriptor.statsArrayLength]; + for (int screenState = 0; screenState < BatteryConsumer.SCREEN_STATE_COUNT; screenState++) { + if (batteryUsageStatsBuilder.isScreenStateDataNeeded()) { + if (screenState == BatteryConsumer.SCREEN_STATE_UNSPECIFIED) { + continue; + } + } else if (screenState != BatteryConsumer.SCREEN_STATE_UNSPECIFIED) { + continue; + } + + for (int powerState = 0; powerState < BatteryConsumer.POWER_STATE_COUNT; powerState++) { + if (batteryUsageStatsBuilder.isPowerStateDataNeeded() && !isCustomComponent) { + if (powerState == BatteryConsumer.POWER_STATE_UNSPECIFIED) { + continue; + } + } else if (powerState != BatteryConsumer.POWER_STATE_BATTERY) { + continue; + } + + populateAggregatedBatteryConsumer(batteryUsageStatsBuilder, powerComponentStats, + layout, deviceStats, screenState, powerState); + } + } + if (layout.isUidPowerAttributionSupported()) { + populateBatteryConsumers(batteryUsageStatsBuilder, + powerComponentStats, layout); + } + } + + private static void populateAggregatedBatteryConsumer( + BatteryUsageStats.Builder batteryUsageStatsBuilder, + PowerComponentAggregatedPowerStats powerComponentStats, PowerStatsLayout layout, + long[] deviceStats, @BatteryConsumer.ScreenState int screenState, + @BatteryConsumer.PowerState int powerState) { + int powerComponentId = powerComponentStats.powerComponentId; + boolean isCustomComponent = + powerComponentId >= BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID; + double[] totalPower = new double[1]; MultiStateStats.States.forEachTrackedStateCombination( powerComponentStats.getConfig().getDeviceStateConfig(), states -> { - if (states[AggregatedPowerStatsConfig.STATE_POWER] - != AggregatedPowerStatsConfig.POWER_STATE_BATTERY) { + if (!areMatchingStates(states, screenState, powerState)) { return; } @@ -153,24 +191,23 @@ public class PowerStatsExporter { AggregateBatteryConsumer.Builder deviceScope = batteryUsageStatsBuilder.getAggregateBatteryConsumerBuilder( BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE); - if (descriptor.powerComponentId >= BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID) { - if (batteryUsageStatsBuilder.isSupportedCustomPowerComponent( - descriptor.powerComponentId)) { - deviceScope.addConsumedPowerForCustomComponent(descriptor.powerComponentId, - totalPower[0]); + if (isCustomComponent) { + if (batteryUsageStatsBuilder.isSupportedCustomPowerComponent(powerComponentId)) { + deviceScope.addConsumedPowerForCustomComponent(powerComponentId, totalPower[0]); } } else { - deviceScope.addConsumedPower(descriptor.powerComponentId, - totalPower[0], BatteryConsumer.POWER_MODEL_UNDEFINED); - } - - if (layout.isUidPowerAttributionSupported()) { - populateUidBatteryConsumers(batteryUsageStatsBuilder, - powerComponentStats, layout); + BatteryConsumer.Key key = deviceScope.getKey(powerComponentId, + BatteryConsumer.PROCESS_STATE_ANY, screenState, powerState); + if (key != null) { + deviceScope.addConsumedPower(key, totalPower[0], + BatteryConsumer.POWER_MODEL_UNDEFINED); + } + deviceScope.addConsumedPower(powerComponentId, totalPower[0], + BatteryConsumer.POWER_MODEL_UNDEFINED); } } - private static void populateUidBatteryConsumers( + private static void populateBatteryConsumers( BatteryUsageStats.Builder batteryUsageStatsBuilder, PowerComponentAggregatedPowerStats powerComponentStats, PowerStatsLayout layout) { @@ -185,11 +222,44 @@ public class PowerStatsExporter { .getUidStateConfig()[AggregatedPowerStatsConfig.STATE_PROCESS_STATE].isTracked() && powerComponentId < BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID; + ArrayList<Integer> uids = new ArrayList<>(); + powerComponentStats.collectUids(uids); + for (int screenState = 0; screenState < BatteryConsumer.SCREEN_STATE_COUNT; screenState++) { + if (batteryUsageStatsBuilder.isScreenStateDataNeeded()) { + if (screenState == BatteryConsumer.SCREEN_STATE_UNSPECIFIED) { + continue; + } + } else if (screenState != BatteryConsumer.SCREEN_STATE_UNSPECIFIED) { + continue; + } + + for (int powerState = 0; powerState < BatteryConsumer.POWER_STATE_COUNT; powerState++) { + if (batteryUsageStatsBuilder.isPowerStateDataNeeded()) { + if (powerState == BatteryConsumer.POWER_STATE_UNSPECIFIED) { + continue; + } + } else if (powerState != BatteryConsumer.POWER_STATE_BATTERY) { + continue; + } + + populateUidBatteryConsumers(batteryUsageStatsBuilder, powerComponentStats, layout, + uids, powerComponent, uidStats, breakDownByProcState, screenState, + powerState); + } + } + } + + private static void populateUidBatteryConsumers( + BatteryUsageStats.Builder batteryUsageStatsBuilder, + PowerComponentAggregatedPowerStats powerComponentStats, PowerStatsLayout layout, + List<Integer> uids, AggregatedPowerStatsConfig.PowerComponent powerComponent, + long[] uidStats, boolean breakDownByProcState, + @BatteryConsumer.ScreenState int screenState, + @BatteryConsumer.PowerState int powerState) { + int powerComponentId = powerComponentStats.powerComponentId; double[] powerByProcState = new double[breakDownByProcState ? BatteryConsumer.PROCESS_STATE_COUNT : 1]; double powerAllApps = 0; - ArrayList<Integer> uids = new ArrayList<>(); - powerComponentStats.collectUids(uids); for (int uid : uids) { UidBatteryConsumer.Builder builder = batteryUsageStatsBuilder.getOrCreateUidBatteryConsumerBuilder(uid); @@ -199,8 +269,7 @@ public class PowerStatsExporter { MultiStateStats.States.forEachTrackedStateCombination( powerComponent.getUidStateConfig(), states -> { - if (states[AggregatedPowerStatsConfig.STATE_POWER] - != AggregatedPowerStatsConfig.POWER_STATE_BATTERY) { + if (!areMatchingStates(states, screenState, powerState)) { return; } @@ -224,8 +293,17 @@ public class PowerStatsExporter { powerAllProcStates += power; if (breakDownByProcState && procState != BatteryConsumer.PROCESS_STATE_UNSPECIFIED) { - builder.addConsumedPower(builder.getKey(powerComponentId, procState), power, - BatteryConsumer.POWER_MODEL_UNDEFINED); + if (batteryUsageStatsBuilder.isPowerStateDataNeeded()) { + builder.addConsumedPower( + builder.getKey(powerComponentId, procState, screenState, + powerState), + power, BatteryConsumer.POWER_MODEL_UNDEFINED); + } else { + builder.addConsumedPower( + builder.getKey(powerComponentId, procState, screenState, + BatteryConsumer.POWER_STATE_UNSPECIFIED), + power, BatteryConsumer.POWER_MODEL_UNDEFINED); + } } } if (powerComponentId >= BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID) { @@ -243,8 +321,49 @@ public class PowerStatsExporter { if (powerComponentId >= BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID) { allAppsScope.addConsumedPowerForCustomComponent(powerComponentId, powerAllApps); } else { + BatteryConsumer.Key key = allAppsScope.getKey(powerComponentId, + BatteryConsumer.PROCESS_STATE_ANY, screenState, powerState); + if (key != null) { + allAppsScope.addConsumedPower(key, powerAllApps, + BatteryConsumer.POWER_MODEL_UNDEFINED); + } allAppsScope.addConsumedPower(powerComponentId, powerAllApps, BatteryConsumer.POWER_MODEL_UNDEFINED); } } + + private static boolean areMatchingStates(int[] states, + @BatteryConsumer.ScreenState int screenState, + @BatteryConsumer.PowerState int powerState) { + switch (screenState) { + case BatteryConsumer.SCREEN_STATE_ON: + if (states[AggregatedPowerStatsConfig.STATE_SCREEN] + != AggregatedPowerStatsConfig.SCREEN_STATE_ON) { + return false; + } + break; + case BatteryConsumer.SCREEN_STATE_OTHER: + if (states[AggregatedPowerStatsConfig.STATE_SCREEN] + != AggregatedPowerStatsConfig.SCREEN_STATE_OTHER) { + return false; + } + break; + } + + switch (powerState) { + case BatteryConsumer.POWER_STATE_BATTERY: + if (states[AggregatedPowerStatsConfig.STATE_POWER] + != AggregatedPowerStatsConfig.POWER_STATE_BATTERY) { + return false; + } + break; + case BatteryConsumer.POWER_STATE_OTHER: + if (states[AggregatedPowerStatsConfig.STATE_POWER] + != AggregatedPowerStatsConfig.POWER_STATE_OTHER) { + return false; + } + break; + } + return true; + } } diff --git a/services/core/java/com/android/server/power/stats/flags.aconfig b/services/core/java/com/android/server/power/stats/flags.aconfig index d34498a3764b..cc0a283db6a0 100644 --- a/services/core/java/com/android/server/power/stats/flags.aconfig +++ b/services/core/java/com/android/server/power/stats/flags.aconfig @@ -54,3 +54,17 @@ flag { description: "Adds battery_usage_stats_slice atom" bug: "324602949" } + +flag { + name: "battery_usage_stats_by_power_and_screen_state" + namespace: "backstage_power" + description: "Batterystats dumpsys is enhanced by including power break-down by power s" + bug: "352835319" +} + +flag { + name: "disable_composite_battery_usage_stats_atoms" + namespace: "backstage_power" + description: "Disable deprecated BatteryUsageStatsAtom pulled atom" + bug: "324602949" +} diff --git a/services/core/java/com/android/server/webkit/SystemImpl.java b/services/core/java/com/android/server/webkit/SystemImpl.java index a821f54520df..c4d601d03652 100644 --- a/services/core/java/com/android/server/webkit/SystemImpl.java +++ b/services/core/java/com/android/server/webkit/SystemImpl.java @@ -67,19 +67,13 @@ public class SystemImpl implements SystemInterface { private static final String TAG_SIGNATURE = "signature"; private static final String TAG_FALLBACK = "isFallback"; private static final String PIN_GROUP = "webview"; - private final WebViewProviderInfo[] mWebViewProviderPackages; - // Initialization-on-demand holder idiom for getting the WebView provider packages once and - // for all in a thread-safe manner. - private static class LazyHolder { - private static final SystemImpl INSTANCE = new SystemImpl(); - } + private final Context mContext; + private final WebViewProviderInfo[] mWebViewProviderPackages; - public static SystemImpl getInstance() { - return LazyHolder.INSTANCE; - } + SystemImpl(Context context) { + mContext = context; - private SystemImpl() { int numFallbackPackages = 0; int numAvailableByDefaultPackages = 0; XmlResourceParser parser = null; @@ -184,14 +178,14 @@ public class SystemImpl implements SystemInterface { } @Override - public String getUserChosenWebViewProvider(Context context) { - return Settings.Global.getString(context.getContentResolver(), + public String getUserChosenWebViewProvider() { + return Settings.Global.getString(mContext.getContentResolver(), Settings.Global.WEBVIEW_PROVIDER); } @Override - public void updateUserSetting(Context context, String newProviderName) { - Settings.Global.putString(context.getContentResolver(), + public void updateUserSetting(String newProviderName) { + Settings.Global.putString(mContext.getContentResolver(), Settings.Global.WEBVIEW_PROVIDER, newProviderName == null ? "" : newProviderName); } @@ -207,8 +201,8 @@ public class SystemImpl implements SystemInterface { } @Override - public void enablePackageForAllUsers(Context context, String packageName, boolean enable) { - UserManager userManager = (UserManager)context.getSystemService(Context.USER_SERVICE); + public void enablePackageForAllUsers(String packageName, boolean enable) { + UserManager userManager = mContext.getSystemService(UserManager.class); for(UserInfo userInfo : userManager.getUsers()) { enablePackageForUser(packageName, enable, userInfo.id); } @@ -228,16 +222,15 @@ public class SystemImpl implements SystemInterface { } @Override - public void installExistingPackageForAllUsers(Context context, String packageName) { - UserManager userManager = context.getSystemService(UserManager.class); + public void installExistingPackageForAllUsers(String packageName) { + UserManager userManager = mContext.getSystemService(UserManager.class); for (UserInfo userInfo : userManager.getUsers()) { installPackageForUser(packageName, userInfo.id); } } private void installPackageForUser(String packageName, int userId) { - final Context context = AppGlobals.getInitialApplication(); - final Context contextAsUser = context.createContextAsUser(UserHandle.of(userId), 0); + final Context contextAsUser = mContext.createContextAsUser(UserHandle.of(userId), 0); final PackageInstaller installer = contextAsUser.getPackageManager().getPackageInstaller(); installer.installExistingPackage(packageName, PackageManager.INSTALL_REASON_UNKNOWN, null); } @@ -255,29 +248,28 @@ public class SystemImpl implements SystemInterface { } @Override - public List<UserPackage> getPackageInfoForProviderAllUsers(Context context, - WebViewProviderInfo configInfo) { - return UserPackage.getPackageInfosAllUsers(context, configInfo.packageName, PACKAGE_FLAGS); + public List<UserPackage> getPackageInfoForProviderAllUsers(WebViewProviderInfo configInfo) { + return UserPackage.getPackageInfosAllUsers(mContext, configInfo.packageName, PACKAGE_FLAGS); } @Override - public int getMultiProcessSetting(Context context) { + public int getMultiProcessSetting() { if (updateServiceV2()) { throw new IllegalStateException( "getMultiProcessSetting shouldn't be called if update_service_v2 flag is set."); } return Settings.Global.getInt( - context.getContentResolver(), Settings.Global.WEBVIEW_MULTIPROCESS, 0); + mContext.getContentResolver(), Settings.Global.WEBVIEW_MULTIPROCESS, 0); } @Override - public void setMultiProcessSetting(Context context, int value) { + public void setMultiProcessSetting(int value) { if (updateServiceV2()) { throw new IllegalStateException( "setMultiProcessSetting shouldn't be called if update_service_v2 flag is set."); } Settings.Global.putInt( - context.getContentResolver(), Settings.Global.WEBVIEW_MULTIPROCESS, value); + mContext.getContentResolver(), Settings.Global.WEBVIEW_MULTIPROCESS, value); } @Override diff --git a/services/core/java/com/android/server/webkit/SystemInterface.java b/services/core/java/com/android/server/webkit/SystemInterface.java index ad32f623c80d..3b77d07412ce 100644 --- a/services/core/java/com/android/server/webkit/SystemInterface.java +++ b/services/core/java/com/android/server/webkit/SystemInterface.java @@ -16,7 +16,6 @@ package com.android.server.webkit; -import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager.NameNotFoundException; @@ -34,19 +33,19 @@ import java.util.List; * @hide */ public interface SystemInterface { - public WebViewProviderInfo[] getWebViewPackages(); - public int onWebViewProviderChanged(PackageInfo packageInfo); - public long getFactoryPackageVersion(String packageName) throws NameNotFoundException; + WebViewProviderInfo[] getWebViewPackages(); + int onWebViewProviderChanged(PackageInfo packageInfo); + long getFactoryPackageVersion(String packageName) throws NameNotFoundException; - public String getUserChosenWebViewProvider(Context context); - public void updateUserSetting(Context context, String newProviderName); - public void killPackageDependents(String packageName); + String getUserChosenWebViewProvider(); + void updateUserSetting(String newProviderName); + void killPackageDependents(String packageName); - public void enablePackageForAllUsers(Context context, String packageName, boolean enable); - public void installExistingPackageForAllUsers(Context context, String packageName); + void enablePackageForAllUsers(String packageName, boolean enable); + void installExistingPackageForAllUsers(String packageName); - public boolean systemIsDebuggable(); - public PackageInfo getPackageInfoForProvider(WebViewProviderInfo configInfo) + boolean systemIsDebuggable(); + PackageInfo getPackageInfoForProvider(WebViewProviderInfo configInfo) throws NameNotFoundException; /** * Get the PackageInfos of all users for the package represented by {@param configInfo}. @@ -54,15 +53,14 @@ public interface SystemInterface { * certain user. The returned array can contain null PackageInfos if the given package * is uninstalled for some user. */ - public List<UserPackage> getPackageInfoForProviderAllUsers(Context context, - WebViewProviderInfo configInfo); + List<UserPackage> getPackageInfoForProviderAllUsers(WebViewProviderInfo configInfo); - public int getMultiProcessSetting(Context context); - public void setMultiProcessSetting(Context context, int value); - public void notifyZygote(boolean enableMultiProcess); + int getMultiProcessSetting(); + void setMultiProcessSetting(int value); + void notifyZygote(boolean enableMultiProcess); /** Start the zygote if it's not already running. */ - public void ensureZygoteStarted(); - public boolean isMultiProcessDefaultEnabled(); + void ensureZygoteStarted(); + boolean isMultiProcessDefaultEnabled(); - public void pinWebviewIfRequired(ApplicationInfo appInfo); + void pinWebviewIfRequired(ApplicationInfo appInfo); } diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateService.java b/services/core/java/com/android/server/webkit/WebViewUpdateService.java index 043470f62850..7acb864cbb40 100644 --- a/services/core/java/com/android/server/webkit/WebViewUpdateService.java +++ b/services/core/java/com/android/server/webkit/WebViewUpdateService.java @@ -73,9 +73,9 @@ public class WebViewUpdateService extends SystemService { public WebViewUpdateService(Context context) { super(context); if (updateServiceV2()) { - mImpl = new WebViewUpdateServiceImpl2(context, SystemImpl.getInstance()); + mImpl = new WebViewUpdateServiceImpl2(new SystemImpl(context)); } else { - mImpl = new WebViewUpdateServiceImpl(context, SystemImpl.getInstance()); + mImpl = new WebViewUpdateServiceImpl(new SystemImpl(context)); } } diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java index dcf20f97ef71..b9be4a2deef2 100644 --- a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java +++ b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java @@ -16,7 +16,6 @@ package com.android.server.webkit; import android.annotation.Nullable; -import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.Signature; @@ -92,7 +91,6 @@ class WebViewUpdateServiceImpl implements WebViewUpdateServiceInterface { private static final int MULTIPROCESS_SETTING_OFF_VALUE = Integer.MIN_VALUE; private final SystemInterface mSystemInterface; - private final Context mContext; private long mMinimumVersionCode = -1; @@ -110,8 +108,7 @@ class WebViewUpdateServiceImpl implements WebViewUpdateServiceInterface { private final Object mLock = new Object(); - WebViewUpdateServiceImpl(Context context, SystemInterface systemInterface) { - mContext = context; + WebViewUpdateServiceImpl(SystemInterface systemInterface) { mSystemInterface = systemInterface; } @@ -173,7 +170,7 @@ class WebViewUpdateServiceImpl implements WebViewUpdateServiceInterface { try { synchronized (mLock) { mCurrentWebViewPackage = findPreferredWebViewPackage(); - String userSetting = mSystemInterface.getUserChosenWebViewProvider(mContext); + String userSetting = mSystemInterface.getUserChosenWebViewProvider(); if (userSetting != null && !userSetting.equals(mCurrentWebViewPackage.packageName)) { // Don't persist the user-chosen setting across boots if the package being @@ -181,8 +178,7 @@ class WebViewUpdateServiceImpl implements WebViewUpdateServiceInterface { // be surprised by the device switching to using a certain webview package, // that was uninstalled/disabled a long time ago, if it is installed/enabled // again. - mSystemInterface.updateUserSetting(mContext, - mCurrentWebViewPackage.packageName); + mSystemInterface.updateUserSetting(mCurrentWebViewPackage.packageName); } onWebViewProviderChanged(mCurrentWebViewPackage); } @@ -203,8 +199,7 @@ class WebViewUpdateServiceImpl implements WebViewUpdateServiceInterface { WebViewProviderInfo fallbackProvider = getFallbackProvider(webviewProviders); if (fallbackProvider != null) { Slog.w(TAG, "No valid provider, trying to enable " + fallbackProvider.packageName); - mSystemInterface.enablePackageForAllUsers(mContext, fallbackProvider.packageName, - true); + mSystemInterface.enablePackageForAllUsers(fallbackProvider.packageName, true); } else { Slog.e(TAG, "No valid provider and no fallback available."); } @@ -316,7 +311,7 @@ class WebViewUpdateServiceImpl implements WebViewUpdateServiceInterface { oldPackage = mCurrentWebViewPackage; if (newProviderName != null) { - mSystemInterface.updateUserSetting(mContext, newProviderName); + mSystemInterface.updateUserSetting(newProviderName); } try { @@ -447,7 +442,7 @@ class WebViewUpdateServiceImpl implements WebViewUpdateServiceInterface { private PackageInfo findPreferredWebViewPackage() throws WebViewPackageMissingException { ProviderAndPackageInfo[] providers = getValidWebViewPackagesAndInfos(); - String userChosenProvider = mSystemInterface.getUserChosenWebViewProvider(mContext); + String userChosenProvider = mSystemInterface.getUserChosenWebViewProvider(); // If the user has chosen provider, use that (if it's installed and enabled for all // users). @@ -455,7 +450,7 @@ class WebViewUpdateServiceImpl implements WebViewUpdateServiceInterface { if (providerAndPackage.provider.packageName.equals(userChosenProvider)) { // userPackages can contain null objects. List<UserPackage> userPackages = - mSystemInterface.getPackageInfoForProviderAllUsers(mContext, + mSystemInterface.getPackageInfoForProviderAllUsers( providerAndPackage.provider); if (isInstalledAndEnabledForAllUsers(userPackages)) { return providerAndPackage.packageInfo; @@ -470,7 +465,7 @@ class WebViewUpdateServiceImpl implements WebViewUpdateServiceInterface { if (providerAndPackage.provider.availableByDefault) { // userPackages can contain null objects. List<UserPackage> userPackages = - mSystemInterface.getPackageInfoForProviderAllUsers(mContext, + mSystemInterface.getPackageInfoForProviderAllUsers( providerAndPackage.provider); if (isInstalledAndEnabledForAllUsers(userPackages)) { return providerAndPackage.packageInfo; @@ -658,7 +653,7 @@ class WebViewUpdateServiceImpl implements WebViewUpdateServiceInterface { @Override public boolean isMultiProcessEnabled() { - int settingValue = mSystemInterface.getMultiProcessSetting(mContext); + int settingValue = mSystemInterface.getMultiProcessSetting(); if (mSystemInterface.isMultiProcessDefaultEnabled()) { // Multiprocess should be enabled unless the user has turned it off manually. return settingValue > MULTIPROCESS_SETTING_OFF_VALUE; @@ -671,7 +666,7 @@ class WebViewUpdateServiceImpl implements WebViewUpdateServiceInterface { @Override public void enableMultiProcess(boolean enable) { PackageInfo current = getCurrentWebViewPackage(); - mSystemInterface.setMultiProcessSetting(mContext, + mSystemInterface.setMultiProcessSetting( enable ? MULTIPROCESS_SETTING_ON_VALUE : MULTIPROCESS_SETTING_OFF_VALUE); mSystemInterface.notifyZygote(enable); if (current != null) { @@ -725,7 +720,7 @@ class WebViewUpdateServiceImpl implements WebViewUpdateServiceInterface { pw.println(" WebView packages:"); for (WebViewProviderInfo provider : allProviders) { List<UserPackage> userPackages = - mSystemInterface.getPackageInfoForProviderAllUsers(mContext, provider); + mSystemInterface.getPackageInfoForProviderAllUsers(provider); PackageInfo systemUserPackageInfo = userPackages.get(UserHandle.USER_SYSTEM).getPackageInfo(); if (systemUserPackageInfo == null) { @@ -741,7 +736,7 @@ class WebViewUpdateServiceImpl implements WebViewUpdateServiceInterface { systemUserPackageInfo.applicationInfo.targetSdkVersion); if (validity == VALIDITY_OK) { boolean installedForAllUsers = isInstalledAndEnabledForAllUsers( - mSystemInterface.getPackageInfoForProviderAllUsers(mContext, provider)); + mSystemInterface.getPackageInfoForProviderAllUsers(provider)); pw.println(String.format( " Valid package %s (%s) is %s installed/enabled for all users", systemUserPackageInfo.packageName, diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java index 993597eedd2c..307c15b72c76 100644 --- a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java +++ b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java @@ -16,7 +16,6 @@ package com.android.server.webkit; import android.annotation.Nullable; -import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.Signature; @@ -86,7 +85,6 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { private static final int VALIDITY_NO_LIBRARY_FLAG = 4; private final SystemInterface mSystemInterface; - private final Context mContext; private final WebViewProviderInfo mDefaultProvider; private long mMinimumVersionCode = -1; @@ -108,8 +106,7 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { private final Object mLock = new Object(); - WebViewUpdateServiceImpl2(Context context, SystemInterface systemInterface) { - mContext = context; + WebViewUpdateServiceImpl2(SystemInterface systemInterface) { mSystemInterface = systemInterface; WebViewProviderInfo[] webviewProviders = getWebViewPackages(); @@ -194,8 +191,7 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { } if (mCurrentWebViewPackage.packageName.equals(mDefaultProvider.packageName)) { List<UserPackage> userPackages = - mSystemInterface.getPackageInfoForProviderAllUsers( - mContext, mDefaultProvider); + mSystemInterface.getPackageInfoForProviderAllUsers(mDefaultProvider); return !isInstalledAndEnabledForAllUsers(userPackages); } else { return false; @@ -216,10 +212,8 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { TAG, "No provider available for all users, trying to install and enable " + mDefaultProvider.packageName); - mSystemInterface.installExistingPackageForAllUsers( - mContext, mDefaultProvider.packageName); - mSystemInterface.enablePackageForAllUsers( - mContext, mDefaultProvider.packageName, true); + mSystemInterface.installExistingPackageForAllUsers(mDefaultProvider.packageName); + mSystemInterface.enablePackageForAllUsers(mDefaultProvider.packageName, true); } @Override @@ -229,7 +223,7 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { synchronized (mLock) { mCurrentWebViewPackage = findPreferredWebViewPackage(); repairNeeded = shouldTriggerRepairLocked(); - String userSetting = mSystemInterface.getUserChosenWebViewProvider(mContext); + String userSetting = mSystemInterface.getUserChosenWebViewProvider(); if (userSetting != null && !userSetting.equals(mCurrentWebViewPackage.packageName)) { // Don't persist the user-chosen setting across boots if the package being @@ -237,8 +231,7 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { // be surprised by the device switching to using a certain webview package, // that was uninstalled/disabled a long time ago, if it is installed/enabled // again. - mSystemInterface.updateUserSetting(mContext, - mCurrentWebViewPackage.packageName); + mSystemInterface.updateUserSetting(mCurrentWebViewPackage.packageName); } onWebViewProviderChanged(mCurrentWebViewPackage); } @@ -362,7 +355,7 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { oldPackage = mCurrentWebViewPackage; if (newProviderName != null) { - mSystemInterface.updateUserSetting(mContext, newProviderName); + mSystemInterface.updateUserSetting(newProviderName); } try { @@ -493,7 +486,7 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { Counter.logIncrement("webview.value_find_preferred_webview_package_counter"); // If the user has chosen provider, use that (if it's installed and enabled for all // users). - String userChosenPackageName = mSystemInterface.getUserChosenWebViewProvider(mContext); + String userChosenPackageName = mSystemInterface.getUserChosenWebViewProvider(); WebViewProviderInfo userChosenProvider = getWebViewProviderForPackage(userChosenPackageName); if (userChosenProvider != null) { @@ -502,8 +495,7 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { mSystemInterface.getPackageInfoForProvider(userChosenProvider); if (validityResult(userChosenProvider, packageInfo) == VALIDITY_OK) { List<UserPackage> userPackages = - mSystemInterface.getPackageInfoForProviderAllUsers( - mContext, userChosenProvider); + mSystemInterface.getPackageInfoForProviderAllUsers(userChosenProvider); if (isInstalledAndEnabledForAllUsers(userPackages)) { return packageInfo; } @@ -779,7 +771,7 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { pw.println(" WebView packages:"); for (WebViewProviderInfo provider : allProviders) { List<UserPackage> userPackages = - mSystemInterface.getPackageInfoForProviderAllUsers(mContext, provider); + mSystemInterface.getPackageInfoForProviderAllUsers(provider); PackageInfo systemUserPackageInfo = userPackages.get(UserHandle.USER_SYSTEM).getPackageInfo(); if (systemUserPackageInfo == null) { @@ -798,8 +790,7 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { if (validity == VALIDITY_OK) { boolean installedForAllUsers = isInstalledAndEnabledForAllUsers( - mSystemInterface.getPackageInfoForProviderAllUsers( - mContext, provider)); + mSystemInterface.getPackageInfoForProviderAllUsers(provider)); pw.println( TextUtils.formatSimple( " Valid package %s (%s) is %s installed/enabled for all users", diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 03a13776a0ea..400919a88b1f 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -2867,7 +2867,10 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A if (mStartingData != null) { if (mStartingData.mAssociatedTask != null) { // The snapshot type may have called associateStartingDataWithTask(). - attachStartingSurfaceToAssociatedTask(); + // If this activity is rotated, don't attach to task to preserve the transform. + if (!hasFixedRotationTransform()) { + attachStartingSurfaceToAssociatedTask(); + } } else if (isEmbedded()) { associateStartingWindowWithTaskIfNeeded(); } @@ -2898,6 +2901,12 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A || mStartingData.mAssociatedTask != null) { return; } + if (task.isVisible() && !task.inTransition()) { + // Don't associated with task if the task is visible especially when the activity is + // embedded. We just need to show splash screen on the activity in case the first frame + // is not ready. + return; + } associateStartingDataWithTask(); attachStartingSurfaceToAssociatedTask(); } @@ -8964,7 +8973,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A * <p>Conditions that need to be met: * * <ul> - * <li>{@link LetterboxConfiguration#getIsEducationEnabled} is true. + * <li>{@link AppCompatConfiguration#getIsEducationEnabled} is true. * <li>The activity is eligible for fixed orientation letterbox. * <li>The activity is in fullscreen. * <li>The activity is portrait-only. @@ -8973,7 +8982,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A * </ul> */ boolean isEligibleForLetterboxEducation() { - return mWmService.mLetterboxConfiguration.getIsEducationEnabled() + return mWmService.mAppCompatConfiguration.getIsEducationEnabled() && mIsEligibleForFixedOrientationLetterbox && getWindowingMode() == WINDOWING_MODE_FULLSCREEN && getRequestedConfigurationOrientation() == ORIENTATION_PORTRAIT diff --git a/services/core/java/com/android/server/wm/ActivityRefresher.java b/services/core/java/com/android/server/wm/ActivityRefresher.java index 0c32dfcf6d43..bc822718d45a 100644 --- a/services/core/java/com/android/server/wm/ActivityRefresher.java +++ b/services/core/java/com/android/server/wm/ActivityRefresher.java @@ -75,7 +75,7 @@ class ActivityRefresher { } final boolean cycleThroughStop = - mWmService.mLetterboxConfiguration + mWmService.mAppCompatConfiguration .isCameraCompatRefreshCycleThroughStopEnabled() && !activity.mAppCompatController.getAppCompatCameraOverrides() .shouldRefreshActivityViaPauseForCameraCompat(); @@ -114,7 +114,7 @@ class ActivityRefresher { private boolean shouldRefreshActivity(@NonNull ActivityRecord activity, @NonNull Configuration newConfig, @NonNull Configuration lastReportedConfig) { - return mWmService.mLetterboxConfiguration.isCameraCompatRefreshEnabled() + return mWmService.mAppCompatConfiguration.isCameraCompatRefreshEnabled() && activity.mAppCompatController.getAppCompatOverrides() .getAppCompatCameraOverrides().shouldRefreshActivityForCameraCompat() && ArrayUtils.find(mEvaluators.toArray(), evaluator -> diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java index 3b0b727597a5..26a6b00254d3 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java @@ -584,7 +584,7 @@ public abstract class ActivityTaskManagerInternal { public abstract void clearLockedTasks(String reason); public abstract void updateUserConfiguration(); - public abstract boolean canShowErrorDialogs(); + public abstract boolean canShowErrorDialogs(int userId); public abstract void setProfileApp(String profileApp); public abstract void setProfileProc(WindowProcessController wpc); diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index ff46b33571f3..a84598dd73dc 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -190,6 +190,7 @@ import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; import android.content.pm.ParceledListSlice; import android.content.pm.ResolveInfo; +import android.content.pm.UserInfo; import android.content.res.CompatibilityInfo; import android.content.res.Configuration; import android.content.res.Resources; @@ -4899,14 +4900,21 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { * dialog / global actions also might want different behaviors. */ private void updateShouldShowDialogsLocked(Configuration config) { + mShowDialogs = shouldShowDialogs(config, /* checkUiMode= */ true); + } + + private boolean shouldShowDialogs(Configuration config, boolean checkUiMode) { final boolean inputMethodExists = !(config.keyboard == Configuration.KEYBOARD_NOKEYS && config.touchscreen == Configuration.TOUCHSCREEN_NOTOUCH && config.navigation == Configuration.NAVIGATION_NONAV); final boolean hideDialogsSet = Settings.Global.getInt(mContext.getContentResolver(), HIDE_ERROR_DIALOGS, 0) != 0; - mShowDialogs = inputMethodExists - && ActivityTaskManager.currentUiModeSupportsErrorDialogs(config) - && !hideDialogsSet; + boolean showDialogs = inputMethodExists && !hideDialogsSet; + if (checkUiMode) { + showDialogs = showDialogs + && ActivityTaskManager.currentUiModeSupportsErrorDialogs(config); + } + return showDialogs; } private void updateFontScaleIfNeeded(@UserIdInt int userId) { @@ -7148,15 +7156,67 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { } @Override - public boolean canShowErrorDialogs() { + public boolean canShowErrorDialogs(int userId) { synchronized (mGlobalLock) { - return mShowDialogs && !mSleeping && !mShuttingDown + final boolean showDialogs = mShowDialogs + || shouldShowDialogsForVisibleBackgroundUserLocked(userId); + final UserInfo userInfo = getUserManager().getUserInfo(userId); + if (userInfo == null) { + // Unable to retrieve user information. Returning false, assuming there is + // no valid user with the given id. + return false; + } + return showDialogs && !mSleeping && !mShuttingDown && !mKeyguardController.isKeyguardOrAodShowing(DEFAULT_DISPLAY) - && !hasUserRestriction(UserManager.DISALLOW_SYSTEM_ERROR_DIALOGS, - mAmInternal.getCurrentUserId()) + && !hasUserRestriction(UserManager.DISALLOW_SYSTEM_ERROR_DIALOGS, userId) && !(UserManager.isDeviceInDemoMode(mContext) - && mAmInternal.getCurrentUser().isDemo()); + && userInfo.isDemo()); + } + } + + /** + * Checks if the given user is a visible background user, which is a full, background user + * assigned to secondary displays on the devices that have + * {@link UserManager#isVisibleBackgroundUsersEnabled() + * config_multiuserVisibleBackgroundUsers enabled} (for example, passenger users on + * automotive builds, using the display associated with their seats). + * + * @see UserManager#isUserVisible() + */ + private boolean isVisibleBackgroundUser(int userId) { + if (!UserManager.isVisibleBackgroundUsersEnabled()) { + return false; + } + boolean isForeground = getCurrentUserId() == userId; + boolean isProfile = getUserManager().isProfile(userId); + boolean isVisible = mWindowManager.mUmInternal.isUserVisible(userId); + return isVisible && !isForeground && !isProfile; + } + + /** + * In a car environment, {@link ActivityTaskManagerService#mShowDialogs} is always set to + * {@code false} from {@link ActivityTaskManagerService#updateShouldShowDialogsLocked} + * because its UI mode is {@link Configuration#UI_MODE_TYPE_CAR}. Thus, error dialogs are + * not displayed when an ANR or a crash occurs. However, in the automotive multi-user + * multi-display environment, this can confuse the passenger users and leave them + * uninformed when an app is terminated by the ANR or crash without any notification. + * To address this, error dialogs are allowed for the passenger users who have UI access + * on assigned displays (a.k.a. visible background users) on devices that have + * config_multiuserVisibleBackgroundUsers enabled even though the UI mode is + * {@link Configuration#UI_MODE_TYPE_CAR}. + * + * @see ActivityTaskManagerService#updateShouldShowDialogsLocked + */ + private boolean shouldShowDialogsForVisibleBackgroundUserLocked(int userId) { + if (!isVisibleBackgroundUser(userId)) { + return false; + } + final int displayId = mWindowManager.mUmInternal.getMainDisplayAssignedToUser(userId); + final DisplayContent dc = mRootWindowContainer.getDisplayContent(displayId); + if (dc == null) { + return false; } + return shouldShowDialogs(dc.getConfiguration(), /* checkUiMode= */ false); } @Override diff --git a/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java b/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java index cf008e73321e..05d4c821c161 100644 --- a/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java +++ b/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java @@ -34,8 +34,8 @@ import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM; import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME; -import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_POSITION_MULTIPLIER_CENTER; -import static com.android.server.wm.LetterboxConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO; +import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_POSITION_MULTIPLIER_CENTER; +import static com.android.server.wm.AppCompatConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO; import android.annotation.NonNull; import android.content.pm.PackageManager; @@ -62,7 +62,7 @@ class AppCompatAspectRatioOverrides { @NonNull private final ActivityRecord mActivityRecord; @NonNull - private final LetterboxConfiguration mLetterboxConfiguration; + private final AppCompatConfiguration mAppCompatConfiguration; @NonNull private final UserAspectRatioState mUserAspectRatioState; @@ -80,12 +80,12 @@ class AppCompatAspectRatioOverrides { private final Function<Configuration, Float> mGetHorizontalPositionMultiplierProvider; AppCompatAspectRatioOverrides(@NonNull ActivityRecord activityRecord, - @NonNull LetterboxConfiguration letterboxConfiguration, + @NonNull AppCompatConfiguration appCompatConfiguration, @NonNull OptPropFactory optPropBuilder, @NonNull Function<Boolean, Boolean> isDisplayFullScreenAndInPostureProvider, @NonNull Function<Configuration, Float> getHorizontalPositionMultiplierProvider) { mActivityRecord = activityRecord; - mLetterboxConfiguration = letterboxConfiguration; + mAppCompatConfiguration = appCompatConfiguration; mUserAspectRatioState = new UserAspectRatioState(); mIsDisplayFullScreenAndInPostureProvider = isDisplayFullScreenAndInPostureProvider; mGetHorizontalPositionMultiplierProvider = getHorizontalPositionMultiplierProvider; @@ -93,10 +93,10 @@ class AppCompatAspectRatioOverrides { PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE); mAllowUserAspectRatioOverrideOptProp = optPropBuilder.create( PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE, - mLetterboxConfiguration::isUserAppAspectRatioSettingsEnabled); + mAppCompatConfiguration::isUserAppAspectRatioSettingsEnabled); mAllowUserAspectRatioFullscreenOverrideOptProp = optPropBuilder.create( PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE, - mLetterboxConfiguration::isUserAppAspectRatioFullscreenEnabled); + mAppCompatConfiguration::isUserAppAspectRatioFullscreenEnabled); mAllowOrientationOverrideOptProp = optPropBuilder.create( PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE); } @@ -147,7 +147,7 @@ class AppCompatAspectRatioOverrides { boolean isUserFullscreenOverrideEnabled() { if (mAllowUserAspectRatioOverrideOptProp.isFalse() || mAllowUserAspectRatioFullscreenOverrideOptProp.isFalse() - || !mLetterboxConfiguration.isUserAppAspectRatioFullscreenEnabled()) { + || !mAppCompatConfiguration.isUserAppAspectRatioFullscreenEnabled()) { return false; } return true; @@ -168,7 +168,7 @@ class AppCompatAspectRatioOverrides { // effect only if explicitly false. If mBooleanPropertyAllowUserAspectRatioOverride is null, // the current app doesn't opt-out so the first part of the predicate is true. return !mAllowUserAspectRatioOverrideOptProp.isFalse() - && mLetterboxConfiguration.isUserAppAspectRatioSettingsEnabled() + && mAppCompatConfiguration.isUserAppAspectRatioSettingsEnabled() && mActivityRecord.mDisplayContent != null && mActivityRecord.mDisplayContent.getIgnoreOrientationRequest(); } @@ -226,6 +226,14 @@ class AppCompatAspectRatioOverrides { : getDefaultMinAspectRatio(); } + float getDefaultMinAspectRatioForUnresizableAppsFromConfig() { + return mAppCompatConfiguration.getDefaultMinAspectRatioForUnresizableApps(); + } + + boolean isSplitScreenAspectRatioForUnresizableAppsEnabled() { + return mAppCompatConfiguration.getIsSplitScreenAspectRatioForUnresizableAppsEnabled(); + } + private float getDisplaySizeMinAspectRatio() { final DisplayArea displayArea = mActivityRecord.getDisplayArea(); if (displayArea == null) { @@ -267,22 +275,22 @@ class AppCompatAspectRatioOverrides { } private float getDefaultMinAspectRatioForUnresizableApps() { - if (!mLetterboxConfiguration.getIsSplitScreenAspectRatioForUnresizableAppsEnabled() + if (!mAppCompatConfiguration.getIsSplitScreenAspectRatioForUnresizableAppsEnabled() || mActivityRecord.getDisplayArea() == null) { - return mLetterboxConfiguration.getDefaultMinAspectRatioForUnresizableApps() + return mAppCompatConfiguration.getDefaultMinAspectRatioForUnresizableApps() > MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO - ? mLetterboxConfiguration.getDefaultMinAspectRatioForUnresizableApps() + ? mAppCompatConfiguration.getDefaultMinAspectRatioForUnresizableApps() : getDefaultMinAspectRatio(); } return getSplitScreenAspectRatio(); } - private float getDefaultMinAspectRatio() { + float getDefaultMinAspectRatio() { if (mActivityRecord.getDisplayArea() == null - || !mLetterboxConfiguration + || !mAppCompatConfiguration .getIsDisplayAspectRatioEnabledForFixedOrientationLetterbox()) { - return mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio(); + return mAppCompatConfiguration.getFixedOrientationLetterboxAspectRatio(); } return getDisplaySizeMinAspectRatio(); } diff --git a/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java b/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java index a7d2ecce4984..b23e75a0fbc2 100644 --- a/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java +++ b/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java @@ -26,8 +26,8 @@ import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_TO_ALIGN import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; -import static com.android.server.wm.LetterboxConfiguration.DEFAULT_LETTERBOX_ASPECT_RATIO_FOR_MULTI_WINDOW; -import static com.android.server.wm.LetterboxConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO; +import static com.android.server.wm.AppCompatConfiguration.DEFAULT_LETTERBOX_ASPECT_RATIO_FOR_MULTI_WINDOW; +import static com.android.server.wm.AppCompatConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO; import android.annotation.NonNull; import android.annotation.Nullable; diff --git a/services/core/java/com/android/server/wm/AppCompatCameraOverrides.java b/services/core/java/com/android/server/wm/AppCompatCameraOverrides.java index 0d108e1d9fd9..93a866380550 100644 --- a/services/core/java/com/android/server/wm/AppCompatCameraOverrides.java +++ b/services/core/java/com/android/server/wm/AppCompatCameraOverrides.java @@ -52,7 +52,7 @@ class AppCompatCameraOverrides { @NonNull private final AppCompatCameraOverridesState mAppCompatCameraOverridesState; @NonNull - private final LetterboxConfiguration mLetterboxConfiguration; + private final AppCompatConfiguration mAppCompatConfiguration; @NonNull private final OptPropFactory.OptProp mAllowMinAspectRatioOverrideOptProp; @NonNull @@ -63,15 +63,15 @@ class AppCompatCameraOverrides { private final OptPropFactory.OptProp mCameraCompatAllowForceRotationOptProp; AppCompatCameraOverrides(@NonNull ActivityRecord activityRecord, - @NonNull LetterboxConfiguration letterboxConfiguration, + @NonNull AppCompatConfiguration appCompatConfiguration, @NonNull OptPropFactory optPropBuilder) { mActivityRecord = activityRecord; - mLetterboxConfiguration = letterboxConfiguration; + mAppCompatConfiguration = appCompatConfiguration; mAppCompatCameraOverridesState = new AppCompatCameraOverridesState(); mAllowMinAspectRatioOverrideOptProp = optPropBuilder.create( PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE); final BooleanSupplier isCameraCompatTreatmentEnabled = AppCompatUtils.asLazy( - mLetterboxConfiguration::isCameraCompatTreatmentEnabled); + mAppCompatConfiguration::isCameraCompatTreatmentEnabled); mCameraCompatAllowRefreshOptProp = optPropBuilder.create( PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH, isCameraCompatTreatmentEnabled); @@ -214,7 +214,7 @@ class AppCompatCameraOverrides { * is active because the corresponding config is enabled and activity supports resizing. */ boolean isCameraCompatSplitScreenAspectRatioAllowed() { - return mLetterboxConfiguration.isCameraCompatSplitScreenAspectRatioEnabled() + return mAppCompatConfiguration.isCameraCompatSplitScreenAspectRatioEnabled() && !mActivityRecord.shouldCreateCompatDisplayInsets(); } diff --git a/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java b/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java index c8955fdd7911..1562cf64ad96 100644 --- a/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java +++ b/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java @@ -45,7 +45,7 @@ class AppCompatCameraPolicy { // Not checking DeviceConfig value here to allow enabling via DeviceConfig // without the need to restart the device. final boolean needsDisplayRotationCompatPolicy = - wmService.mLetterboxConfiguration.isCameraCompatTreatmentEnabledAtBuildTime(); + wmService.mAppCompatConfiguration.isCameraCompatTreatmentEnabledAtBuildTime(); final boolean needsCameraCompatFreeformPolicy = Flags.cameraCompatForFreeform() && DesktopModeHelper.canEnterDesktopMode(wmService.mContext); if (needsDisplayRotationCompatPolicy || needsCameraCompatFreeformPolicy) { diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/AppCompatConfiguration.java index 0161ae5b5473..ffa4251d0ded 100644 --- a/services/core/java/com/android/server/wm/LetterboxConfiguration.java +++ b/services/core/java/com/android/server/wm/AppCompatConfiguration.java @@ -34,10 +34,10 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.function.Function; -/** Reads letterbox configs from resources and controls their overrides at runtime. */ -final class LetterboxConfiguration { +/** Reads app compatibility configs from resources and controls their overrides at runtime. */ +final class AppCompatConfiguration { - private static final String TAG = TAG_WITH_CLASS_NAME ? "LetterboxConfiguration" : TAG_ATM; + private static final String TAG = TAG_WITH_CLASS_NAME ? "AppCompatConfiguration" : TAG_ATM; // Whether camera compatibility treatment is enabled. // See DisplayRotationCompatPolicy for context. @@ -183,7 +183,7 @@ final class LetterboxConfiguration { // Responsible for the persistence of letterbox[Horizontal|Vertical]PositionMultiplier @NonNull - private final LetterboxConfigurationPersister mLetterboxConfigurationPersister; + private final AppCompatConfigurationPersister mAppCompatConfigurationPersister; // Aspect ratio of letterbox for fixed orientation, values <= // MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO will be ignored. @@ -307,8 +307,8 @@ final class LetterboxConfiguration { // Flags dynamically updated with {@link android.provider.DeviceConfig}. @NonNull private final SynchedDeviceConfig mDeviceConfig; - LetterboxConfiguration(@NonNull final Context systemUiContext) { - this(systemUiContext, new LetterboxConfigurationPersister( + AppCompatConfiguration(@NonNull final Context systemUiContext) { + this(systemUiContext, new AppCompatConfigurationPersister( () -> readLetterboxHorizontalReachabilityPositionFromConfig( systemUiContext, /* forBookMode */ false), () -> readLetterboxVerticalReachabilityPositionFromConfig( @@ -320,8 +320,8 @@ final class LetterboxConfiguration { } @VisibleForTesting - LetterboxConfiguration(@NonNull final Context systemUiContext, - @NonNull final LetterboxConfigurationPersister letterboxConfigurationPersister) { + AppCompatConfiguration(@NonNull final Context systemUiContext, + @NonNull final AppCompatConfigurationPersister appCompatConfigurationPersister) { mContext = systemUiContext; mFixedOrientationLetterboxAspectRatio = mContext.getResources().getFloat( @@ -370,8 +370,8 @@ final class LetterboxConfiguration { mThinLetterboxHeightPxSupplier = new DimenPxIntSupplier(mContext, R.dimen.config_letterboxThinLetterboxHeightDp); - mLetterboxConfigurationPersister = letterboxConfigurationPersister; - mLetterboxConfigurationPersister.start(); + mAppCompatConfigurationPersister = appCompatConfigurationPersister; + mAppCompatConfigurationPersister.start(); mDeviceConfig = SynchedDeviceConfig.builder(DeviceConfig.NAMESPACE_WINDOW_MANAGER, systemUiContext.getMainExecutor()) @@ -605,7 +605,7 @@ final class LetterboxConfiguration { /** * Overrides alpha of a black scrim shown over wallpaper for {@link - * #LETTERBOX_BACKGROUND_WALLPAPER} option returned from {@link getLetterboxBackgroundType()}. + * #LETTERBOX_BACKGROUND_WALLPAPER} option returned from {@link #getLetterboxBackgroundType()}. * * <p>If given value is < 0 or >= 1, both it and a value of {@link * com.android.internal.R.dimen.config_letterboxBackgroundWallaperDarkScrimAlpha} are ignored @@ -632,8 +632,8 @@ final class LetterboxConfiguration { } /** - * Overrides blur radius for {@link #LETTERBOX_BACKGROUND_WALLPAPER} option from - * {@link getLetterboxBackgroundType()}. + * Overrides blur radius for {@link #LETTERBOX_BACKGROUND_WALLPAPER} option from {@link + * #getLetterboxBackgroundType()}. * * <p> If given value <= 0, both it and a value of {@link * com.android.internal.R.dimen.config_letterboxBackgroundWallpaperBlurRadius} are ignored @@ -645,7 +645,7 @@ final class LetterboxConfiguration { /** * Resets blur raidus for {@link #LETTERBOX_BACKGROUND_WALLPAPER} option returned by {@link - * getLetterboxBackgroundType()} to {@link + * #getLetterboxBackgroundType()} to {@link * com.android.internal.R.dimen.config_letterboxBackgroundWallpaperBlurRadius}. */ void resetLetterboxBackgroundWallpaperBlurRadiusPx() { @@ -654,14 +654,14 @@ final class LetterboxConfiguration { } /** - * Gets blur raidus for {@link #LETTERBOX_BACKGROUND_WALLPAPER} option returned by {@link - * getLetterboxBackgroundType()}. + * Gets blur radius for {@link #LETTERBOX_BACKGROUND_WALLPAPER} option returned by {@link + * #getLetterboxBackgroundType()}. */ int getLetterboxBackgroundWallpaperBlurRadiusPx() { return mLetterboxBackgroundWallpaperBlurRadiusPx; } - /* + /** * Gets horizontal position of a center of the letterboxed app window specified * in {@link com.android.internal.R.dimen.config_letterboxHorizontalPositionMultiplier} * or via an ADB command. 0 corresponds to the left side of the screen and 1 to the @@ -672,7 +672,7 @@ final class LetterboxConfiguration { : mLetterboxHorizontalPositionMultiplier; } - /* + /** * Gets vertical position of a center of the letterboxed app window specified * in {@link com.android.internal.R.dimen.config_letterboxVerticalPositionMultiplier} * or via an ADB command. 0 corresponds to the top side of the screen and 1 to the @@ -743,7 +743,7 @@ final class LetterboxConfiguration { "mLetterboxBookModePositionMultiplier"); } - /* + /** * Whether horizontal reachability repositioning is allowed for letterboxed fullscreen apps in * landscape device orientation. */ @@ -751,7 +751,7 @@ final class LetterboxConfiguration { return mIsHorizontalReachabilityEnabled; } - /* + /** * Whether vertical reachability repositioning is allowed for letterboxed fullscreen apps in * portrait device orientation. */ @@ -759,7 +759,7 @@ final class LetterboxConfiguration { return mIsVerticalReachabilityEnabled; } - /* + /** * Whether automatic horizontal reachability repositioning in book mode is allowed for * letterboxed fullscreen apps in landscape device orientation. */ @@ -821,7 +821,7 @@ final class LetterboxConfiguration { R.bool.config_letterboxIsAutomaticReachabilityInBookModeEnabled); } - /* + /** * Gets default horizontal position of the letterboxed app window when horizontal reachability * is enabled. * @@ -833,7 +833,7 @@ final class LetterboxConfiguration { return mDefaultPositionForHorizontalReachability; } - /* + /** * Gets default vertical position of the letterboxed app window when vertical reachability is * enabled. * @@ -889,7 +889,7 @@ final class LetterboxConfiguration { */ void setPersistentLetterboxPositionForHorizontalReachability(boolean forBookMode, @LetterboxHorizontalReachabilityPosition int position) { - mLetterboxConfigurationPersister.setLetterboxPositionForHorizontalReachability( + mAppCompatConfigurationPersister.setLetterboxPositionForHorizontalReachability( forBookMode, position); } @@ -899,7 +899,7 @@ final class LetterboxConfiguration { */ void setPersistentLetterboxPositionForVerticalReachability(boolean forTabletopMode, @LetterboxVerticalReachabilityPosition int position) { - mLetterboxConfigurationPersister.setLetterboxPositionForVerticalReachability( + mAppCompatConfigurationPersister.setLetterboxPositionForVerticalReachability( forTabletopMode, position); } @@ -909,11 +909,11 @@ final class LetterboxConfiguration { * is enabled to default position. */ void resetPersistentLetterboxPositionForHorizontalReachability() { - mLetterboxConfigurationPersister.setLetterboxPositionForHorizontalReachability( + mAppCompatConfigurationPersister.setLetterboxPositionForHorizontalReachability( false /* forBookMode */, readLetterboxHorizontalReachabilityPositionFromConfig(mContext, false /* forBookMode */)); - mLetterboxConfigurationPersister.setLetterboxPositionForHorizontalReachability( + mAppCompatConfigurationPersister.setLetterboxPositionForHorizontalReachability( true /* forBookMode */, readLetterboxHorizontalReachabilityPositionFromConfig(mContext, true /* forBookMode */)); @@ -925,11 +925,11 @@ final class LetterboxConfiguration { * enabled to default position. */ void resetPersistentLetterboxPositionForVerticalReachability() { - mLetterboxConfigurationPersister.setLetterboxPositionForVerticalReachability( + mAppCompatConfigurationPersister.setLetterboxPositionForVerticalReachability( false /* forTabletopMode */, readLetterboxVerticalReachabilityPositionFromConfig(mContext, false /* forTabletopMode */)); - mLetterboxConfigurationPersister.setLetterboxPositionForVerticalReachability( + mAppCompatConfigurationPersister.setLetterboxPositionForVerticalReachability( true /* forTabletopMode */, readLetterboxVerticalReachabilityPositionFromConfig(mContext, true /* forTabletopMode */)); @@ -961,7 +961,7 @@ final class LetterboxConfiguration { ? position : LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER; } - /* + /** * Gets horizontal position of a center of the letterboxed app window when reachability * is enabled specified. 0 corresponds to the left side of the screen and 1 to the right side. * @@ -969,7 +969,7 @@ final class LetterboxConfiguration { */ float getHorizontalMultiplierForReachability(boolean isDeviceInBookMode) { final int letterboxPositionForHorizontalReachability = - mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability( + mAppCompatConfigurationPersister.getLetterboxPositionForHorizontalReachability( isDeviceInBookMode); switch (letterboxPositionForHorizontalReachability) { case LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT: @@ -985,7 +985,7 @@ final class LetterboxConfiguration { } } - /* + /** * Gets vertical position of a center of the letterboxed app window when reachability * is enabled specified. 0 corresponds to the top side of the screen and 1 to the bottom side. * @@ -993,7 +993,7 @@ final class LetterboxConfiguration { */ float getVerticalMultiplierForReachability(boolean isDeviceInTabletopMode) { final int letterboxPositionForVerticalReachability = - mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability( + mAppCompatConfigurationPersister.getLetterboxPositionForVerticalReachability( isDeviceInTabletopMode); switch (letterboxPositionForVerticalReachability) { case LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP: @@ -1009,23 +1009,23 @@ final class LetterboxConfiguration { } } - /* + /** * Gets the horizontal position of the letterboxed app window when horizontal reachability is * enabled. */ @LetterboxHorizontalReachabilityPosition int getLetterboxPositionForHorizontalReachability(boolean isInFullScreenBookMode) { - return mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability( + return mAppCompatConfigurationPersister.getLetterboxPositionForHorizontalReachability( isInFullScreenBookMode); } - /* + /** * Gets the vertical position of the letterboxed app window when vertical reachability is * enabled. */ @LetterboxVerticalReachabilityPosition int getLetterboxPositionForVerticalReachability(boolean isInFullScreenTabletopMode) { - return mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability( + return mAppCompatConfigurationPersister.getLetterboxPositionForVerticalReachability( isInFullScreenTabletopMode); } @@ -1197,6 +1197,10 @@ final class LetterboxConfiguration { || mDeviceConfig.getFlagValue(KEY_ENABLE_LETTERBOX_TRANSLUCENT_ACTIVITY); } + /** + * Sets whether we use the constraints override strategy for letterboxing when dealing + * with translucent activities. + */ void setTranslucentLetterboxingOverrideEnabled( boolean translucentLetterboxingOverrideEnabled) { mTranslucentLetterboxingOverrideEnabled = translucentLetterboxingOverrideEnabled; @@ -1204,8 +1208,8 @@ final class LetterboxConfiguration { /** * Resets whether we use the constraints override strategy for letterboxing when dealing - * with translucent activities - * {@link mDeviceConfig.getFlagValue(KEY_ENABLE_LETTERBOX_TRANSLUCENT_ACTIVITY)}. + * with translucent activities, which means mDeviceConfig.getFlagValue( + * KEY_ENABLE_LETTERBOX_TRANSLUCENT_ACTIVITY) will be used. */ void resetTranslucentLetterboxingEnabled() { setTranslucentLetterboxingOverrideEnabled(false); @@ -1215,11 +1219,11 @@ final class LetterboxConfiguration { private void updatePositionForHorizontalReachability(boolean isDeviceInBookMode, Function<Integer, Integer> newHorizonalPositionFun) { final int letterboxPositionForHorizontalReachability = - mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability( + mAppCompatConfigurationPersister.getLetterboxPositionForHorizontalReachability( isDeviceInBookMode); final int nextHorizontalPosition = newHorizonalPositionFun.apply( letterboxPositionForHorizontalReachability); - mLetterboxConfigurationPersister.setLetterboxPositionForHorizontalReachability( + mAppCompatConfigurationPersister.setLetterboxPositionForHorizontalReachability( isDeviceInBookMode, nextHorizontalPosition); } @@ -1227,11 +1231,11 @@ final class LetterboxConfiguration { private void updatePositionForVerticalReachability(boolean isDeviceInTabletopMode, Function<Integer, Integer> newVerticalPositionFun) { final int letterboxPositionForVerticalReachability = - mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability( + mAppCompatConfigurationPersister.getLetterboxPositionForVerticalReachability( isDeviceInTabletopMode); final int nextVerticalPosition = newVerticalPositionFun.apply( letterboxPositionForVerticalReachability); - mLetterboxConfigurationPersister.setLetterboxPositionForVerticalReachability( + mAppCompatConfigurationPersister.setLetterboxPositionForVerticalReachability( isDeviceInTabletopMode, nextVerticalPosition); } diff --git a/services/core/java/com/android/server/wm/LetterboxConfigurationPersister.java b/services/core/java/com/android/server/wm/AppCompatConfigurationPersister.java index 38aa903e3954..852ce0401e2c 100644 --- a/services/core/java/com/android/server/wm/LetterboxConfigurationPersister.java +++ b/services/core/java/com/android/server/wm/AppCompatConfigurationPersister.java @@ -30,8 +30,8 @@ import android.util.AtomicFile; import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; -import com.android.server.wm.LetterboxConfiguration.LetterboxHorizontalReachabilityPosition; -import com.android.server.wm.LetterboxConfiguration.LetterboxVerticalReachabilityPosition; +import com.android.server.wm.AppCompatConfiguration.LetterboxHorizontalReachabilityPosition; +import com.android.server.wm.AppCompatConfiguration.LetterboxVerticalReachabilityPosition; import com.android.server.wm.nano.WindowManagerProtos; import java.io.ByteArrayOutputStream; @@ -45,12 +45,12 @@ import java.util.function.Supplier; /** * Persists the values of letterboxPositionForHorizontalReachability and - * letterboxPositionForVerticalReachability for {@link LetterboxConfiguration}. + * letterboxPositionForVerticalReachability for {@link AppCompatConfiguration}. */ -class LetterboxConfigurationPersister { +class AppCompatConfigurationPersister { private static final String TAG = - TAG_WITH_CLASS_NAME ? "LetterboxConfigurationPersister" : TAG_WM; + TAG_WITH_CLASS_NAME ? "AppCompatConfigurationPersister" : TAG_WM; private static final String LETTERBOX_CONFIGURATION_FILENAME = "letterbox_config"; @@ -94,7 +94,7 @@ class LetterboxConfigurationPersister { @NonNull private final PersisterQueue mPersisterQueue; - LetterboxConfigurationPersister( + AppCompatConfigurationPersister( @NonNull Supplier<Integer> defaultHorizontalReachabilitySupplier, @NonNull Supplier<Integer> defaultVerticalReachabilitySupplier, @NonNull Supplier<Integer> defaultBookModeReachabilitySupplier, @@ -106,7 +106,7 @@ class LetterboxConfigurationPersister { } @VisibleForTesting - LetterboxConfigurationPersister( + AppCompatConfigurationPersister( @NonNull Supplier<Integer> defaultHorizontalReachabilitySupplier, @NonNull Supplier<Integer> defaultVerticalReachabilitySupplier, @NonNull Supplier<Integer> defaultBookModeReachabilitySupplier, @@ -233,7 +233,7 @@ class LetterboxConfigurationPersister { letterboxData.letterboxPositionForTabletopModeReachability; } catch (IOException ioe) { Slog.e(TAG, - "Error reading from LetterboxConfigurationPersister. " + "Error reading from AppCompatConfigurationPersister. " + "Using default values!", ioe); useDefaultValue(); } finally { @@ -242,7 +242,7 @@ class LetterboxConfigurationPersister { fis.close(); } catch (IOException e) { useDefaultValue(); - Slog.e(TAG, "Error reading from LetterboxConfigurationPersister ", e); + Slog.e(TAG, "Error reading from AppCompatConfigurationPersister ", e); } } } @@ -332,7 +332,7 @@ class LetterboxConfigurationPersister { } catch (IOException ioe) { mFileToUpdate.failWrite(fos); Slog.e(TAG, - "Error writing to LetterboxConfigurationPersister. " + "Error writing to AppCompatConfigurationPersister. " + "Using default values!", ioe); } finally { if (mOnComplete != null) { diff --git a/services/core/java/com/android/server/wm/AppCompatController.java b/services/core/java/com/android/server/wm/AppCompatController.java index 3eed96dd90c2..f9e2507aa1eb 100644 --- a/services/core/java/com/android/server/wm/AppCompatController.java +++ b/services/core/java/com/android/server/wm/AppCompatController.java @@ -44,9 +44,9 @@ class AppCompatController { final OptPropFactory optPropBuilder = new OptPropFactory(packageManager, activityRecord.packageName); mTransparentPolicy = new TransparentPolicy(activityRecord, - wmService.mLetterboxConfiguration); + wmService.mAppCompatConfiguration); mAppCompatOverrides = new AppCompatOverrides(activityRecord, - wmService.mLetterboxConfiguration, optPropBuilder); + wmService.mAppCompatConfiguration, optPropBuilder); mOrientationPolicy = new AppCompatOrientationPolicy(activityRecord, mAppCompatOverrides); mAppCompatAspectRatioPolicy = new AppCompatAspectRatioPolicy(activityRecord, mTransparentPolicy, mAppCompatOverrides); diff --git a/services/core/java/com/android/server/wm/AppCompatOrientationOverrides.java b/services/core/java/com/android/server/wm/AppCompatOrientationOverrides.java index 9bf80119e431..0adf825b43ae 100644 --- a/services/core/java/com/android/server/wm/AppCompatOrientationOverrides.java +++ b/services/core/java/com/android/server/wm/AppCompatOrientationOverrides.java @@ -59,7 +59,7 @@ class AppCompatOrientationOverrides { final OrientationOverridesState mOrientationOverridesState; AppCompatOrientationOverrides(@NonNull ActivityRecord activityRecord, - @NonNull LetterboxConfiguration letterboxConfiguration, + @NonNull AppCompatConfiguration appCompatConfiguration, @NonNull OptPropFactory optPropBuilder, @NonNull AppCompatCameraOverrides appCompatCameraOverrides) { mActivityRecord = activityRecord; @@ -67,7 +67,7 @@ class AppCompatOrientationOverrides { mOrientationOverridesState = new OrientationOverridesState(mActivityRecord, System::currentTimeMillis); final BooleanSupplier isPolicyForIgnoringRequestedOrientationEnabled = asLazy( - letterboxConfiguration::isPolicyForIgnoringRequestedOrientationEnabled); + appCompatConfiguration::isPolicyForIgnoringRequestedOrientationEnabled); mIgnoreRequestedOrientationOptProp = optPropBuilder.create( PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION, isPolicyForIgnoringRequestedOrientationEnabled); diff --git a/services/core/java/com/android/server/wm/AppCompatOverrides.java b/services/core/java/com/android/server/wm/AppCompatOverrides.java index f6f93f9eda7c..b611ba9bb065 100644 --- a/services/core/java/com/android/server/wm/AppCompatOverrides.java +++ b/services/core/java/com/android/server/wm/AppCompatOverrides.java @@ -42,7 +42,7 @@ public class AppCompatOverrides { private static final String TAG = TAG_WITH_CLASS_NAME ? "AppCompatOverrides" : TAG_ATM; @NonNull - private final LetterboxConfiguration mLetterboxConfiguration; + private final AppCompatConfiguration mAppCompatConfiguration; @NonNull private final ActivityRecord mActivityRecord; @@ -63,23 +63,23 @@ public class AppCompatOverrides { private final AppCompatAspectRatioOverrides mAppCompatAspectRatioOverrides; AppCompatOverrides(@NonNull ActivityRecord activityRecord, - @NonNull LetterboxConfiguration letterboxConfiguration, + @NonNull AppCompatConfiguration appCompatConfiguration, @NonNull OptPropFactory optPropBuilder) { - mLetterboxConfiguration = letterboxConfiguration; + mAppCompatConfiguration = appCompatConfiguration; mActivityRecord = activityRecord; mAppCompatCameraOverrides = new AppCompatCameraOverrides(mActivityRecord, - mLetterboxConfiguration, optPropBuilder); + mAppCompatConfiguration, optPropBuilder); mAppCompatOrientationOverrides = new AppCompatOrientationOverrides(mActivityRecord, - mLetterboxConfiguration, optPropBuilder, mAppCompatCameraOverrides); + mAppCompatConfiguration, optPropBuilder, mAppCompatCameraOverrides); // TODO(b/341903757) Remove BooleanSuppliers after fixing dependency with reachability. mAppCompatAspectRatioOverrides = new AppCompatAspectRatioOverrides(activityRecord, - mLetterboxConfiguration, optPropBuilder, + mAppCompatConfiguration, optPropBuilder, activityRecord.mLetterboxUiController::isDisplayFullScreenAndInPosture, activityRecord.mLetterboxUiController::getHorizontalPositionMultiplier); mFakeFocusOptProp = optPropBuilder.create(PROPERTY_COMPAT_ENABLE_FAKE_FOCUS, - mLetterboxConfiguration::isCompatFakeFocusEnabled); + mAppCompatConfiguration::isCompatFakeFocusEnabled); mAllowOrientationOverrideOptProp = optPropBuilder.create( diff --git a/services/core/java/com/android/server/wm/DesktopModeBoundsCalculator.java b/services/core/java/com/android/server/wm/DesktopModeBoundsCalculator.java index f9f5058415c6..3ecdff6b18a0 100644 --- a/services/core/java/com/android/server/wm/DesktopModeBoundsCalculator.java +++ b/services/core/java/com/android/server/wm/DesktopModeBoundsCalculator.java @@ -16,18 +16,33 @@ package com.android.server.wm; +import static android.content.pm.ActivityInfo.isFixedOrientationLandscape; +import static android.content.pm.ActivityInfo.isFixedOrientationPortrait; +import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; +import static android.content.res.Configuration.ORIENTATION_PORTRAIT; + +import static com.android.server.wm.AppCompatConfiguration.DEFAULT_LETTERBOX_ASPECT_RATIO_FOR_MULTI_WINDOW; +import static com.android.server.wm.AppCompatConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO; +import static com.android.server.wm.AppCompatUtils.computeAspectRatio; import static com.android.server.wm.LaunchParamsUtil.applyLayoutGravity; import static com.android.server.wm.LaunchParamsUtil.calculateLayoutBounds; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityOptions; +import android.app.AppCompatTaskInfo; +import android.app.TaskInfo; import android.content.pm.ActivityInfo; +import android.content.res.Configuration; import android.graphics.Rect; import android.os.SystemProperties; import android.util.Size; import android.view.Gravity; +import com.android.internal.R; +import com.android.internal.annotations.VisibleForTesting; +import com.android.window.flags.Flags; + import java.util.function.Consumer; /** @@ -38,6 +53,8 @@ public final class DesktopModeBoundsCalculator { public static final float DESKTOP_MODE_INITIAL_BOUNDS_SCALE = SystemProperties .getInt("persist.wm.debug.desktop_mode_initial_bounds_scale", 75) / 100f; + public static final int DESKTOP_MODE_LANDSCAPE_APP_PADDING = SystemProperties + .getInt("persist.wm.debug.desktop_mode_landscape_app_padding", 25); /** * Updates launch bounds for an activity with respect to its activity options, window layout, @@ -48,12 +65,8 @@ public final class DesktopModeBoundsCalculator { @NonNull Rect outBounds, @NonNull Consumer<String> logger) { // Use stable frame instead of raw frame to avoid launching freeform windows on top of // stable insets, which usually are system widgets such as sysbar & navbar. - final TaskDisplayArea displayArea = task.getDisplayArea(); - final Rect screenBounds = displayArea.getBounds(); final Rect stableBounds = new Rect(); - displayArea.getStableRect(stableBounds); - final int desiredWidth = (int) (stableBounds.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE); - final int desiredHeight = (int) (stableBounds.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE); + task.getDisplayArea().getStableRect(stableBounds); if (options != null && options.getLaunchBounds() != null) { outBounds.set(options.getLaunchBounds()); @@ -63,37 +76,282 @@ public final class DesktopModeBoundsCalculator { final int horizontalGravity = layout.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; if (layout.hasSpecifiedSize()) { calculateLayoutBounds(stableBounds, layout, outBounds, - new Size(desiredWidth, desiredHeight)); + calculateIdealSize(stableBounds, DESKTOP_MODE_INITIAL_BOUNDS_SCALE)); applyLayoutGravity(verticalGravity, horizontalGravity, outBounds, stableBounds); logger.accept("layout specifies sizes, inheriting size and applying gravity"); } else if (verticalGravity > 0 || horizontalGravity > 0) { - calculateAndCentreInitialBounds(outBounds, screenBounds); + outBounds.set(calculateInitialBounds(task, activity, stableBounds)); applyLayoutGravity(verticalGravity, horizontalGravity, outBounds, stableBounds); logger.accept("layout specifies gravity, applying desired bounds and gravity"); } } else { - calculateAndCentreInitialBounds(outBounds, screenBounds); + outBounds.set(calculateInitialBounds(task, activity, stableBounds)); logger.accept("layout not specified, applying desired bounds"); } } /** - * Calculates the initial height and width of a task in desktop mode and centers it within the - * window bounds. + * Calculates the initial bounds required for an application to fill a scale of the display + * bounds without any letterboxing. This is done by taking into account the applications + * fullscreen size, aspect ratio, orientation and resizability to calculate an area this is + * compatible with the applications previous configuration. + */ + private static @NonNull Rect calculateInitialBounds(@NonNull Task task, + @NonNull ActivityRecord activity, @NonNull Rect stableBounds + ) { + final TaskInfo taskInfo = task.getTaskInfo(); + // Display bounds not taking into account insets. + final TaskDisplayArea displayArea = task.getDisplayArea(); + final Rect screenBounds = displayArea.getBounds(); + final Size idealSize = calculateIdealSize(screenBounds, DESKTOP_MODE_INITIAL_BOUNDS_SCALE); + if (!Flags.enableWindowingDynamicInitialBounds()) { + 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(); + final float tdaHeight = stableBounds.height(); + final int activityOrientation = activity.getOverrideOrientation(); + final Size initialSize = switch (taskInfo.configuration.orientation) { + case ORIENTATION_LANDSCAPE -> { + // Device in landscape orientation. + if (appAspectRatio == 0) { + appAspectRatio = 1; + } + if (taskInfo.isResizeable) { + if (isFixedOrientationPortrait(activityOrientation)) { + // For portrait resizeable activities, respect apps fullscreen width but + // apply ideal size height. + yield new Size((int) ((tdaHeight / appAspectRatio) + 0.5f), + idealSize.getHeight()); + } + // For landscape resizeable activities, simply apply ideal size. + yield idealSize; + } + // If activity is unresizeable, regardless of orientation, calculate maximum size + // (within the ideal size) maintaining original aspect ratio. + yield maximizeSizeGivenAspectRatio( + activity.getOverrideOrientation(), idealSize, appAspectRatio); + } + case ORIENTATION_PORTRAIT -> { + // Device in portrait orientation. + final int customPortraitWidthForLandscapeApp = screenBounds.width() + - (DESKTOP_MODE_LANDSCAPE_APP_PADDING * 2); + if (taskInfo.isResizeable) { + if (isFixedOrientationLandscape(activityOrientation)) { + if (appAspectRatio == 0) { + appAspectRatio = tdaWidth / (tdaWidth - 1); + } + // For landscape resizeable activities, respect apps fullscreen height and + // apply custom app width. + yield new Size(customPortraitWidthForLandscapeApp, + (int) ((tdaWidth / appAspectRatio) + 0.5f)); + } + // For portrait resizeable activities, simply apply ideal size. + yield idealSize; + } + if (appAspectRatio == 0) { + appAspectRatio = 1; + } + if (isFixedOrientationLandscape(activityOrientation)) { + // For landscape unresizeable activities, apply custom app width to ideal size + // and calculate maximum size with this area while maintaining original aspect + // ratio. + yield maximizeSizeGivenAspectRatio(activityOrientation, + new Size(customPortraitWidthForLandscapeApp, idealSize.getHeight()), + appAspectRatio); + } + // For portrait unresizeable activities, calculate maximum size (within the ideal + // size) maintaining original aspect ratio. + yield maximizeSizeGivenAspectRatio(activityOrientation, idealSize, appAspectRatio); + } + default -> idealSize; + }; + return centerInScreen(initialSize, screenBounds); + } + + /** + * Calculates the largest size that can fit in a given area while maintaining a specific aspect + * ratio. + */ + private static @NonNull Size maximizeSizeGivenAspectRatio( + @ActivityInfo.ScreenOrientation int orientation, + @NonNull Size targetArea, + float aspectRatio + ) { + final int targetHeight = targetArea.getHeight(); + final int targetWidth = targetArea.getWidth(); + final int finalHeight; + final int finalWidth; + if (isFixedOrientationPortrait(orientation)) { + // Portrait activity. + // Calculate required width given ideal height and aspect ratio. + int tempWidth = (int) (targetHeight / aspectRatio); + if (tempWidth <= targetWidth) { + // If the calculated width does not exceed the ideal width, overall size is within + // ideal size and can be applied. + finalHeight = targetHeight; + finalWidth = tempWidth; + } else { + // Applying target height cause overall size to exceed ideal size when maintain + // aspect ratio. Instead apply ideal width and calculate required height to respect + // aspect ratio. + finalWidth = targetWidth; + finalHeight = (int) (finalWidth * aspectRatio); + } + } else { + // Landscape activity. + // Calculate required width given ideal height and aspect ratio. + int tempWidth = (int) (targetHeight * aspectRatio); + if (tempWidth <= targetWidth) { + // If the calculated width does not exceed the ideal width, overall size is within + // ideal size and can be applied. + finalHeight = targetHeight; + finalWidth = tempWidth; + } else { + // Applying target height cause overall size to exceed ideal size when maintain + // aspect ratio. Instead apply ideal width and calculate required height to respect + // aspect ratio. + finalWidth = targetWidth; + finalHeight = (int) (finalWidth / aspectRatio); + } + } + return new Size(finalWidth, finalHeight); + } + + /** + * Calculates the aspect ratio of an activity from its fullscreen bounds. + */ + @VisibleForTesting + static float calculateAspectRatio(@NonNull Task task, @NonNull ActivityRecord activity) { + final TaskInfo taskInfo = task.getTaskInfo(); + final float fullscreenWidth = task.getDisplayArea().getBounds().width(); + final float fullscreenHeight = task.getDisplayArea().getBounds().height(); + final float maxAspectRatio = activity.getMaxAspectRatio(); + final float minAspectRatio = activity.getMinAspectRatio(); + float desiredAspectRatio = 0; + if (taskInfo.isRunning) { + final AppCompatTaskInfo appCompatTaskInfo = taskInfo.appCompatTaskInfo; + if (appCompatTaskInfo.topActivityBoundsLetterboxed) { + desiredAspectRatio = (float) Math.max( + appCompatTaskInfo.topActivityLetterboxWidth, + appCompatTaskInfo.topActivityLetterboxHeight) + / Math.min(appCompatTaskInfo.topActivityLetterboxWidth, + appCompatTaskInfo.topActivityLetterboxHeight); + } else { + desiredAspectRatio = Math.max(fullscreenHeight, fullscreenWidth) + / Math.min(fullscreenHeight, fullscreenWidth); + } + } else { + final float letterboxAspectRatioOverride = + getFixedOrientationLetterboxAspectRatio(activity, task); + if (!task.mDisplayContent.getIgnoreOrientationRequest()) { + desiredAspectRatio = DEFAULT_LETTERBOX_ASPECT_RATIO_FOR_MULTI_WINDOW; + } else if (letterboxAspectRatioOverride + > MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO) { + desiredAspectRatio = letterboxAspectRatioOverride; + } + } + // If the activity matches display orientation, the display aspect ratio should be used + if (activityMatchesDisplayOrientation( + taskInfo.configuration.orientation, + activity.getOverrideOrientation())) { + desiredAspectRatio = Math.max(fullscreenWidth, fullscreenHeight) + / Math.min(fullscreenWidth, fullscreenHeight); + } + if (maxAspectRatio >= 1 && desiredAspectRatio > maxAspectRatio) { + desiredAspectRatio = maxAspectRatio; + } else if (minAspectRatio >= 1 && desiredAspectRatio < minAspectRatio) { + desiredAspectRatio = minAspectRatio; + } + return desiredAspectRatio; + } + + private static boolean activityMatchesDisplayOrientation( + @Configuration.Orientation int deviceOrientation, + @ActivityInfo.ScreenOrientation int activityOrientation) { + if (deviceOrientation == ORIENTATION_PORTRAIT) { + return isFixedOrientationPortrait(activityOrientation); + } + return isFixedOrientationLandscape(activityOrientation); + } + + /** + * Calculates the desired initial bounds for applications in desktop windowing. This is done as + * a scale of the screen bounds. + */ + private static @NonNull Size calculateIdealSize(@NonNull Rect screenBounds, float scale) { + final int width = (int) (screenBounds.width() * scale); + final int height = (int) (screenBounds.height() * scale); + return new Size(width, height); + } + + /** + * Adjusts bounds to be positioned in the middle of the screen. */ - private static void calculateAndCentreInitialBounds(@NonNull Rect outBounds, + private static @NonNull Rect centerInScreen(@NonNull Size desiredSize, @NonNull Rect screenBounds) { - // TODO(b/319819547): Account for app constraints so apps do not become letterboxed - // The desired dimensions that a fully resizable window should take when initially entering - // desktop mode. Calculated as a percentage of the available display area as defined by the - // DESKTOP_MODE_INITIAL_BOUNDS_SCALE. - final int desiredWidth = (int) (screenBounds.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE); - final int desiredHeight = (int) (screenBounds.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE); - outBounds.right = desiredWidth; - outBounds.bottom = desiredHeight; - outBounds.offset(screenBounds.centerX() - outBounds.centerX(), - screenBounds.centerY() - outBounds.centerY()); + // TODO(b/325240051): Position apps with bottom heavy offset + final int heightOffset = (screenBounds.height() - desiredSize.getHeight()) / 2; + final int widthOffset = (screenBounds.width() - desiredSize.getWidth()) / 2; + final Rect resultBounds = new Rect(0, 0, + desiredSize.getWidth(), desiredSize.getHeight()); + resultBounds.offset(screenBounds.left + widthOffset, screenBounds.top + heightOffset); + return resultBounds; + } + + private static float getFixedOrientationLetterboxAspectRatio(@NonNull ActivityRecord activity, + @NonNull Task task) { + return activity.shouldCreateCompatDisplayInsets() + ? getDefaultMinAspectRatioForUnresizableApps(activity, task) + : activity.mAppCompatController.getAppCompatAspectRatioOverrides() + .getDefaultMinAspectRatio(); + } + + private static float getDefaultMinAspectRatioForUnresizableApps( + @NonNull ActivityRecord activity, + @NonNull Task task) { + final AppCompatAspectRatioOverrides appCompatAspectRatioOverrides = + activity.mAppCompatController.getAppCompatAspectRatioOverrides(); + if (appCompatAspectRatioOverrides.isSplitScreenAspectRatioForUnresizableAppsEnabled()) { + // Default letterbox aspect ratio for unresizable apps. + return getSplitScreenAspectRatio(activity, task); + } + + if (appCompatAspectRatioOverrides.getDefaultMinAspectRatioForUnresizableAppsFromConfig() + > MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO) { + return appCompatAspectRatioOverrides + .getDefaultMinAspectRatioForUnresizableAppsFromConfig(); + } + + return appCompatAspectRatioOverrides.getDefaultMinAspectRatio(); + } + + /** + * Calculates the aspect ratio of the available display area when an app enters split-screen on + * a given device, taking into account any dividers and insets. + */ + private static float getSplitScreenAspectRatio(@NonNull ActivityRecord activity, + @NonNull Task task) { + final int dividerWindowWidth = + activity.mWmService.mContext.getResources().getDimensionPixelSize( + R.dimen.docked_stack_divider_thickness); + final int dividerInsets = + activity.mWmService.mContext.getResources().getDimensionPixelSize( + R.dimen.docked_stack_divider_insets); + final int dividerSize = dividerWindowWidth - dividerInsets * 2; + final Rect bounds = new Rect(0, 0, + task.mDisplayContent.getDisplayInfo().appWidth, + task.mDisplayContent.getDisplayInfo().appHeight); + if (bounds.width() >= bounds.height()) { + bounds.inset(/* dx */ dividerSize / 2, /* dy */ 0); + bounds.right = bounds.centerX(); + } else { + bounds.inset(/* dx */ 0, /* dy */ dividerSize / 2); + bounds.bottom = bounds.centerY(); + } + return computeAspectRatio(bounds); } } diff --git a/services/core/java/com/android/server/wm/DesktopModeHelper.java b/services/core/java/com/android/server/wm/DesktopModeHelper.java index 1f341147deb1..e0c0c2c60123 100644 --- a/services/core/java/com/android/server/wm/DesktopModeHelper.java +++ b/services/core/java/com/android/server/wm/DesktopModeHelper.java @@ -22,7 +22,7 @@ import android.os.SystemProperties; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; -import com.android.window.flags.Flags; +import com.android.server.wm.utils.DesktopModeFlagsUtil; /** * Constants for desktop mode feature @@ -35,8 +35,8 @@ public final class DesktopModeHelper { "persist.wm.debug.desktop_mode_enforce_device_restrictions", true); /** Whether desktop mode is enabled. */ - static boolean isDesktopModeEnabled() { - return Flags.enableDesktopWindowingMode(); + static boolean isDesktopModeEnabled(@NonNull Context context) { + return DesktopModeFlagsUtil.DESKTOP_WINDOWING_MODE.isEnabled(context); } /** @@ -60,7 +60,7 @@ public final class DesktopModeHelper { * Return {@code true} if desktop mode can be entered on the current device. */ static boolean canEnterDesktopMode(@NonNull Context context) { - return isDesktopModeEnabled() + return isDesktopModeEnabled(context) && (!shouldEnforceDeviceRestrictions() || isDesktopModeSupported(context)); } } diff --git a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java index aacd3c65154f..548addbef39d 100644 --- a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java +++ b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java @@ -25,7 +25,6 @@ import android.annotation.Nullable; import android.app.ActivityOptions; import android.content.Context; import android.content.pm.ActivityInfo; -import android.os.SystemProperties; import android.util.Slog; import com.android.server.wm.LaunchParamsController.LaunchParamsModifier; @@ -38,19 +37,9 @@ class DesktopModeLaunchParamsModifier implements LaunchParamsModifier { TAG_WITH_CLASS_NAME ? "DesktopModeLaunchParamsModifier" : TAG_ATM; private static final boolean DEBUG = false; - public static final float DESKTOP_MODE_INITIAL_BOUNDS_SCALE = - SystemProperties - .getInt("persist.wm.debug.desktop_mode_initial_bounds_scale", 75) / 100f; - - /** - * Flag to indicate whether to restrict desktop mode to supported devices. - */ - private static final boolean ENFORCE_DEVICE_RESTRICTIONS = SystemProperties.getBoolean( - "persist.wm.debug.desktop_mode_enforce_device_restrictions", true); - private StringBuilder mLogBuilder; - private final Context mContext; + @NonNull private final Context mContext; DesktopModeLaunchParamsModifier(@NonNull Context context) { mContext = context; diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java index d2976b01acbb..8272e1609e0d 100644 --- a/services/core/java/com/android/server/wm/DisplayRotation.java +++ b/services/core/java/com/android/server/wm/DisplayRotation.java @@ -326,7 +326,7 @@ public class DisplayRotation { DisplayRotationImmersiveAppCompatPolicy initImmersiveAppCompatPolicy( WindowManagerService service, DisplayContent displayContent) { return DisplayRotationImmersiveAppCompatPolicy.createIfNeeded( - service.mLetterboxConfiguration, this, displayContent); + service.mAppCompatConfiguration, this, displayContent); } // Change the default value to the value specified in the sysprop diff --git a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java index 1a0124a1d4de..63fe94c33061 100644 --- a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java @@ -247,7 +247,7 @@ final class DisplayRotationCompatPolicy implements CameraStateMonitor.CameraComp * </ul> */ private boolean isTreatmentEnabledForDisplay() { - return mWmService.mLetterboxConfiguration.isCameraCompatTreatmentEnabled() + return mWmService.mAppCompatConfiguration.isCameraCompatTreatmentEnabled() && mDisplayContent.getIgnoreOrientationRequest() // TODO(b/225928882): Support camera compat rotation for external displays && mDisplayContent.getDisplay().getType() == TYPE_INTERNAL; diff --git a/services/core/java/com/android/server/wm/DisplayRotationImmersiveAppCompatPolicy.java b/services/core/java/com/android/server/wm/DisplayRotationImmersiveAppCompatPolicy.java index de70c4df7985..094434d07cfe 100644 --- a/services/core/java/com/android/server/wm/DisplayRotationImmersiveAppCompatPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayRotationImmersiveAppCompatPolicy.java @@ -40,28 +40,28 @@ final class DisplayRotationImmersiveAppCompatPolicy { @Nullable static DisplayRotationImmersiveAppCompatPolicy createIfNeeded( - @NonNull final LetterboxConfiguration letterboxConfiguration, + @NonNull final AppCompatConfiguration appCompatConfiguration, @NonNull final DisplayRotation displayRotation, @NonNull final DisplayContent displayContent) { - if (!letterboxConfiguration + if (!appCompatConfiguration .isDisplayRotationImmersiveAppCompatPolicyEnabledAtBuildTime()) { return null; } return new DisplayRotationImmersiveAppCompatPolicy( - letterboxConfiguration, displayRotation, displayContent); + appCompatConfiguration, displayRotation, displayContent); } private final DisplayRotation mDisplayRotation; - private final LetterboxConfiguration mLetterboxConfiguration; + private final AppCompatConfiguration mAppCompatConfiguration; private final DisplayContent mDisplayContent; private DisplayRotationImmersiveAppCompatPolicy( - @NonNull final LetterboxConfiguration letterboxConfiguration, + @NonNull final AppCompatConfiguration appCompatConfiguration, @NonNull final DisplayRotation displayRotation, @NonNull final DisplayContent displayContent) { mDisplayRotation = displayRotation; - mLetterboxConfiguration = letterboxConfiguration; + mAppCompatConfiguration = appCompatConfiguration; mDisplayContent = displayContent; } @@ -80,14 +80,14 @@ final class DisplayRotationImmersiveAppCompatPolicy { * <li>Rotation will lead to letterboxing due to fixed orientation. * <li>{@link DisplayContent#getIgnoreOrientationRequest} is {@code true} * <li>This policy is enabled on the device, for details see - * {@link LetterboxConfiguration#isDisplayRotationImmersiveAppCompatPolicyEnabled} + * {@link AppCompatConfiguration#isDisplayRotationImmersiveAppCompatPolicyEnabled} * </ul> * * @param proposedRotation new proposed {@link Surface.Rotation} for the screen. * @return {@code true}, if there is a need to lock screen rotation, {@code false} otherwise. */ boolean isRotationLockEnforced(@Surface.Rotation final int proposedRotation) { - if (!mLetterboxConfiguration.isDisplayRotationImmersiveAppCompatPolicyEnabled()) { + if (!mAppCompatConfiguration.isDisplayRotationImmersiveAppCompatPolicyEnabled()) { return false; } synchronized (mDisplayContent.mWmService.mGlobalLock) { diff --git a/services/core/java/com/android/server/wm/InsetsStateController.java b/services/core/java/com/android/server/wm/InsetsStateController.java index 348384203fba..dcadb0f31085 100644 --- a/services/core/java/com/android/server/wm/InsetsStateController.java +++ b/services/core/java/com/android/server/wm/InsetsStateController.java @@ -397,9 +397,11 @@ class InsetsStateController { onRequestedVisibleTypesChanged(newControlTargets.valueAt(i)); } newControlTargets.clear(); - // Check for and try to run the scheduled show IME request (if it exists), as we - // now applied the surface transaction and notified the target of the new control. - getImeSourceProvider().checkAndStartShowImePostLayout(); + if (!android.view.inputmethod.Flags.refactorInsetsController()) { + // Check for and try to run the scheduled show IME request (if it exists), as we + // now applied the surface transaction and notified the target of the new control. + getImeSourceProvider().checkAndStartShowImePostLayout(); + } }); } diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java index 235d6cd09d35..be8e806a5752 100644 --- a/services/core/java/com/android/server/wm/LetterboxUiController.java +++ b/services/core/java/com/android/server/wm/LetterboxUiController.java @@ -37,17 +37,17 @@ import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHA import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__TOP_TO_CENTER; import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM; import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME; -import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND; -import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING; -import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_SOLID_COLOR; -import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_WALLPAPER; -import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER; -import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT; -import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT; -import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM; -import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER; -import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP; -import static com.android.server.wm.LetterboxConfiguration.letterboxBackgroundTypeToString; +import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND; +import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING; +import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_SOLID_COLOR; +import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_WALLPAPER; +import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER; +import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT; +import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT; +import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM; +import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER; +import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP; +import static com.android.server.wm.AppCompatConfiguration.letterboxBackgroundTypeToString; import android.annotation.NonNull; import android.annotation.Nullable; @@ -67,7 +67,7 @@ import android.view.WindowManager; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.statusbar.LetterboxDetails; -import com.android.server.wm.LetterboxConfiguration.LetterboxBackgroundType; +import com.android.server.wm.AppCompatConfiguration.LetterboxBackgroundType; import com.android.window.flags.Flags; import java.io.PrintWriter; @@ -81,7 +81,7 @@ final class LetterboxUiController { private final Point mTmpPoint = new Point(); - private final LetterboxConfiguration mLetterboxConfiguration; + private final AppCompatConfiguration mAppCompatConfiguration; private final ActivityRecord mActivityRecord; @@ -95,7 +95,7 @@ final class LetterboxUiController { private boolean mDoubleTapEvent; LetterboxUiController(WindowManagerService wmService, ActivityRecord activityRecord) { - mLetterboxConfiguration = wmService.mLetterboxConfiguration; + mAppCompatConfiguration = wmService.mAppCompatConfiguration; // Given activityRecord may not be fully constructed since LetterboxUiController // is created in its constructor. It shouldn't be used in this constructor but it's safe // to use it after since controller is only used in ActivityRecord. @@ -345,7 +345,7 @@ final class LetterboxUiController { private boolean shouldLetterboxHaveRoundedCorners() { // TODO(b/214030873): remove once background is drawn for transparent activities // Letterbox shouldn't have rounded corners if the activity is transparent - return mLetterboxConfiguration.isLetterboxActivityCornersRounded() + return mAppCompatConfiguration.isLetterboxActivityCornersRounded() && mActivityRecord.fillsParent(); } @@ -382,13 +382,13 @@ final class LetterboxUiController { return isHorizontalReachabilityEnabled(parentConfiguration) // Using the last global dynamic position to avoid "jumps" when moving // between apps or activities. - ? mLetterboxConfiguration.getHorizontalMultiplierForReachability(bookModeEnabled) - : mLetterboxConfiguration.getLetterboxHorizontalPositionMultiplier(bookModeEnabled); + ? mAppCompatConfiguration.getHorizontalMultiplierForReachability(bookModeEnabled) + : mAppCompatConfiguration.getLetterboxHorizontalPositionMultiplier(bookModeEnabled); } private boolean isFullScreenAndBookModeEnabled() { return isDisplayFullScreenAndInPosture(/* isTabletop */ false) - && mLetterboxConfiguration.getIsAutomaticReachabilityInBookModeEnabled(); + && mAppCompatConfiguration.getIsAutomaticReachabilityInBookModeEnabled(); } float getVerticalPositionMultiplier(Configuration parentConfiguration) { @@ -398,8 +398,8 @@ final class LetterboxUiController { return isVerticalReachabilityEnabled(parentConfiguration) // Using the last global dynamic position to avoid "jumps" when moving // between apps or activities. - ? mLetterboxConfiguration.getVerticalMultiplierForReachability(tabletopMode) - : mLetterboxConfiguration.getLetterboxVerticalPositionMultiplier(tabletopMode); + ? mAppCompatConfiguration.getVerticalMultiplierForReachability(tabletopMode) + : mAppCompatConfiguration.getLetterboxVerticalPositionMultiplier(tabletopMode); } float getFixedOrientationLetterboxAspectRatio(@NonNull Configuration parentConfiguration) { @@ -408,14 +408,14 @@ final class LetterboxUiController { } boolean isLetterboxEducationEnabled() { - return mLetterboxConfiguration.getIsEducationEnabled(); + return mAppCompatConfiguration.getIsEducationEnabled(); } /** * @return {@value true} if the resulting app is letterboxed in a way defined as thin. */ boolean isVerticalThinLetterboxed() { - final int thinHeight = mLetterboxConfiguration.getThinLetterboxHeightPx(); + final int thinHeight = mAppCompatConfiguration.getThinLetterboxHeightPx(); if (thinHeight < 0) { return false; } @@ -432,7 +432,7 @@ final class LetterboxUiController { * @return {@value true} if the resulting app is pillarboxed in a way defined as thin. */ boolean isHorizontalThinLetterboxed() { - final int thinWidth = mLetterboxConfiguration.getThinLetterboxWidthPx(); + final int thinWidth = mAppCompatConfiguration.getThinLetterboxWidthPx(); if (thinWidth < 0) { return false; } @@ -477,17 +477,17 @@ final class LetterboxUiController { .shouldOverrideMinAspectRatio(); } - @LetterboxConfiguration.LetterboxVerticalReachabilityPosition + @AppCompatConfiguration.LetterboxVerticalReachabilityPosition int getLetterboxPositionForVerticalReachability() { final boolean isInFullScreenTabletopMode = isDisplayFullScreenAndSeparatingHinge(); - return mLetterboxConfiguration.getLetterboxPositionForVerticalReachability( + return mAppCompatConfiguration.getLetterboxPositionForVerticalReachability( isInFullScreenTabletopMode); } - @LetterboxConfiguration.LetterboxHorizontalReachabilityPosition + @AppCompatConfiguration.LetterboxHorizontalReachabilityPosition int getLetterboxPositionForHorizontalReachability() { final boolean isInFullScreenBookMode = isFullScreenAndBookModeEnabled(); - return mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability( + return mAppCompatConfiguration.getLetterboxPositionForHorizontalReachability( isInFullScreenBookMode); } @@ -503,12 +503,12 @@ final class LetterboxUiController { } boolean isInFullScreenBookMode = isDisplayFullScreenAndSeparatingHinge() - && mLetterboxConfiguration.getIsAutomaticReachabilityInBookModeEnabled(); - int letterboxPositionForHorizontalReachability = mLetterboxConfiguration + && mAppCompatConfiguration.getIsAutomaticReachabilityInBookModeEnabled(); + int letterboxPositionForHorizontalReachability = mAppCompatConfiguration .getLetterboxPositionForHorizontalReachability(isInFullScreenBookMode); if (mLetterbox.getInnerFrame().left > x) { // Moving to the next stop on the left side of the app window: right > center > left. - mLetterboxConfiguration.movePositionForHorizontalReachabilityToNextLeftStop( + mAppCompatConfiguration.movePositionForHorizontalReachabilityToNextLeftStop( isInFullScreenBookMode); int changeToLog = letterboxPositionForHorizontalReachability @@ -519,7 +519,7 @@ final class LetterboxUiController { mDoubleTapEvent = true; } else if (mLetterbox.getInnerFrame().right < x) { // Moving to the next stop on the right side of the app window: left > center > right. - mLetterboxConfiguration.movePositionForHorizontalReachabilityToNextRightStop( + mAppCompatConfiguration.movePositionForHorizontalReachabilityToNextRightStop( isInFullScreenBookMode); int changeToLog = letterboxPositionForHorizontalReachability @@ -544,11 +544,11 @@ final class LetterboxUiController { return; } boolean isInFullScreenTabletopMode = isDisplayFullScreenAndSeparatingHinge(); - int letterboxPositionForVerticalReachability = mLetterboxConfiguration + int letterboxPositionForVerticalReachability = mAppCompatConfiguration .getLetterboxPositionForVerticalReachability(isInFullScreenTabletopMode); if (mLetterbox.getInnerFrame().top > y) { // Moving to the next stop on the top side of the app window: bottom > center > top. - mLetterboxConfiguration.movePositionForVerticalReachabilityToNextTopStop( + mAppCompatConfiguration.movePositionForVerticalReachabilityToNextTopStop( isInFullScreenTabletopMode); int changeToLog = letterboxPositionForVerticalReachability @@ -559,7 +559,7 @@ final class LetterboxUiController { mDoubleTapEvent = true; } else if (mLetterbox.getInnerFrame().bottom < y) { // Moving to the next stop on the bottom side of the app window: top > center > bottom. - mLetterboxConfiguration.movePositionForVerticalReachabilityToNextBottomStop( + mAppCompatConfiguration.movePositionForVerticalReachabilityToNextBottomStop( isInFullScreenTabletopMode); int changeToLog = letterboxPositionForVerticalReachability @@ -596,7 +596,7 @@ final class LetterboxUiController { .getTransparentPolicy().getFirstOpaqueActivity() .map(ActivityRecord::getScreenResolvedBounds) .orElse(mActivityRecord.getScreenResolvedBounds()); - return mLetterboxConfiguration.getIsHorizontalReachabilityEnabled() + return mAppCompatConfiguration.getIsHorizontalReachabilityEnabled() && parentConfiguration.windowConfiguration.getWindowingMode() == WINDOWING_MODE_FULLSCREEN // Check whether the activity fills the parent vertically. @@ -641,7 +641,7 @@ final class LetterboxUiController { .getTransparentPolicy().getFirstOpaqueActivity() .map(ActivityRecord::getScreenResolvedBounds) .orElse(mActivityRecord.getScreenResolvedBounds()); - return mLetterboxConfiguration.getIsVerticalReachabilityEnabled() + return mAppCompatConfiguration.getIsVerticalReachabilityEnabled() && parentConfiguration.windowConfiguration.getWindowingMode() == WINDOWING_MODE_FULLSCREEN // Check whether the activity fills the parent horizontally. @@ -681,7 +681,7 @@ final class LetterboxUiController { return Color.valueOf(Color.BLACK); } @LetterboxBackgroundType int letterboxBackgroundType = - mLetterboxConfiguration.getLetterboxBackgroundType(); + mAppCompatConfiguration.getLetterboxBackgroundType(); TaskDescription taskDescription = mActivityRecord.taskDescription; switch (letterboxBackgroundType) { case LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING: @@ -697,7 +697,7 @@ final class LetterboxUiController { case LETTERBOX_BACKGROUND_WALLPAPER: if (hasWallpaperBackgroundForLetterbox()) { // Color is used for translucent scrim that dims wallpaper. - return mLetterboxConfiguration.getLetterboxBackgroundColor(); + return mAppCompatConfiguration.getLetterboxBackgroundColor(); } Slog.w(TAG, "Wallpaper option is selected for letterbox background but " + "blur is not supported by a device or not supported in the current " @@ -705,14 +705,14 @@ final class LetterboxUiController { + "provided so using solid color background"); break; case LETTERBOX_BACKGROUND_SOLID_COLOR: - return mLetterboxConfiguration.getLetterboxBackgroundColor(); + return mAppCompatConfiguration.getLetterboxBackgroundColor(); default: throw new AssertionError( "Unexpected letterbox background type: " + letterboxBackgroundType); } // If picked option configured incorrectly or not supported then default to a solid color // background. - return mLetterboxConfiguration.getLetterboxBackgroundColor(); + return mAppCompatConfiguration.getLetterboxBackgroundColor(); } private void updateRoundedCornersIfNeeded(final WindowState mainWindow) { @@ -771,7 +771,7 @@ final class LetterboxUiController { private boolean requiresRoundedCorners(final WindowState mainWindow) { return isLetterboxedNotForDisplayCutout(mainWindow) - && mLetterboxConfiguration.isLetterboxActivityCornersRounded(); + && mAppCompatConfiguration.isLetterboxActivityCornersRounded(); } // Returns rounded corners radius the letterboxed activity should have based on override in @@ -785,8 +785,8 @@ final class LetterboxUiController { } final int radius; - if (mLetterboxConfiguration.getLetterboxActivityCornersRadius() >= 0) { - radius = mLetterboxConfiguration.getLetterboxActivityCornersRadius(); + if (mAppCompatConfiguration.getLetterboxActivityCornersRadius() >= 0) { + radius = mAppCompatConfiguration.getLetterboxActivityCornersRadius(); } else { final InsetsState insetsState = mainWindow.getInsetsState(); radius = Math.min( @@ -850,7 +850,7 @@ final class LetterboxUiController { private void updateWallpaperForLetterbox(WindowState mainWindow) { @LetterboxBackgroundType int letterboxBackgroundType = - mLetterboxConfiguration.getLetterboxBackgroundType(); + mAppCompatConfiguration.getLetterboxBackgroundType(); boolean wallpaperShouldBeShown = letterboxBackgroundType == LETTERBOX_BACKGROUND_WALLPAPER // Don't use wallpaper as a background if letterboxed for display cutout. @@ -868,18 +868,18 @@ final class LetterboxUiController { } private int getLetterboxWallpaperBlurRadiusPx() { - int blurRadius = mLetterboxConfiguration.getLetterboxBackgroundWallpaperBlurRadiusPx(); + int blurRadius = mAppCompatConfiguration.getLetterboxBackgroundWallpaperBlurRadiusPx(); return Math.max(blurRadius, 0); } private float getLetterboxWallpaperDarkScrimAlpha() { - float alpha = mLetterboxConfiguration.getLetterboxBackgroundWallpaperDarkScrimAlpha(); + float alpha = mAppCompatConfiguration.getLetterboxBackgroundWallpaperDarkScrimAlpha(); // No scrim by default. return (alpha < 0 || alpha >= 1) ? 0.0f : alpha; } private boolean isLetterboxWallpaperBlurSupported() { - return mLetterboxConfiguration.mContext.getSystemService(WindowManager.class) + return mAppCompatConfiguration.mContext.getSystemService(WindowManager.class) .isCrossWindowBlurEnabled(); } @@ -911,10 +911,10 @@ final class LetterboxUiController { getLetterboxBackgroundColor().toArgb())); pw.println(prefix + " letterboxBackgroundType=" + letterboxBackgroundTypeToString( - mLetterboxConfiguration.getLetterboxBackgroundType())); + mAppCompatConfiguration.getLetterboxBackgroundType())); pw.println(prefix + " letterboxCornerRadius=" + getRoundedCornersRadius(mainWin)); - if (mLetterboxConfiguration.getLetterboxBackgroundType() + if (mAppCompatConfiguration.getLetterboxBackgroundType() == LETTERBOX_BACKGROUND_WALLPAPER) { pw.println(prefix + " isLetterboxWallpaperBlurSupported=" + isLetterboxWallpaperBlurSupported()); @@ -932,19 +932,19 @@ final class LetterboxUiController { pw.println(prefix + " letterboxVerticalPositionMultiplier=" + getVerticalPositionMultiplier(mActivityRecord.getParent().getConfiguration())); pw.println(prefix + " letterboxPositionForHorizontalReachability=" - + LetterboxConfiguration.letterboxHorizontalReachabilityPositionToString( - mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability(false))); + + AppCompatConfiguration.letterboxHorizontalReachabilityPositionToString( + mAppCompatConfiguration.getLetterboxPositionForHorizontalReachability(false))); pw.println(prefix + " letterboxPositionForVerticalReachability=" - + LetterboxConfiguration.letterboxVerticalReachabilityPositionToString( - mLetterboxConfiguration.getLetterboxPositionForVerticalReachability(false))); + + AppCompatConfiguration.letterboxVerticalReachabilityPositionToString( + mAppCompatConfiguration.getLetterboxPositionForVerticalReachability(false))); pw.println(prefix + " fixedOrientationLetterboxAspectRatio=" - + mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio()); + + mAppCompatConfiguration.getFixedOrientationLetterboxAspectRatio()); pw.println(prefix + " defaultMinAspectRatioForUnresizableApps=" - + mLetterboxConfiguration.getDefaultMinAspectRatioForUnresizableApps()); + + mAppCompatConfiguration.getDefaultMinAspectRatioForUnresizableApps()); pw.println(prefix + " isSplitScreenAspectRatioForUnresizableAppsEnabled=" - + mLetterboxConfiguration.getIsSplitScreenAspectRatioForUnresizableAppsEnabled()); + + mAppCompatConfiguration.getIsSplitScreenAspectRatioForUnresizableAppsEnabled()); pw.println(prefix + " isDisplayAspectRatioEnabledForFixedOrientationLetterbox=" - + mLetterboxConfiguration + + mAppCompatConfiguration .getIsDisplayAspectRatioEnabledForFixedOrientationLetterbox()); } @@ -971,7 +971,7 @@ final class LetterboxUiController { } private int letterboxHorizontalReachabilityPositionToLetterboxPosition( - @LetterboxConfiguration.LetterboxHorizontalReachabilityPosition int position) { + @AppCompatConfiguration.LetterboxHorizontalReachabilityPosition int position) { switch (position) { case LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT: return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__LEFT; @@ -987,7 +987,7 @@ final class LetterboxUiController { } private int letterboxVerticalReachabilityPositionToLetterboxPosition( - @LetterboxConfiguration.LetterboxVerticalReachabilityPosition int position) { + @AppCompatConfiguration.LetterboxVerticalReachabilityPosition int position) { switch (position) { case LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP: return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__TOP; @@ -1005,13 +1005,13 @@ final class LetterboxUiController { int getLetterboxPositionForLogging() { int positionToLog = APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__UNKNOWN_POSITION; if (isHorizontalReachabilityEnabled()) { - int letterboxPositionForHorizontalReachability = mLetterboxConfiguration + int letterboxPositionForHorizontalReachability = mAppCompatConfiguration .getLetterboxPositionForHorizontalReachability( isDisplayFullScreenAndInPosture(/* isTabletop */ false)); positionToLog = letterboxHorizontalReachabilityPositionToLetterboxPosition( letterboxPositionForHorizontalReachability); } else if (isVerticalReachabilityEnabled()) { - int letterboxPositionForVerticalReachability = mLetterboxConfiguration + int letterboxPositionForVerticalReachability = mAppCompatConfiguration .getLetterboxPositionForVerticalReachability( isDisplayFullScreenAndInPosture(/* isTabletop */ true)); positionToLog = letterboxVerticalReachabilityPositionToLetterboxPosition( diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index d5fea0c6d636..8f83a7c2c853 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -3383,7 +3383,7 @@ class Task extends TaskFragment { // Whether the direct top activity is in size compat mode appCompatTaskInfo.topActivityInSizeCompat = isTopActivityVisible && top.inSizeCompatMode(); if (appCompatTaskInfo.topActivityInSizeCompat - && mWmService.mLetterboxConfiguration.isTranslucentLetterboxingEnabled()) { + && mWmService.mAppCompatConfiguration.isTranslucentLetterboxingEnabled()) { // We hide the restart button in case of transparent activities. appCompatTaskInfo.topActivityInSizeCompat = top.fillsParent(); } diff --git a/services/core/java/com/android/server/wm/TransparentPolicy.java b/services/core/java/com/android/server/wm/TransparentPolicy.java index cdb14ab1dc0a..2f46103fdf17 100644 --- a/services/core/java/com/android/server/wm/TransparentPolicy.java +++ b/services/core/java/com/android/server/wm/TransparentPolicy.java @@ -73,10 +73,10 @@ class TransparentPolicy { private final TransparentPolicyState mTransparentPolicyState; TransparentPolicy(@NonNull ActivityRecord activityRecord, - @NonNull LetterboxConfiguration letterboxConfiguration) { + @NonNull AppCompatConfiguration appCompatConfiguration) { mActivityRecord = activityRecord; mIsTranslucentLetterboxingEnabledSupplier = - letterboxConfiguration::isTranslucentLetterboxingEnabled; + appCompatConfiguration::isTranslucentLetterboxingEnabled; mTransparentPolicyState = new TransparentPolicyState(activityRecord); } diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index ebbf6e346e91..acd8b3f1dbc3 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -120,10 +120,10 @@ import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_W import static com.android.server.wm.ActivityTaskManagerService.POWER_MODE_REASON_CHANGE_DISPLAY; import static com.android.server.wm.DisplayContent.IME_TARGET_CONTROL; import static com.android.server.wm.DisplayContent.IME_TARGET_LAYERING; -import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND; -import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING; -import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_SOLID_COLOR; -import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_WALLPAPER; +import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND; +import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING; +import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_SOLID_COLOR; +import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_WALLPAPER; import static com.android.server.wm.RootWindowContainer.MATCH_ATTACHED_TASK_OR_RECENT_TASKS; import static com.android.server.wm.SensitiveContentPackages.PackageInfo; import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_ALL; @@ -1054,7 +1054,7 @@ public class WindowManagerService extends IWindowManager.Stub private boolean mAnimationsDisabled = false; boolean mPointerLocationEnabled = false; - final LetterboxConfiguration mLetterboxConfiguration; + final AppCompatConfiguration mAppCompatConfiguration; private boolean mIsIgnoreOrientationRequestDisabled; @@ -1294,7 +1294,7 @@ public class WindowManagerService extends IWindowManager.Stub | WindowInsets.Type.navigationBars(); } - mLetterboxConfiguration = new LetterboxConfiguration( + mAppCompatConfiguration = new AppCompatConfiguration( // Using SysUI context to have access to Material colors extracted from Wallpaper. ActivityThread.currentActivityThread().getSystemUiContext()); @@ -4441,7 +4441,7 @@ public class WindowManagerService extends IWindowManager.Stub */ boolean isIgnoreOrientationRequestDisabled() { return mIsIgnoreOrientationRequestDisabled - || !mLetterboxConfiguration.isIgnoreOrientationRequestAllowed(); + || !mAppCompatConfiguration.isIgnoreOrientationRequestAllowed(); } @Override @@ -9992,7 +9992,7 @@ public class WindowManagerService extends IWindowManager.Stub */ @Override public int getLetterboxBackgroundColorInArgb() { - return mLetterboxConfiguration.getLetterboxBackgroundColor().toArgb(); + return mAppCompatConfiguration.getLetterboxBackgroundColor().toArgb(); } /** @@ -10000,8 +10000,8 @@ public class WindowManagerService extends IWindowManager.Stub */ @Override public boolean isLetterboxBackgroundMultiColored() { - @LetterboxConfiguration.LetterboxBackgroundType int letterboxBackgroundType = - mLetterboxConfiguration.getLetterboxBackgroundType(); + @AppCompatConfiguration.LetterboxBackgroundType int letterboxBackgroundType = + mAppCompatConfiguration.getLetterboxBackgroundType(); switch (letterboxBackgroundType) { case LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING: case LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND: diff --git a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java index 6febe80166f4..51d5bc099bd6 100644 --- a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java +++ b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java @@ -19,16 +19,16 @@ package com.android.server.wm; import static android.os.Build.IS_USER; import static android.view.CrossWindowBlurListeners.CROSS_WINDOW_BLUR_SUPPORTED; -import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND; -import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING; -import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_SOLID_COLOR; -import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_WALLPAPER; -import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER; -import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT; -import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT; -import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM; -import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER; -import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP; +import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND; +import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING; +import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_SOLID_COLOR; +import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_WALLPAPER; +import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER; +import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT; +import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT; +import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM; +import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER; +import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP; import android.content.res.Resources.NotFoundException; import android.graphics.Color; @@ -50,12 +50,12 @@ import android.view.ViewDebug; import com.android.internal.os.ByteTransferPipe; import com.android.internal.protolog.LegacyProtoLogImpl; import com.android.internal.protolog.PerfettoProtoLogImpl; -import com.android.internal.protolog.common.IProtoLog; import com.android.internal.protolog.ProtoLog; +import com.android.internal.protolog.common.IProtoLog; import com.android.server.IoThread; -import com.android.server.wm.LetterboxConfiguration.LetterboxBackgroundType; -import com.android.server.wm.LetterboxConfiguration.LetterboxHorizontalReachabilityPosition; -import com.android.server.wm.LetterboxConfiguration.LetterboxVerticalReachabilityPosition; +import com.android.server.wm.AppCompatConfiguration.LetterboxBackgroundType; +import com.android.server.wm.AppCompatConfiguration.LetterboxHorizontalReachabilityPosition; +import com.android.server.wm.AppCompatConfiguration.LetterboxVerticalReachabilityPosition; import java.io.IOException; import java.io.PrintWriter; @@ -78,12 +78,12 @@ public class WindowManagerShellCommand extends ShellCommand { // Internal service impl -- must perform security checks before touching. private final WindowManagerService mInternal; - private final LetterboxConfiguration mLetterboxConfiguration; + private final AppCompatConfiguration mAppCompatConfiguration; public WindowManagerShellCommand(WindowManagerService service) { mInterface = service; mInternal = service; - mLetterboxConfiguration = service.mLetterboxConfiguration; + mAppCompatConfiguration = service.mAppCompatConfiguration; } @Override @@ -678,7 +678,7 @@ public class WindowManagerShellCommand extends ShellCommand { return -1; } synchronized (mInternal.mGlobalLock) { - mLetterboxConfiguration.setFixedOrientationLetterboxAspectRatio(aspectRatio); + mAppCompatConfiguration.setFixedOrientationLetterboxAspectRatio(aspectRatio); } return 0; } @@ -698,7 +698,7 @@ public class WindowManagerShellCommand extends ShellCommand { return -1; } synchronized (mInternal.mGlobalLock) { - mLetterboxConfiguration.setDefaultMinAspectRatioForUnresizableApps(aspectRatio); + mAppCompatConfiguration.setDefaultMinAspectRatioForUnresizableApps(aspectRatio); } return 0; } @@ -717,7 +717,7 @@ public class WindowManagerShellCommand extends ShellCommand { return -1; } synchronized (mInternal.mGlobalLock) { - mLetterboxConfiguration.setLetterboxActivityCornersRadius(cornersRadius); + mAppCompatConfiguration.setLetterboxActivityCornersRadius(cornersRadius); } return 0; } @@ -752,7 +752,7 @@ public class WindowManagerShellCommand extends ShellCommand { return -1; } synchronized (mInternal.mGlobalLock) { - mLetterboxConfiguration.setLetterboxBackgroundTypeOverride(backgroundType); + mAppCompatConfiguration.setLetterboxBackgroundTypeOverride(backgroundType); } return 0; } @@ -770,7 +770,7 @@ public class WindowManagerShellCommand extends ShellCommand { return -1; } synchronized (mInternal.mGlobalLock) { - mLetterboxConfiguration.setLetterboxBackgroundColorResourceId(colorId); + mAppCompatConfiguration.setLetterboxBackgroundColorResourceId(colorId); } return 0; } @@ -787,7 +787,7 @@ public class WindowManagerShellCommand extends ShellCommand { return -1; } synchronized (mInternal.mGlobalLock) { - mLetterboxConfiguration.setLetterboxBackgroundColor(color); + mAppCompatConfiguration.setLetterboxBackgroundColor(color); } return 0; } @@ -809,7 +809,7 @@ public class WindowManagerShellCommand extends ShellCommand { synchronized (mInternal.mGlobalLock) { final int radiusPx = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, radiusDp, mInternal.mContext.getResources().getDisplayMetrics()); - mLetterboxConfiguration.setLetterboxBackgroundWallpaperBlurRadiusPx(radiusPx); + mAppCompatConfiguration.setLetterboxBackgroundWallpaperBlurRadiusPx(radiusPx); } return 0; } @@ -829,7 +829,7 @@ public class WindowManagerShellCommand extends ShellCommand { return -1; } synchronized (mInternal.mGlobalLock) { - mLetterboxConfiguration.setLetterboxBackgroundWallpaperDarkScrimAlpha(alpha); + mAppCompatConfiguration.setLetterboxBackgroundWallpaperDarkScrimAlpha(alpha); } return 0; } @@ -849,7 +849,7 @@ public class WindowManagerShellCommand extends ShellCommand { } synchronized (mInternal.mGlobalLock) { try { - mLetterboxConfiguration.setLetterboxHorizontalPositionMultiplier(multiplier); + mAppCompatConfiguration.setLetterboxHorizontalPositionMultiplier(multiplier); } catch (IllegalArgumentException e) { getErrPrintWriter().println("Error: invalid multiplier value " + e); return -1; @@ -873,7 +873,7 @@ public class WindowManagerShellCommand extends ShellCommand { } synchronized (mInternal.mGlobalLock) { try { - mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(multiplier); + mAppCompatConfiguration.setLetterboxVerticalPositionMultiplier(multiplier); } catch (IllegalArgumentException e) { getErrPrintWriter().println("Error: invalid multiplier value " + e); return -1; @@ -908,7 +908,7 @@ public class WindowManagerShellCommand extends ShellCommand { return -1; } synchronized (mInternal.mGlobalLock) { - mLetterboxConfiguration.setDefaultPositionForHorizontalReachability(position); + mAppCompatConfiguration.setDefaultPositionForHorizontalReachability(position); } return 0; } @@ -939,7 +939,7 @@ public class WindowManagerShellCommand extends ShellCommand { return -1; } synchronized (mInternal.mGlobalLock) { - mLetterboxConfiguration.setDefaultPositionForVerticalReachability(position); + mAppCompatConfiguration.setDefaultPositionForVerticalReachability(position); } return 0; } @@ -970,7 +970,7 @@ public class WindowManagerShellCommand extends ShellCommand { return -1; } synchronized (mInternal.mGlobalLock) { - mLetterboxConfiguration.setPersistentLetterboxPositionForHorizontalReachability( + mAppCompatConfiguration.setPersistentLetterboxPositionForHorizontalReachability( false /* IsInBookMode */, position); } return 0; @@ -1002,7 +1002,7 @@ public class WindowManagerShellCommand extends ShellCommand { return -1; } synchronized (mInternal.mGlobalLock) { - mLetterboxConfiguration.setPersistentLetterboxPositionForVerticalReachability( + mAppCompatConfiguration.setPersistentLetterboxPositionForVerticalReachability( false /* forTabletopMode */, position); } return 0; @@ -1074,15 +1074,15 @@ public class WindowManagerShellCommand extends ShellCommand { runSetLetterboxVerticalPositionMultiplier(pw); break; case "--isHorizontalReachabilityEnabled": - runSetBooleanFlag(pw, mLetterboxConfiguration + runSetBooleanFlag(pw, mAppCompatConfiguration ::setIsHorizontalReachabilityEnabled); break; case "--isVerticalReachabilityEnabled": - runSetBooleanFlag(pw, mLetterboxConfiguration + runSetBooleanFlag(pw, mAppCompatConfiguration ::setIsVerticalReachabilityEnabled); break; case "--isAutomaticReachabilityInBookModeEnabled": - runSetBooleanFlag(pw, mLetterboxConfiguration + runSetBooleanFlag(pw, mAppCompatConfiguration ::setIsAutomaticReachabilityInBookModeEnabled); break; case "--defaultPositionForHorizontalReachability": @@ -1098,34 +1098,34 @@ public class WindowManagerShellCommand extends ShellCommand { runSetPersistentLetterboxPositionForVerticalReachability(pw); break; case "--isEducationEnabled": - runSetBooleanFlag(pw, mLetterboxConfiguration::setIsEducationEnabled); + runSetBooleanFlag(pw, mAppCompatConfiguration::setIsEducationEnabled); break; case "--isSplitScreenAspectRatioForUnresizableAppsEnabled": - runSetBooleanFlag(pw, mLetterboxConfiguration + runSetBooleanFlag(pw, mAppCompatConfiguration ::setIsSplitScreenAspectRatioForUnresizableAppsEnabled); break; case "--isDisplayAspectRatioEnabledForFixedOrientationLetterbox": - runSetBooleanFlag(pw, mLetterboxConfiguration + runSetBooleanFlag(pw, mAppCompatConfiguration ::setIsDisplayAspectRatioEnabledForFixedOrientationLetterbox); break; case "--isTranslucentLetterboxingEnabled": - runSetBooleanFlag(pw, mLetterboxConfiguration + runSetBooleanFlag(pw, mAppCompatConfiguration ::setTranslucentLetterboxingOverrideEnabled); break; case "--isUserAppAspectRatioSettingsEnabled": - runSetBooleanFlag(pw, mLetterboxConfiguration + runSetBooleanFlag(pw, mAppCompatConfiguration ::setUserAppAspectRatioSettingsOverrideEnabled); break; case "--isUserAppAspectRatioFullscreenEnabled": - runSetBooleanFlag(pw, mLetterboxConfiguration + runSetBooleanFlag(pw, mAppCompatConfiguration ::setUserAppAspectRatioFullscreenOverrideEnabled); break; case "--isCameraCompatRefreshEnabled": - runSetBooleanFlag(pw, mLetterboxConfiguration::setCameraCompatRefreshEnabled); + runSetBooleanFlag(pw, mAppCompatConfiguration::setCameraCompatRefreshEnabled); break; case "--isCameraCompatRefreshCycleThroughStopEnabled": runSetBooleanFlag(pw, - mLetterboxConfiguration::setCameraCompatRefreshCycleThroughStopEnabled); + mAppCompatConfiguration::setCameraCompatRefreshCycleThroughStopEnabled); break; default: getErrPrintWriter().println( @@ -1145,77 +1145,77 @@ public class WindowManagerShellCommand extends ShellCommand { String arg = getNextArg(); switch (arg) { case "aspectRatio": - mLetterboxConfiguration.resetFixedOrientationLetterboxAspectRatio(); + mAppCompatConfiguration.resetFixedOrientationLetterboxAspectRatio(); break; case "minAspectRatioForUnresizable": - mLetterboxConfiguration.resetDefaultMinAspectRatioForUnresizableApps(); + mAppCompatConfiguration.resetDefaultMinAspectRatioForUnresizableApps(); break; case "cornerRadius": - mLetterboxConfiguration.resetLetterboxActivityCornersRadius(); + mAppCompatConfiguration.resetLetterboxActivityCornersRadius(); break; case "backgroundType": - mLetterboxConfiguration.resetLetterboxBackgroundType(); + mAppCompatConfiguration.resetLetterboxBackgroundType(); break; case "backgroundColor": - mLetterboxConfiguration.resetLetterboxBackgroundColor(); + mAppCompatConfiguration.resetLetterboxBackgroundColor(); break; case "wallpaperBlurRadius": - mLetterboxConfiguration.resetLetterboxBackgroundWallpaperBlurRadiusPx(); + mAppCompatConfiguration.resetLetterboxBackgroundWallpaperBlurRadiusPx(); break; case "wallpaperDarkScrimAlpha": - mLetterboxConfiguration.resetLetterboxBackgroundWallpaperDarkScrimAlpha(); + mAppCompatConfiguration.resetLetterboxBackgroundWallpaperDarkScrimAlpha(); break; case "horizontalPositionMultiplier": - mLetterboxConfiguration.resetLetterboxHorizontalPositionMultiplier(); + mAppCompatConfiguration.resetLetterboxHorizontalPositionMultiplier(); break; case "verticalPositionMultiplier": - mLetterboxConfiguration.resetLetterboxVerticalPositionMultiplier(); + mAppCompatConfiguration.resetLetterboxVerticalPositionMultiplier(); break; case "isHorizontalReachabilityEnabled": - mLetterboxConfiguration.resetIsHorizontalReachabilityEnabled(); + mAppCompatConfiguration.resetIsHorizontalReachabilityEnabled(); break; case "isVerticalReachabilityEnabled": - mLetterboxConfiguration.resetIsVerticalReachabilityEnabled(); + mAppCompatConfiguration.resetIsVerticalReachabilityEnabled(); break; case "defaultPositionForHorizontalReachability": - mLetterboxConfiguration.resetDefaultPositionForHorizontalReachability(); + mAppCompatConfiguration.resetDefaultPositionForHorizontalReachability(); break; case "defaultPositionForVerticalReachability": - mLetterboxConfiguration.resetDefaultPositionForVerticalReachability(); + mAppCompatConfiguration.resetDefaultPositionForVerticalReachability(); break; case "persistentPositionForHorizontalReachability": - mLetterboxConfiguration + mAppCompatConfiguration .resetPersistentLetterboxPositionForHorizontalReachability(); break; case "persistentPositionForVerticalReachability": - mLetterboxConfiguration + mAppCompatConfiguration .resetPersistentLetterboxPositionForVerticalReachability(); break; case "isEducationEnabled": - mLetterboxConfiguration.resetIsEducationEnabled(); + mAppCompatConfiguration.resetIsEducationEnabled(); break; case "isSplitScreenAspectRatioForUnresizableAppsEnabled": - mLetterboxConfiguration + mAppCompatConfiguration .resetIsSplitScreenAspectRatioForUnresizableAppsEnabled(); break; case "IsDisplayAspectRatioEnabledForFixedOrientationLetterbox": - mLetterboxConfiguration + mAppCompatConfiguration .resetIsDisplayAspectRatioEnabledForFixedOrientationLetterbox(); break; case "isTranslucentLetterboxingEnabled": - mLetterboxConfiguration.resetTranslucentLetterboxingEnabled(); + mAppCompatConfiguration.resetTranslucentLetterboxingEnabled(); break; case "isUserAppAspectRatioSettingsEnabled": - mLetterboxConfiguration.resetUserAppAspectRatioSettingsEnabled(); + mAppCompatConfiguration.resetUserAppAspectRatioSettingsEnabled(); break; case "isUserAppAspectRatioFullscreenEnabled": - mLetterboxConfiguration.resetUserAppAspectRatioFullscreenEnabled(); + mAppCompatConfiguration.resetUserAppAspectRatioFullscreenEnabled(); break; case "isCameraCompatRefreshEnabled": - mLetterboxConfiguration.resetCameraCompatRefreshEnabled(); + mAppCompatConfiguration.resetCameraCompatRefreshEnabled(); break; case "isCameraCompatRefreshCycleThroughStopEnabled": - mLetterboxConfiguration + mAppCompatConfiguration .resetCameraCompatRefreshCycleThroughStopEnabled(); break; default: @@ -1304,104 +1304,104 @@ public class WindowManagerShellCommand extends ShellCommand { private void resetLetterboxStyle() { synchronized (mInternal.mGlobalLock) { - mLetterboxConfiguration.resetFixedOrientationLetterboxAspectRatio(); - mLetterboxConfiguration.resetDefaultMinAspectRatioForUnresizableApps(); - mLetterboxConfiguration.resetLetterboxActivityCornersRadius(); - mLetterboxConfiguration.resetLetterboxBackgroundType(); - mLetterboxConfiguration.resetLetterboxBackgroundColor(); - mLetterboxConfiguration.resetLetterboxBackgroundWallpaperBlurRadiusPx(); - mLetterboxConfiguration.resetLetterboxBackgroundWallpaperDarkScrimAlpha(); - mLetterboxConfiguration.resetLetterboxHorizontalPositionMultiplier(); - mLetterboxConfiguration.resetLetterboxVerticalPositionMultiplier(); - mLetterboxConfiguration.resetIsHorizontalReachabilityEnabled(); - mLetterboxConfiguration.resetIsVerticalReachabilityEnabled(); - mLetterboxConfiguration.resetEnabledAutomaticReachabilityInBookMode(); - mLetterboxConfiguration.resetDefaultPositionForHorizontalReachability(); - mLetterboxConfiguration.resetDefaultPositionForVerticalReachability(); - mLetterboxConfiguration.resetPersistentLetterboxPositionForHorizontalReachability(); - mLetterboxConfiguration.resetPersistentLetterboxPositionForVerticalReachability(); - mLetterboxConfiguration.resetIsEducationEnabled(); - mLetterboxConfiguration.resetIsSplitScreenAspectRatioForUnresizableAppsEnabled(); - mLetterboxConfiguration.resetIsDisplayAspectRatioEnabledForFixedOrientationLetterbox(); - mLetterboxConfiguration.resetTranslucentLetterboxingEnabled(); - mLetterboxConfiguration.resetUserAppAspectRatioSettingsEnabled(); - mLetterboxConfiguration.resetUserAppAspectRatioFullscreenEnabled(); - mLetterboxConfiguration.resetCameraCompatRefreshEnabled(); - mLetterboxConfiguration.resetCameraCompatRefreshCycleThroughStopEnabled(); + mAppCompatConfiguration.resetFixedOrientationLetterboxAspectRatio(); + mAppCompatConfiguration.resetDefaultMinAspectRatioForUnresizableApps(); + mAppCompatConfiguration.resetLetterboxActivityCornersRadius(); + mAppCompatConfiguration.resetLetterboxBackgroundType(); + mAppCompatConfiguration.resetLetterboxBackgroundColor(); + mAppCompatConfiguration.resetLetterboxBackgroundWallpaperBlurRadiusPx(); + mAppCompatConfiguration.resetLetterboxBackgroundWallpaperDarkScrimAlpha(); + mAppCompatConfiguration.resetLetterboxHorizontalPositionMultiplier(); + mAppCompatConfiguration.resetLetterboxVerticalPositionMultiplier(); + mAppCompatConfiguration.resetIsHorizontalReachabilityEnabled(); + mAppCompatConfiguration.resetIsVerticalReachabilityEnabled(); + mAppCompatConfiguration.resetEnabledAutomaticReachabilityInBookMode(); + mAppCompatConfiguration.resetDefaultPositionForHorizontalReachability(); + mAppCompatConfiguration.resetDefaultPositionForVerticalReachability(); + mAppCompatConfiguration.resetPersistentLetterboxPositionForHorizontalReachability(); + mAppCompatConfiguration.resetPersistentLetterboxPositionForVerticalReachability(); + mAppCompatConfiguration.resetIsEducationEnabled(); + mAppCompatConfiguration.resetIsSplitScreenAspectRatioForUnresizableAppsEnabled(); + mAppCompatConfiguration.resetIsDisplayAspectRatioEnabledForFixedOrientationLetterbox(); + mAppCompatConfiguration.resetTranslucentLetterboxingEnabled(); + mAppCompatConfiguration.resetUserAppAspectRatioSettingsEnabled(); + mAppCompatConfiguration.resetUserAppAspectRatioFullscreenEnabled(); + mAppCompatConfiguration.resetCameraCompatRefreshEnabled(); + mAppCompatConfiguration.resetCameraCompatRefreshCycleThroughStopEnabled(); } } private int runGetLetterboxStyle(PrintWriter pw) throws RemoteException { synchronized (mInternal.mGlobalLock) { pw.println("Corner radius: " - + mLetterboxConfiguration.getLetterboxActivityCornersRadius()); + + mAppCompatConfiguration.getLetterboxActivityCornersRadius()); pw.println("Horizontal position multiplier: " - + mLetterboxConfiguration.getLetterboxHorizontalPositionMultiplier( + + mAppCompatConfiguration.getLetterboxHorizontalPositionMultiplier( false /* isInBookMode */)); pw.println("Vertical position multiplier: " - + mLetterboxConfiguration.getLetterboxVerticalPositionMultiplier( + + mAppCompatConfiguration.getLetterboxVerticalPositionMultiplier( false /* isInTabletopMode */)); pw.println("Horizontal position multiplier (book mode): " - + mLetterboxConfiguration.getLetterboxHorizontalPositionMultiplier( + + mAppCompatConfiguration.getLetterboxHorizontalPositionMultiplier( true /* isInBookMode */)); pw.println("Vertical position multiplier (tabletop mode): " - + mLetterboxConfiguration.getLetterboxVerticalPositionMultiplier( + + mAppCompatConfiguration.getLetterboxVerticalPositionMultiplier( true /* isInTabletopMode */)); pw.println("Horizontal position multiplier for reachability: " - + mLetterboxConfiguration.getHorizontalMultiplierForReachability( + + mAppCompatConfiguration.getHorizontalMultiplierForReachability( false /* isInBookMode */)); pw.println("Vertical position multiplier for reachability: " - + mLetterboxConfiguration.getVerticalMultiplierForReachability( + + mAppCompatConfiguration.getVerticalMultiplierForReachability( false /* isInTabletopMode */)); pw.println("Aspect ratio: " - + mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio()); + + mAppCompatConfiguration.getFixedOrientationLetterboxAspectRatio()); pw.println("Default min aspect ratio for unresizable apps: " - + mLetterboxConfiguration.getDefaultMinAspectRatioForUnresizableApps()); + + mAppCompatConfiguration.getDefaultMinAspectRatioForUnresizableApps()); pw.println("Is horizontal reachability enabled: " - + mLetterboxConfiguration.getIsHorizontalReachabilityEnabled()); + + mAppCompatConfiguration.getIsHorizontalReachabilityEnabled()); pw.println("Is vertical reachability enabled: " - + mLetterboxConfiguration.getIsVerticalReachabilityEnabled()); + + mAppCompatConfiguration.getIsVerticalReachabilityEnabled()); pw.println("Is automatic reachability in book mode enabled: " - + mLetterboxConfiguration.getIsAutomaticReachabilityInBookModeEnabled()); + + mAppCompatConfiguration.getIsAutomaticReachabilityInBookModeEnabled()); pw.println("Default position for horizontal reachability: " - + LetterboxConfiguration.letterboxHorizontalReachabilityPositionToString( - mLetterboxConfiguration.getDefaultPositionForHorizontalReachability())); + + AppCompatConfiguration.letterboxHorizontalReachabilityPositionToString( + mAppCompatConfiguration.getDefaultPositionForHorizontalReachability())); pw.println("Default position for vertical reachability: " - + LetterboxConfiguration.letterboxVerticalReachabilityPositionToString( - mLetterboxConfiguration.getDefaultPositionForVerticalReachability())); + + AppCompatConfiguration.letterboxVerticalReachabilityPositionToString( + mAppCompatConfiguration.getDefaultPositionForVerticalReachability())); pw.println("Current position for horizontal reachability:" - + LetterboxConfiguration.letterboxHorizontalReachabilityPositionToString( - mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability(false))); + + AppCompatConfiguration.letterboxHorizontalReachabilityPositionToString( + mAppCompatConfiguration.getLetterboxPositionForHorizontalReachability(false))); pw.println("Current position for vertical reachability:" - + LetterboxConfiguration.letterboxVerticalReachabilityPositionToString( - mLetterboxConfiguration.getLetterboxPositionForVerticalReachability(false))); + + AppCompatConfiguration.letterboxVerticalReachabilityPositionToString( + mAppCompatConfiguration.getLetterboxPositionForVerticalReachability(false))); pw.println("Is education enabled: " - + mLetterboxConfiguration.getIsEducationEnabled()); + + mAppCompatConfiguration.getIsEducationEnabled()); pw.println("Is using split screen aspect ratio as aspect ratio for unresizable apps: " - + mLetterboxConfiguration + + mAppCompatConfiguration .getIsSplitScreenAspectRatioForUnresizableAppsEnabled()); pw.println("Is using display aspect ratio as aspect ratio for all letterboxed apps: " - + mLetterboxConfiguration + + mAppCompatConfiguration .getIsDisplayAspectRatioEnabledForFixedOrientationLetterbox()); pw.println(" Is activity \"refresh\" in camera compatibility treatment enabled: " - + mLetterboxConfiguration.isCameraCompatRefreshEnabled()); + + mAppCompatConfiguration.isCameraCompatRefreshEnabled()); pw.println(" Refresh using \"stopped -> resumed\" cycle: " - + mLetterboxConfiguration.isCameraCompatRefreshCycleThroughStopEnabled()); + + mAppCompatConfiguration.isCameraCompatRefreshCycleThroughStopEnabled()); pw.println("Background type: " - + LetterboxConfiguration.letterboxBackgroundTypeToString( - mLetterboxConfiguration.getLetterboxBackgroundType())); + + AppCompatConfiguration.letterboxBackgroundTypeToString( + mAppCompatConfiguration.getLetterboxBackgroundType())); pw.println(" Background color: " + Integer.toHexString( - mLetterboxConfiguration.getLetterboxBackgroundColor().toArgb())); + mAppCompatConfiguration.getLetterboxBackgroundColor().toArgb())); pw.println(" Wallpaper blur radius: " - + mLetterboxConfiguration.getLetterboxBackgroundWallpaperBlurRadiusPx()); + + mAppCompatConfiguration.getLetterboxBackgroundWallpaperBlurRadiusPx()); pw.println(" Wallpaper dark scrim alpha: " - + mLetterboxConfiguration.getLetterboxBackgroundWallpaperDarkScrimAlpha()); + + mAppCompatConfiguration.getLetterboxBackgroundWallpaperDarkScrimAlpha()); pw.println("Is letterboxing for translucent activities enabled: " - + mLetterboxConfiguration.isTranslucentLetterboxingEnabled()); + + mAppCompatConfiguration.isTranslucentLetterboxingEnabled()); pw.println("Is the user aspect ratio settings enabled: " - + mLetterboxConfiguration.isUserAppAspectRatioSettingsEnabled()); + + mAppCompatConfiguration.isUserAppAspectRatioSettingsEnabled()); pw.println("Is the fullscreen option in user aspect ratio settings enabled: " - + mLetterboxConfiguration.isUserAppAspectRatioFullscreenEnabled()); + + mAppCompatConfiguration.isUserAppAspectRatioFullscreenEnabled()); } return 0; } @@ -1539,13 +1539,13 @@ public class WindowManagerShellCommand extends ShellCommand { pw.println(" Sets letterbox style using the following options:"); pw.println(" --aspectRatio aspectRatio"); pw.println(" Aspect ratio of letterbox for fixed orientation. If aspectRatio <= " - + LetterboxConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO); + + AppCompatConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO); pw.println(" both it and R.dimen.config_fixedOrientationLetterboxAspectRatio will"); pw.println(" be ignored and framework implementation will determine aspect ratio."); pw.println(" --minAspectRatioForUnresizable aspectRatio"); pw.println(" Default min aspect ratio for unresizable apps which is used when an"); pw.println(" app is eligible for the size compat mode. If aspectRatio <= " - + LetterboxConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO); + + AppCompatConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO); pw.println(" both it and R.dimen.config_fixedOrientationLetterboxAspectRatio will"); pw.println(" be ignored and framework implementation will determine aspect ratio."); pw.println(" --cornerRadius radius"); diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java index 60d3e787cac4..12c50739f66b 100644 --- a/services/core/java/com/android/server/wm/WindowProcessController.java +++ b/services/core/java/com/android/server/wm/WindowProcessController.java @@ -19,6 +19,7 @@ package com.android.server.wm; import static android.app.ActivityManager.PROCESS_STATE_CACHED_ACTIVITY; import static android.app.ActivityManager.PROCESS_STATE_NONEXISTENT; import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.content.res.Configuration.ASSETS_SEQ_UNDEFINED; import static android.os.Build.VERSION_CODES.Q; @@ -73,6 +74,7 @@ import android.os.LocaleList; import android.os.Message; import android.os.Process; import android.os.RemoteException; +import android.os.SystemProperties; import android.os.UserHandle; import android.util.ArrayMap; import android.util.Log; @@ -112,6 +114,13 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio private static final String TAG_RELEASE = TAG + POSTFIX_RELEASE; private static final String TAG_CONFIGURATION = TAG + POSTFIX_CONFIGURATION; + /** + * The max number of processes which can be top scheduling group if there are non-top visible + * freeform activities run in the process. + */ + private static final int MAX_NUM_PERCEPTIBLE_FREEFORM = + SystemProperties.getInt("persist.wm.max_num_perceptible_freeform", 1); + private static final int MAX_RAPID_ACTIVITY_LAUNCH_COUNT = 200; private static final long RAPID_ACTIVITY_LAUNCH_MS = 500; private static final long RESET_RAPID_ACTIVITY_LAUNCH_MS = 3 * RAPID_ACTIVITY_LAUNCH_MS; @@ -318,6 +327,7 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio public static final int ACTIVITY_STATE_FLAG_HAS_RESUMED = 1 << 21; public static final int ACTIVITY_STATE_FLAG_HAS_ACTIVITY_IN_VISIBLE_TASK = 1 << 22; public static final int ACTIVITY_STATE_FLAG_RESUMED_SPLIT_SCREEN = 1 << 23; + public static final int ACTIVITY_STATE_FLAG_PERCEPTIBLE_FREEFORM = 1 << 24; public static final int ACTIVITY_STATE_FLAG_MASK_MIN_TASK_LAYER = 0x0000ffff; /** @@ -1229,6 +1239,7 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio ActivityRecord.State bestInvisibleState = DESTROYED; boolean allStoppingFinishing = true; boolean visible = false; + boolean hasResumedFreeform = false; int minTaskLayer = Integer.MAX_VALUE; int stateFlags = 0; final boolean wasResumed = hasResumedActivity(); @@ -1256,6 +1267,8 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio .processPriorityPolicyForMultiWindowMode() && task.getAdjacentTask() != null) { stateFlags |= ACTIVITY_STATE_FLAG_RESUMED_SPLIT_SCREEN; + } else if (windowingMode == WINDOWING_MODE_FREEFORM) { + hasResumedFreeform = true; } } if (minTaskLayer > 0) { @@ -1289,6 +1302,12 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio } } + if (hasResumedFreeform + && com.android.window.flags.Flags.processPriorityPolicyForMultiWindowMode() + // Exclude task layer 1 because it is already the top most. + && minTaskLayer > 1 && minTaskLayer <= 1 + MAX_NUM_PERCEPTIBLE_FREEFORM) { + stateFlags |= ACTIVITY_STATE_FLAG_PERCEPTIBLE_FREEFORM; + } stateFlags |= minTaskLayer & ACTIVITY_STATE_FLAG_MASK_MIN_TASK_LAYER; if (visible) { stateFlags |= ACTIVITY_STATE_FLAG_IS_VISIBLE; @@ -2105,6 +2124,9 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio if ((stateFlags & ACTIVITY_STATE_FLAG_RESUMED_SPLIT_SCREEN) != 0) { pw.print("RS|"); } + if ((stateFlags & ACTIVITY_STATE_FLAG_PERCEPTIBLE_FREEFORM) != 0) { + pw.print("PF|"); + } } } else if ((stateFlags & ACTIVITY_STATE_FLAG_IS_PAUSING_OR_PAUSED) != 0) { pw.print("P|"); diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 9ebb89dfe9b6..a36cff6d7bc5 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -4648,14 +4648,16 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP if (!isImeLayeringTarget()) { return false; } - // Note that we don't process IME window if the IME input target is not on the screen. - // In case some unexpected IME visibility cases happen like starting the remote - // animation on the keyguard but seeing the IME window that originally on the app - // which behinds the keyguard. - final WindowState imeInputTarget = getImeInputTarget(); - if (imeInputTarget != null - && !(imeInputTarget.isDrawn() || imeInputTarget.isVisibleRequested())) { - return false; + if (!com.android.window.flags.Flags.doNotSkipImeByTargetVisibility()) { + // Note that we don't process IME window if the IME input target is not on the screen. + // In case some unexpected IME visibility cases happen like starting the remote + // animation on the keyguard but seeing the IME window that originally on the app + // which behinds the keyguard. + final WindowState imeInputTarget = getImeInputTarget(); + if (imeInputTarget != null + && !(imeInputTarget.isDrawn() || imeInputTarget.isVisibleRequested())) { + return false; + } } return mDisplayContent.forAllImeWindows(callback, traverseTopToBottom); } @@ -5504,7 +5506,8 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP @Override public SurfaceControl getAnimationLeashParent() { - if (isStartingWindowAssociatedToTask()) { + if (mActivityRecord != null && !mActivityRecord.hasFixedRotationTransform() + && isStartingWindowAssociatedToTask()) { return mStartingData.mAssociatedTask.mSurfaceControl; } return super.getAnimationLeashParent(); diff --git a/services/core/java/com/android/server/wm/utils/DesktopModeFlagsUtil.java b/services/core/java/com/android/server/wm/utils/DesktopModeFlagsUtil.java new file mode 100644 index 000000000000..4211764085b1 --- /dev/null +++ b/services/core/java/com/android/server/wm/utils/DesktopModeFlagsUtil.java @@ -0,0 +1,173 @@ +/* + * 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.utils; + +import static com.android.server.wm.utils.DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET; + +import android.annotation.Nullable; +import android.content.Context; +import android.provider.Settings; +import android.util.Log; + +import com.android.window.flags.Flags; + +import java.util.function.Supplier; + +/** + * Util to check desktop mode flags state. + * + * This utility is used to allow developer option toggles to override flags related to desktop + * windowing. + * + * Computes whether Desktop Windowing related flags should be enabled by using the aconfig flag + * value and the developer option override state (if applicable). + * + * This is a partial copy of {@link com.android.wm.shell.shared.desktopmode.DesktopModeFlags} which + * is to be used in WM core. + */ +public enum DesktopModeFlagsUtil { + // All desktop mode related flags to be overridden by developer option toggle will be added here + DESKTOP_WINDOWING_MODE( + Flags::enableDesktopWindowingMode, /* shouldOverrideByDevOption= */ true), + WALLPAPER_ACTIVITY( + Flags::enableDesktopWindowingWallpaperActivity, /* shouldOverrideByDevOption= */ true); + + private static final String TAG = "DesktopModeFlagsUtil"; + private static final String SYSTEM_PROPERTY_OVERRIDE_KEY = + "sys.wmshell.desktopmode.dev_toggle_override"; + + // Function called to obtain aconfig flag value. + private final Supplier<Boolean> mFlagFunction; + // Whether the flag state should be affected by developer option. + private final boolean mShouldOverrideByDevOption; + + // Local cache for toggle override, which is initialized once on its first access. It needs to + // be refreshed only on reboots as overridden state takes effect on reboots. + private static ToggleOverride sCachedToggleOverride; + + DesktopModeFlagsUtil(Supplier<Boolean> flagFunction, boolean shouldOverrideByDevOption) { + this.mFlagFunction = flagFunction; + this.mShouldOverrideByDevOption = shouldOverrideByDevOption; + } + + /** + * Determines state of flag based on the actual flag and desktop mode developer option + * overrides. + * + * Note: this method makes sure that a constant developer toggle overrides is read until + * reboot. + */ + public boolean isEnabled(Context context) { + if (!Flags.showDesktopWindowingDevOption() + || !mShouldOverrideByDevOption + || context.getContentResolver() == null) { + return mFlagFunction.get(); + } else { + boolean shouldToggleBeEnabledByDefault = Flags.enableDesktopWindowingMode(); + return switch (getToggleOverride(context)) { + case OVERRIDE_UNSET -> mFlagFunction.get(); + // When toggle override matches its default state, don't override flags. This + // helps users reset their feature overrides. + case OVERRIDE_OFF -> !shouldToggleBeEnabledByDefault && mFlagFunction.get(); + case OVERRIDE_ON -> shouldToggleBeEnabledByDefault ? mFlagFunction.get() : true; + }; + } + } + + private ToggleOverride getToggleOverride(Context context) { + // If cached, return it + if (sCachedToggleOverride != null) { + return sCachedToggleOverride; + } + + // Otherwise, fetch and cache it + ToggleOverride override = getToggleOverrideFromSystem(context); + sCachedToggleOverride = override; + Log.d(TAG, "Toggle override initialized to: " + override); + return override; + } + + /** + * Returns {@link ToggleOverride} from a non-persistent system property if present. Otherwise + * initializes the system property by reading Settings.Global. + */ + private ToggleOverride getToggleOverrideFromSystem(Context context) { + // A non-persistent System Property is used to store override to ensure it remains + // constant till reboot. + String overrideProperty = System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, null); + ToggleOverride overrideFromSystemProperties = convertToToggleOverride(overrideProperty); + + // If valid system property, return it + if (overrideFromSystemProperties != null) { + return overrideFromSystemProperties; + } + + // Fallback when System Property is not present (just after reboot) or not valid (user + // manually changed the value): Read from Settings.Global + int settingValue = Settings.Global.getInt( + context.getContentResolver(), + Settings.Global.DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES, + OVERRIDE_UNSET.getSetting() + ); + ToggleOverride overrideFromSettingsGlobal = + ToggleOverride.fromSetting(settingValue, OVERRIDE_UNSET); + // Initialize System Property + System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, String.valueOf(settingValue)); + return overrideFromSettingsGlobal; + } + + /** + * Converts {@code intString} into {@link ToggleOverride}. Return {@code null} if + * {@code intString} does not correspond to a {@link ToggleOverride}. + */ + private static @Nullable ToggleOverride convertToToggleOverride( + @Nullable String intString + ) { + if (intString == null) return null; + try { + int intValue = Integer.parseInt(intString); + return ToggleOverride.fromSetting(intValue, null); + } catch (NumberFormatException e) { + Log.w(TAG, "Unknown toggleOverride int " + intString); + return null; + } + } + + /** Override state of desktop mode developer option toggle. */ + enum ToggleOverride { + OVERRIDE_UNSET, + OVERRIDE_OFF, + OVERRIDE_ON; + + int getSetting() { + return switch (this) { + case OVERRIDE_ON -> 1; + case OVERRIDE_OFF -> 0; + case OVERRIDE_UNSET -> -1; + }; + } + + static ToggleOverride fromSetting(int setting, @Nullable ToggleOverride fallback) { + return switch (setting) { + case 1 -> OVERRIDE_ON; + case 0 -> OVERRIDE_OFF; + case -1 -> OVERRIDE_UNSET; + default -> fallback; + }; + } + } +} diff --git a/services/core/jni/com_android_server_UsbDeviceManager.cpp b/services/core/jni/com_android_server_UsbDeviceManager.cpp index 9dc70afad9d9..4ef9cf4d2388 100644 --- a/services/core/jni/com_android_server_UsbDeviceManager.cpp +++ b/services/core/jni/com_android_server_UsbDeviceManager.cpp @@ -15,33 +15,168 @@ */ #define LOG_TAG "UsbDeviceManagerJNI" -#include "utils/Log.h" - -#include "jni.h" +#include <android-base/properties.h> +#include <android-base/unique_fd.h> +#include <core_jni_helpers.h> +#include <fcntl.h> +#include <linux/usb/f_accessory.h> #include <nativehelper/JNIPlatformHelp.h> #include <nativehelper/ScopedUtfChars.h> -#include "android_runtime/AndroidRuntime.h" -#include "android_runtime/Log.h" -#include "MtpDescriptors.h" - #include <stdio.h> -#include <sys/types.h> -#include <sys/stat.h> -#include <fcntl.h> +#include <sys/epoll.h> #include <sys/ioctl.h> -#include <linux/usb/f_accessory.h> +#include <sys/stat.h> +#include <sys/types.h> + +#include <thread> + +#include "MtpDescriptors.h" +#include "android_runtime/AndroidRuntime.h" +#include "android_runtime/Log.h" +#include "jni.h" +#include "utils/Log.h" #define DRIVER_NAME "/dev/usb_accessory" +#define EPOLL_MAX_EVENTS 4 +#define USB_STATE_MAX_LEN 20 namespace android { +static JavaVM *gvm = nullptr; +static jmethodID gUpdateGadgetStateMethod; + static struct parcel_file_descriptor_offsets_t { jclass mClass; jmethodID mConstructor; } gParcelFileDescriptorOffsets; +/* + * NativeGadgetMonitorThread starts a new thread to monitor udc state by epoll, + * convert and update the state to UsbDeviceManager. + */ +class NativeGadgetMonitorThread { + android::base::unique_fd mMonitorFd; + int mPipefd[2]; + std::thread mThread; + jobject mCallbackObj; + std::string mGadgetState; + + void handleStateUpdate(const char *state) { + JNIEnv *env = AndroidRuntime::getJNIEnv(); + std::string gadgetState; + + if (!std::strcmp(state, "not attached\n")) { + gadgetState = "DISCONNECTED"; + } else if (!std::strcmp(state, "attached\n") || !std::strcmp(state, "powered\n") || + !std::strcmp(state, "default\n") || !std::strcmp(state, "addressed\n")) { + gadgetState = "CONNECTED"; + } else if (!std::strcmp(state, "configured\n")) { + gadgetState = "CONFIGURED"; + } else if (!std::strcmp(state, "suspended\n")) { + return; + } else { + ALOGE("Unknown gadget state %s", state); + return; + } + + if (mGadgetState.compare(gadgetState)) { + mGadgetState = gadgetState; + jstring obj = env->NewStringUTF(gadgetState.c_str()); + env->CallVoidMethod(mCallbackObj, gUpdateGadgetStateMethod, obj); + } + } + + int setupEpoll(android::base::unique_fd &epollFd) { + struct epoll_event ev; + + ev.data.fd = mMonitorFd.get(); + ev.events = EPOLLPRI; + if (epoll_ctl(epollFd.get(), EPOLL_CTL_ADD, mMonitorFd.get(), &ev) != 0) { + ALOGE("epoll_ctl failed for monitor fd; errno=%d", errno); + return errno; + } + + ev.data.fd = mPipefd[0]; + ev.events = EPOLLIN; + if (epoll_ctl(epollFd.get(), EPOLL_CTL_ADD, mPipefd[0], &ev) != 0) { + ALOGE("epoll_ctl failed for pipe fd; errno=%d", errno); + return errno; + } + + return 0; + } + + void monitorLoop() { + android::base::unique_fd epollFd(epoll_create(EPOLL_MAX_EVENTS)); + if (epollFd.get() == -1) { + ALOGE("epoll_create failed; errno=%d", errno); + return; + } + if (setupEpoll(epollFd) != 0) return; + + JNIEnv *env = nullptr; + JavaVMAttachArgs aargs = {JNI_VERSION_1_4, "NativeGadgetMonitorThread", nullptr}; + if (gvm->AttachCurrentThread(&env, &aargs) != JNI_OK || env == nullptr) { + ALOGE("Couldn't attach thread"); + return; + } + + struct epoll_event events[EPOLL_MAX_EVENTS]; + int nevents = 0; + while (true) { + nevents = epoll_wait(epollFd.get(), events, EPOLL_MAX_EVENTS, -1); + if (nevents < 0) { + ALOGE("usb epoll_wait failed; errno=%d", errno); + continue; + } + for (int i = 0; i < nevents; ++i) { + int fd = events[i].data.fd; + if (fd == mPipefd[0]) { + goto exit; + } else if (fd == mMonitorFd.get()) { + char state[USB_STATE_MAX_LEN] = {0}; + lseek(fd, 0, SEEK_SET); + read(fd, &state, USB_STATE_MAX_LEN); + handleStateUpdate(state); + } + } + } + + exit: + auto res = gvm->DetachCurrentThread(); + ALOGE_IF(res != JNI_OK, "Couldn't detach thread"); + return; + } + + void stop() { + if (mThread.joinable()) { + int c = 'q'; + write(mPipefd[1], &c, 1); + mThread.join(); + } + } + + DISALLOW_COPY_AND_ASSIGN(NativeGadgetMonitorThread); + +public: + explicit NativeGadgetMonitorThread(jobject obj, android::base::unique_fd monitorFd) + : mMonitorFd(std::move(monitorFd)), mGadgetState("") { + mCallbackObj = AndroidRuntime::getJNIEnv()->NewGlobalRef(obj); + pipe(mPipefd); + mThread = std::thread(&NativeGadgetMonitorThread::monitorLoop, this); + } + + ~NativeGadgetMonitorThread() { + stop(); + close(mPipefd[0]); + close(mPipefd[1]); + AndroidRuntime::getJNIEnv()->DeleteGlobalRef(mCallbackObj); + } +}; +static std::unique_ptr<NativeGadgetMonitorThread> sGadgetMonitorThread; + static void set_accessory_string(JNIEnv *env, int fd, int cmd, jobjectArray strArray, int index) { char buffer[256]; @@ -135,6 +270,41 @@ static jobject android_server_UsbDeviceManager_openControl(JNIEnv *env, jobject return jifd; } +static jboolean android_server_UsbDeviceManager_startGadgetMonitor(JNIEnv *env, jobject thiz, + jstring jUdcName) { + std::string filePath; + ScopedUtfChars udcName(env, jUdcName); + + filePath = "/sys/class/udc/" + std::string(udcName.c_str()) + "/state"; + android::base::unique_fd fd(open(filePath.c_str(), O_RDONLY)); + + if (fd.get() == -1) { + ALOGE("Cannot open %s", filePath.c_str()); + return JNI_FALSE; + } + + ALOGI("Start monitoring %s", filePath.c_str()); + sGadgetMonitorThread.reset(new NativeGadgetMonitorThread(thiz, std::move(fd))); + + return JNI_TRUE; +} + +static void android_server_UsbDeviceManager_stopGadgetMonitor(JNIEnv *env, jobject /* thiz */) { + sGadgetMonitorThread.reset(); + return; +} + +static jstring android_server_UsbDeviceManager_waitAndGetProperty(JNIEnv *env, jobject thiz, + jstring jPropName) { + ScopedUtfChars propName(env, jPropName); + std::string propValue; + + while (!android::base::WaitForPropertyCreation(propName.c_str())); + propValue = android::base::GetProperty(propName.c_str(), "" /* default */); + + return env->NewStringUTF(propValue.c_str()); +} + static const JNINativeMethod method_table[] = { {"nativeGetAccessoryStrings", "()[Ljava/lang/String;", (void *)android_server_UsbDeviceManager_getAccessoryStrings}, @@ -143,16 +313,26 @@ static const JNINativeMethod method_table[] = { {"nativeIsStartRequested", "()Z", (void *)android_server_UsbDeviceManager_isStartRequested}, {"nativeOpenControl", "(Ljava/lang/String;)Ljava/io/FileDescriptor;", (void *)android_server_UsbDeviceManager_openControl}, + {"nativeStartGadgetMonitor", "(Ljava/lang/String;)Z", + (void *)android_server_UsbDeviceManager_startGadgetMonitor}, + {"nativeStopGadgetMonitor", "()V", + (void *)android_server_UsbDeviceManager_stopGadgetMonitor}, + {"nativeWaitAndGetProperty", "(Ljava/lang/String;)Ljava/lang/String;", + (void *)android_server_UsbDeviceManager_waitAndGetProperty}, }; -int register_android_server_UsbDeviceManager(JNIEnv *env) -{ +int register_android_server_UsbDeviceManager(JavaVM *vm, JNIEnv *env) { + gvm = vm; + jclass clazz = env->FindClass("com/android/server/usb/UsbDeviceManager"); if (clazz == NULL) { ALOGE("Can't find com/android/server/usb/UsbDeviceManager"); return -1; } + gUpdateGadgetStateMethod = + GetMethodIDOrDie(env, clazz, "updateGadgetState", "(Ljava/lang/String;)V"); + clazz = env->FindClass("android/os/ParcelFileDescriptor"); LOG_FATAL_IF(clazz == NULL, "Unable to find class android.os.ParcelFileDescriptor"); gParcelFileDescriptorOffsets.mClass = (jclass) env->NewGlobalRef(clazz); @@ -163,5 +343,4 @@ int register_android_server_UsbDeviceManager(JNIEnv *env) return jniRegisterNativeMethods(env, "com/android/server/usb/UsbDeviceManager", method_table, NELEM(method_table)); } - }; diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp index 5719810abc5a..4d6a90c81b27 100644 --- a/services/core/jni/com_android_server_input_InputManagerService.cpp +++ b/services/core/jni/com_android_server_input_InputManagerService.cpp @@ -104,7 +104,6 @@ static const char* VELOCITYTRACKER_STRATEGY = "velocitytracker_strategy"; static struct { jclass clazz; - jmethodID notifyConfigurationChanged; jmethodID notifyInputDevicesChanged; jmethodID notifySwitch; jmethodID notifyInputChannelBroken; @@ -314,7 +313,6 @@ public: void getReaderConfiguration(InputReaderConfiguration* outConfig) override; void notifyInputDevicesChanged(const std::vector<InputDeviceInfo>& inputDevices) override; - void notifyConfigurationChanged(nsecs_t when) override; std::shared_ptr<KeyCharacterMap> getKeyboardLayoutOverlay( const InputDeviceIdentifier& identifier, const std::optional<KeyboardLayoutInfo> keyboardLayoutInfo) override; @@ -940,18 +938,6 @@ void NativeInputManager::notifySwitch(nsecs_t when, checkAndClearExceptionFromCallback(env, "notifySwitch"); } -void NativeInputManager::notifyConfigurationChanged(nsecs_t when) { -#if DEBUG_INPUT_DISPATCHER_POLICY - ALOGD("notifyConfigurationChanged - when=%lld", when); -#endif - ATRACE_CALL(); - - JNIEnv* env = jniEnv(); - - env->CallVoidMethod(mServiceObj, gServiceClassInfo.notifyConfigurationChanged, when); - checkAndClearExceptionFromCallback(env, "notifyConfigurationChanged"); -} - static jobject getInputApplicationHandleObjLocalRef( JNIEnv* env, const std::shared_ptr<InputApplicationHandle>& inputApplicationHandle) { if (inputApplicationHandle == nullptr) { @@ -2873,9 +2859,6 @@ int register_android_server_InputManager(JNIEnv* env) { FIND_CLASS(clazz, "com/android/server/input/InputManagerService"); gServiceClassInfo.clazz = reinterpret_cast<jclass>(env->NewGlobalRef(clazz)); - GET_METHOD_ID(gServiceClassInfo.notifyConfigurationChanged, clazz, - "notifyConfigurationChanged", "(J)V"); - GET_METHOD_ID(gServiceClassInfo.notifyInputDevicesChanged, clazz, "notifyInputDevicesChanged", "([Landroid/view/InputDevice;)V"); diff --git a/services/core/jni/onload.cpp b/services/core/jni/onload.cpp index 6464081d615a..314ff9d3c808 100644 --- a/services/core/jni/onload.cpp +++ b/services/core/jni/onload.cpp @@ -37,7 +37,7 @@ int register_android_server_SerialService(JNIEnv* env); int register_android_server_SystemServer(JNIEnv* env); int register_android_server_UsbAlsaJackDetector(JNIEnv* env); int register_android_server_UsbAlsaMidiDevice(JNIEnv* env); -int register_android_server_UsbDeviceManager(JNIEnv* env); +int register_android_server_UsbDeviceManager(JavaVM* vm, JNIEnv* env); int register_android_server_UsbHostManager(JNIEnv* env); int register_android_server_vr_VrManagerService(JNIEnv* env); int register_android_server_vibrator_VibratorController(JavaVM* vm, JNIEnv* env); @@ -96,7 +96,7 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) register_android_server_SerialService(env); register_android_server_InputManager(env); register_android_server_LightsService(env); - register_android_server_UsbDeviceManager(env); + register_android_server_UsbDeviceManager(vm, env); register_android_server_UsbAlsaJackDetector(env); register_android_server_UsbAlsaMidiDevice(env); register_android_server_UsbHostManager(env); diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java b/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java index dc8cec91001b..6a0dd5a04f82 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java @@ -182,6 +182,7 @@ class ActiveAdmin { private static final String TAG_CREDENTIAL_MANAGER_POLICY = "credential-manager-policy"; private static final String TAG_DIALER_PACKAGE = "dialer_package"; private static final String TAG_SMS_PACKAGE = "sms_package"; + private static final String TAG_PROVISIONING_CONTEXT = "provisioning-context"; // If the ActiveAdmin is a permission-based admin, then info will be null because the // permission-based admin is not mapped to a device administrator component. @@ -359,6 +360,8 @@ class ActiveAdmin { int mWifiMinimumSecurityLevel = DevicePolicyManager.WIFI_SECURITY_OPEN; String mDialerPackage; String mSmsPackage; + private String mProvisioningContext; + private static final int PROVISIONING_CONTEXT_LENGTH_LIMIT = 1000; ActiveAdmin(DeviceAdminInfo info, boolean isParent) { this.userId = -1; @@ -404,6 +407,23 @@ class ActiveAdmin { return UserHandle.of(UserHandle.getUserId(info.getActivityInfo().applicationInfo.uid)); } + /** + * Stores metadata about context of setting an active admin + * @param provisioningContext some metadata, for example test method name + */ + public void setProvisioningContext(@Nullable String provisioningContext) { + if (Flags.provisioningContextParameter() + && !TextUtils.isEmpty(provisioningContext) + && !provisioningContext.isBlank()) { + if (provisioningContext.length() > PROVISIONING_CONTEXT_LENGTH_LIMIT) { + mProvisioningContext = provisioningContext.substring( + 0, PROVISIONING_CONTEXT_LENGTH_LIMIT); + } else { + mProvisioningContext = provisioningContext; + } + } + } + void writeToXml(TypedXmlSerializer out) throws IllegalArgumentException, IllegalStateException, IOException { if (info != null) { @@ -694,6 +714,12 @@ class ActiveAdmin { if (!TextUtils.isEmpty(mSmsPackage)) { writeAttributeValueToXml(out, TAG_SMS_PACKAGE, mSmsPackage); } + + if (Flags.provisioningContextParameter() && !TextUtils.isEmpty(mProvisioningContext)) { + out.startTag(null, TAG_PROVISIONING_CONTEXT); + out.attribute(null, ATTR_VALUE, mProvisioningContext); + out.endTag(null, TAG_PROVISIONING_CONTEXT); + } } private void writePackagePolicy(TypedXmlSerializer out, String tag, @@ -1006,6 +1032,9 @@ class ActiveAdmin { mDialerPackage = parser.getAttributeValue(null, ATTR_VALUE); } else if (TAG_SMS_PACKAGE.equals(tag)) { mSmsPackage = parser.getAttributeValue(null, ATTR_VALUE); + } else if (Flags.provisioningContextParameter() + && TAG_PROVISIONING_CONTEXT.equals(tag)) { + mProvisioningContext = parser.getAttributeValue(null, ATTR_VALUE); } else { Slogf.w(LOG_TAG, "Unknown admin tag: %s", tag); XmlUtils.skipCurrentTag(parser); @@ -1496,5 +1525,10 @@ class ActiveAdmin { pw.println(mDialerPackage); pw.print("mSmsPackage="); pw.println(mSmsPackage); + + if (Flags.provisioningContextParameter()) { + pw.print("mProvisioningContext="); + pw.println(mProvisioningContext); + } } } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index 032d6b56af1b..8cc7383b9bf6 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -3943,10 +3943,16 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { /** * @param adminReceiver The admin to add * @param refreshing true = update an active admin, no error + * @param userHandle which user this admin will be set on + * @param provisioningContext additional information for debugging */ @Override public void setActiveAdmin( - ComponentName adminReceiver, boolean refreshing, int userHandle) { + ComponentName adminReceiver, + boolean refreshing, + int userHandle, + @Nullable String provisioningContext + ) { if (!mHasFeature) { return; } @@ -3972,6 +3978,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { newAdmin.testOnlyAdmin = (existingAdmin != null) ? existingAdmin.testOnlyAdmin : isPackageTestOnly(adminReceiver.getPackageName(), userHandle); + newAdmin.setProvisioningContext(provisioningContext); policy.mAdminMap.put(adminReceiver, newAdmin); int replaceIndex = -1; final int N = policy.mAdminList.size(); @@ -12830,7 +12837,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { }); // Set admin. - setActiveAdmin(profileOwner, /* refreshing= */ true, userId); + setActiveAdmin(profileOwner, /* refreshing= */ true, userId, null); setProfileOwner(profileOwner, userId); synchronized (getLockObject()) { @@ -21883,7 +21890,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { @UserIdInt int userId, @UserIdInt int callingUserId, ComponentName adminComponent) { final String adminPackage = adminComponent.getPackageName(); enablePackage(adminPackage, callingUserId); - setActiveAdmin(adminComponent, /* refreshing= */ true, userId); + setActiveAdmin(adminComponent, /* refreshing= */ true, userId, null); } private void enablePackage(String packageName, @UserIdInt int userId) { diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerServiceShellCommand.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerServiceShellCommand.java index eb893fcfee1f..0cd5b47b75c0 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerServiceShellCommand.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerServiceShellCommand.java @@ -17,6 +17,7 @@ package com.android.server.devicepolicy; import android.app.ActivityManager; import android.app.admin.DevicePolicyManager; +import android.app.admin.flags.Flags; import android.content.ComponentName; import android.os.ShellCommand; import android.os.SystemClock; @@ -46,11 +47,13 @@ final class DevicePolicyManagerServiceShellCommand extends ShellCommand { private static final String USER_OPTION = "--user"; private static final String DO_ONLY_OPTION = "--device-owner-only"; + private static final String PROVISIONING_CONTEXT_OPTION = "--provisioning-context"; private final DevicePolicyManagerService mService; private int mUserId = UserHandle.USER_SYSTEM; private ComponentName mComponent; private boolean mSetDoOnly; + private String mProvisioningContext = null; DevicePolicyManagerServiceShellCommand(DevicePolicyManagerService service) { mService = Objects.requireNonNull(service); @@ -127,15 +130,28 @@ final class DevicePolicyManagerServiceShellCommand extends ShellCommand { pw.printf(" Lists the device / profile owners per user \n\n"); pw.printf(" %s\n", CMD_LIST_POLICY_EXEMPT_APPS); pw.printf(" Lists the apps that are exempt from policies\n\n"); - pw.printf(" %s [ %s <USER_ID> | current ] <COMPONENT>\n", - CMD_SET_ACTIVE_ADMIN, USER_OPTION); - pw.printf(" Sets the given component as active admin for an existing user.\n\n"); - pw.printf(" %s [ %s <USER_ID> | current *EXPERIMENTAL* ] [ %s ]" - + "<COMPONENT>\n", CMD_SET_DEVICE_OWNER, USER_OPTION, DO_ONLY_OPTION); - pw.printf(" Sets the given component as active admin, and its package as device owner." - + "\n\n"); - pw.printf(" %s [ %s <USER_ID> | current ] <COMPONENT>\n", - CMD_SET_PROFILE_OWNER, USER_OPTION); + if (Flags.provisioningContextParameter()) { + pw.printf(" %s [ %s <USER_ID> | current ] [ %s <PROVISIONING_CONTEXT>] <COMPONENT>\n", + CMD_SET_ACTIVE_ADMIN, USER_OPTION, PROVISIONING_CONTEXT_OPTION); + pw.printf(" Sets the given component as active admin for an existing user.\n\n"); + pw.printf(" %s [ %s <USER_ID> | current *EXPERIMENTAL* ] [ %s ]" + + " [ %s <PROVISIONING_CONTEXT>] <COMPONENT>\n", + CMD_SET_DEVICE_OWNER, USER_OPTION, DO_ONLY_OPTION, PROVISIONING_CONTEXT_OPTION); + pw.printf(" Sets the given component as active admin, and its package as device" + + " owner.\n\n"); + pw.printf(" %s [ %s <USER_ID> | current ] [ %s <PROVISIONING_CONTEXT>] <COMPONENT>\n", + CMD_SET_PROFILE_OWNER, USER_OPTION, PROVISIONING_CONTEXT_OPTION); + } else { + pw.printf(" %s [ %s <USER_ID> | current ] <COMPONENT>\n", + CMD_SET_ACTIVE_ADMIN, USER_OPTION); + pw.printf(" Sets the given component as active admin for an existing user.\n\n"); + pw.printf(" %s [ %s <USER_ID> | current *EXPERIMENTAL* ] [ %s ]" + + "<COMPONENT>\n", CMD_SET_DEVICE_OWNER, USER_OPTION, DO_ONLY_OPTION); + pw.printf(" Sets the given component as active admin, and its package as device" + + " owner.\n\n"); + pw.printf(" %s [ %s <USER_ID> | current ] <COMPONENT>\n", + CMD_SET_PROFILE_OWNER, USER_OPTION); + } pw.printf(" Sets the given component as active admin and profile owner for an existing " + "user.\n\n"); pw.printf(" %s [ %s <USER_ID> | current ] <COMPONENT>\n", @@ -243,7 +259,7 @@ final class DevicePolicyManagerServiceShellCommand extends ShellCommand { private int runSetActiveAdmin(PrintWriter pw) { parseArgs(); - mService.setActiveAdmin(mComponent, /* refreshing= */ true, mUserId); + mService.setActiveAdmin(mComponent, /* refreshing= */ true, mUserId, mProvisioningContext); pw.printf("Success: Active admin set to component %s\n", mComponent.flattenToShortString()); return 0; @@ -253,7 +269,12 @@ final class DevicePolicyManagerServiceShellCommand extends ShellCommand { parseArgs(); boolean isAdminAdded = false; try { - mService.setActiveAdmin(mComponent, /* refreshing= */ false, mUserId); + mService.setActiveAdmin( + mComponent, + /* refreshing= */ false, + mUserId, + mProvisioningContext + ); isAdminAdded = true; } catch (IllegalArgumentException e) { pw.printf("%s was already an admin for user %d. No need to set it again.\n", @@ -291,7 +312,7 @@ final class DevicePolicyManagerServiceShellCommand extends ShellCommand { private int runSetProfileOwner(PrintWriter pw) { parseArgs(); - mService.setActiveAdmin(mComponent, /* refreshing= */ true, mUserId); + mService.setActiveAdmin(mComponent, /* refreshing= */ true, mUserId, mProvisioningContext); try { if (!mService.setProfileOwner(mComponent, mUserId)) { @@ -363,6 +384,8 @@ final class DevicePolicyManagerServiceShellCommand extends ShellCommand { } } else if (DO_ONLY_OPTION.equals(opt)) { mSetDoOnly = true; + } else if (PROVISIONING_CONTEXT_OPTION.equals(opt)) { + mProvisioningContext = getNextArgRequired(); } else { throw new IllegalArgumentException("Unknown option: " + opt); } diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/AdditionalSubtypeUtilsTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/AdditionalSubtypeUtilsTest.java index 63224bb2aa3f..c54ff5fb2f69 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/AdditionalSubtypeUtilsTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/AdditionalSubtypeUtilsTest.java @@ -25,7 +25,7 @@ import android.util.AtomicFile; import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodSubtype; -import androidx.test.InstrumentationRegistry; +import androidx.test.platform.app.InstrumentationRegistry; import org.junit.Test; @@ -54,7 +54,8 @@ public final class AdditionalSubtypeUtilsTest { // Save & load. AtomicFile atomicFile = new AtomicFile( - new File(InstrumentationRegistry.getContext().getCacheDir(), "subtypes.xml")); + new File(InstrumentationRegistry.getInstrumentation().getContext().getCacheDir(), + "subtypes.xml")); AdditionalSubtypeUtils.saveToFile(AdditionalSubtypeMap.of(allSubtypes), InputMethodMap.of(methodMap), atomicFile); AdditionalSubtypeMap loadedSubtypes = AdditionalSubtypeUtils.loadFromFile(atomicFile); diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java index c2a069d17446..ec9bfa7200c6 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java @@ -59,6 +59,7 @@ import android.window.ImeOnBackInvokedDispatcher; import androidx.test.platform.app.InstrumentationRegistry; import com.android.internal.compat.IPlatformCompat; +import com.android.internal.inputmethod.DirectBootAwareness; import com.android.internal.inputmethod.IInputMethod; import com.android.internal.inputmethod.IInputMethodClient; import com.android.internal.inputmethod.IInputMethodSession; @@ -161,6 +162,7 @@ public class InputMethodManagerServiceTestBase { .spyStatic(InputMethodUtils.class) .mockStatic(ServiceManager.class) .spyStatic(AdditionalSubtypeMapRepository.class) + .spyStatic(AdditionalSubtypeUtils.class) .startMocking(); mContext = spy(InstrumentationRegistry.getInstrumentation().getContext()); @@ -235,6 +237,7 @@ public class InputMethodManagerServiceTestBase { // The background writer thread in AdditionalSubtypeMapRepository should be stubbed out. doNothing().when(AdditionalSubtypeMapRepository::startWriterThread); + doReturn(AdditionalSubtypeMap.EMPTY_MAP).when(() -> AdditionalSubtypeUtils.load(anyInt())); mServiceThread = new ServiceThread( @@ -267,6 +270,17 @@ public class InputMethodManagerServiceTestBase { LocalServices.removeServiceForTest(InputMethodManagerInternal.class); lifecycle.onStart(); + // Certain tests rely on TEST_IME_ID that is installed with AndroidTest.xml. + // TODO(b/352615651): Consider just synthesizing test InputMethodInfo then injecting it. + AdditionalSubtypeMapRepository.ensureInitializedAndGet(mCallingUserId); + final var settings = InputMethodManagerService.queryInputMethodServicesInternal(mContext, + mCallingUserId, AdditionalSubtypeMapRepository.get(mCallingUserId), + DirectBootAwareness.AUTO); + InputMethodSettingsRepository.put(mCallingUserId, settings); + + // Emulate that the user initialization is done. + mInputMethodManagerService.getUserData(mCallingUserId).mBackgroundLoadLatch.countDown(); + // After this boot phase, services can broadcast Intents. lifecycle.onBootPhase(SystemService.PHASE_ACTIVITY_MANAGER_READY); @@ -277,6 +291,8 @@ public class InputMethodManagerServiceTestBase { @After public void tearDown() { + InputMethodSettingsRepository.remove(mCallingUserId); + if (mInputMethodManagerService != null) { mInputMethodManagerService.mInputMethodDeviceConfigs.destroy(); } diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodUtilsTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodUtilsTest.java index 2857619c70d3..3cf895ee7204 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodUtilsTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodUtilsTest.java @@ -40,7 +40,7 @@ import android.view.inputmethod.InputMethodSubtype; import android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder; import androidx.annotation.NonNull; -import androidx.test.InstrumentationRegistry; +import androidx.test.platform.app.InstrumentationRegistry; import com.android.internal.inputmethod.StartInputFlags; diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java index 624c8971d36f..c6aea5a290e9 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java @@ -2137,6 +2137,14 @@ public final class DisplayPowerControllerTest { private void setUpDisplay(int displayId, String uniqueId, LogicalDisplay logicalDisplayMock, DisplayDevice displayDeviceMock, DisplayDeviceConfig displayDeviceConfigMock, boolean isEnabled) { + + setUpDisplay(displayId, uniqueId, logicalDisplayMock, displayDeviceMock, + displayDeviceConfigMock, isEnabled, "display_name"); + } + + private void setUpDisplay(int displayId, String uniqueId, LogicalDisplay logicalDisplayMock, + DisplayDevice displayDeviceMock, DisplayDeviceConfig displayDeviceConfigMock, + boolean isEnabled, String displayName) { DisplayInfo info = new DisplayInfo(); DisplayDeviceInfo deviceInfo = new DisplayDeviceInfo(); deviceInfo.uniqueId = uniqueId; @@ -2148,6 +2156,7 @@ public final class DisplayPowerControllerTest { when(logicalDisplayMock.isInTransitionLocked()).thenReturn(false); when(displayDeviceMock.getDisplayDeviceInfoLocked()).thenReturn(deviceInfo); when(displayDeviceMock.getUniqueId()).thenReturn(uniqueId); + when(displayDeviceMock.getNameLocked()).thenReturn(displayName); when(displayDeviceMock.getDisplayDeviceConfig()).thenReturn(displayDeviceConfigMock); when(displayDeviceConfigMock.getProximitySensor()).thenReturn( new SensorData(Sensor.STRING_TYPE_PROXIMITY, null)); diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/BrightnessEventTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/BrightnessEventTest.java index 397d77c52f68..26f6e91d29c8 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/brightness/BrightnessEventTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/BrightnessEventTest.java @@ -43,10 +43,14 @@ public final class BrightnessEventTest { mBrightnessEvent = new BrightnessEvent(1); mBrightnessEvent.setReason( getReason(BrightnessReason.REASON_DOZE, BrightnessReason.MODIFIER_LOW_POWER)); - mBrightnessEvent.setPhysicalDisplayId("test"); + mBrightnessEvent.setPhysicalDisplayId("987654321"); + mBrightnessEvent.setPhysicalDisplayName("display_name"); mBrightnessEvent.setDisplayState(Display.STATE_ON); mBrightnessEvent.setDisplayPolicy(POLICY_BRIGHT); mBrightnessEvent.setLux(100.0f); + mBrightnessEvent.setPercent(46.5f); + mBrightnessEvent.setNits(893.8f); + mBrightnessEvent.setUnclampedBrightness(0.65f); mBrightnessEvent.setPreThresholdLux(150.0f); mBrightnessEvent.setTime(System.currentTimeMillis()); mBrightnessEvent.setInitialBrightness(25.0f); @@ -77,12 +81,13 @@ public final class BrightnessEventTest { public void testToStringWorksAsExpected() { String actualString = mBrightnessEvent.toString(false); String expectedString = - "BrightnessEvent: disp=1, physDisp=test, displayState=ON, displayPolicy=BRIGHT," - + " brt=0.6, initBrt=25.0, rcmdBrt=0.6, preBrt=NaN, lux=100.0, preLux=150.0," - + " hbmMax=0.62, hbmMode=off, rbcStrength=-1, thrmMax=0.65, powerFactor=0.2," - + " wasShortTermModelActive=true, flags=, reason=doze [ low_pwr ]," - + " autoBrightness=true, strategy=" + DISPLAY_BRIGHTNESS_STRATEGY_NAME - + ", autoBrightnessMode=idle"; + "BrightnessEvent: brt=0.6 (46.5%), nits= 893.8, lux=100.0, reason=doze [ " + + "low_pwr ], strat=strategy_name, state=ON, policy=BRIGHT, flags=, " + + "initBrt=25.0, rcmdBrt=0.6, preBrt=NaN, preLux=150.0, " + + "wasShortTermModelActive=true, autoBrightness=true (idle), " + + "unclampedBrt=0.65, hbmMax=0.62, hbmMode=off, thrmMax=0.65, " + + "rbcStrength=-1, powerFactor=0.2, physDisp=display_name(987654321), " + + "logicalId=1"; assertEquals(expectedString, actualString); } diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java index e982153acbd1..e04716ed80f6 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java @@ -24,6 +24,7 @@ import static org.junit.Assert.assertFalse; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -41,8 +42,8 @@ import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; import com.android.server.display.DisplayBrightnessState; -import com.android.server.display.DisplayDeviceConfig; import com.android.server.display.brightness.BrightnessReason; +import com.android.server.display.brightness.clamper.BrightnessClamperController.ModifiersAggregatedState; import com.android.server.display.config.SensorData; import com.android.server.display.feature.DeviceConfigParameterProvider; import com.android.server.display.feature.DisplayManagerFlags; @@ -89,6 +90,10 @@ public class BrightnessClamperControllerTest { @Mock private BrightnessModifier mMockModifier; @Mock + private TestStatefulModifier mMockStatefulModifier; + @Mock + private TestDisplayListenerModifier mMockDisplayListenerModifier; + @Mock private DisplayManagerInternal.DisplayPowerRequest mMockRequest; @Mock @@ -99,7 +104,8 @@ public class BrightnessClamperControllerTest { @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); - mTestInjector = new TestInjector(List.of(mMockClamper), List.of(mMockModifier)); + mTestInjector = new TestInjector(List.of(mMockClamper), + List.of(mMockModifier, mMockStatefulModifier, mMockDisplayListenerModifier)); when(mMockDisplayDeviceData.getDisplayId()).thenReturn(DISPLAY_ID); when(mMockDisplayDeviceData.getAmbientLightSensor()).thenReturn(mMockSensorData); @@ -168,6 +174,13 @@ public class BrightnessClamperControllerTest { } @Test + public void testOnDisplayChanged_DelegatesToDisplayListeners() { + mClamperController.onDisplayChanged(mMockDisplayDeviceData); + + verify(mMockDisplayListenerModifier).onDisplayChanged(mMockDisplayDeviceData); + } + + @Test public void testOnDisplayChanged_doesNotRestartLightSensor() { mClamperController.onDisplayChanged(mMockDisplayDeviceData); @@ -189,6 +202,8 @@ public class BrightnessClamperControllerTest { mClamperController.clamp(mMockRequest, initialBrightness, initialSlowChange, STATE_ON); verify(mMockModifier).apply(eq(mMockRequest), any()); + verify(mMockDisplayListenerModifier).apply(eq(mMockRequest), any()); + verify(mMockStatefulModifier).apply(eq(mMockRequest), any()); } @Test @@ -326,11 +341,40 @@ public class BrightnessClamperControllerTest { verify(mMockClamper).stop(); } + @Test + public void test_doesNotNotifyExternalListener_aggregatedStateNotChanged() { + mTestInjector.mCapturedChangeListener.onChanged(); + mTestHandler.flush(); + + verify(mMockExternalListener, never()).onChanged(); + } + + @Test + public void test_notifiesExternalListener_aggregatedStateChanged() { + doAnswer((invocation) -> { + ModifiersAggregatedState argument = invocation.getArgument(0); + argument.mHdrHbmEnabled = true; + return null; + }).when(mMockStatefulModifier).applyStateChange(any()); + mTestInjector.mCapturedChangeListener.onChanged(); + mTestHandler.flush(); + + verify(mMockExternalListener).onChanged(); + } + private BrightnessClamperController createBrightnessClamperController() { return new BrightnessClamperController(mTestInjector, mTestHandler, mMockExternalListener, mMockDisplayDeviceData, mMockContext, mFlags, mSensorManager); } + interface TestDisplayListenerModifier extends BrightnessStateModifier, + BrightnessClamperController.DisplayDeviceDataListener { + } + + interface TestStatefulModifier extends BrightnessStateModifier, + BrightnessClamperController.StatefulModifier { + } + private class TestInjector extends BrightnessClamperController.Injector { private final List<BrightnessClamper<? super BrightnessClamperController.DisplayDeviceData>> @@ -366,7 +410,7 @@ public class BrightnessClamperControllerTest { @Override List<BrightnessStateModifier> getModifiers(DisplayManagerFlags flags, Context context, Handler handler, BrightnessClamperController.ClamperChangeListener listener, - DisplayDeviceConfig displayDeviceConfig) { + BrightnessClamperController.DisplayDeviceData displayDeviceData) { return mModifiers; } diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/ClamperTestUtils.kt b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/ClamperTestUtils.kt new file mode 100644 index 000000000000..5fd848f6adcc --- /dev/null +++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/ClamperTestUtils.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.display.brightness.clamper + +import android.os.IBinder +import android.view.Display +import com.android.server.display.DisplayDeviceConfig +import com.android.server.display.brightness.clamper.BrightnessClamperController.DisplayDeviceData + +fun createDisplayDeviceData( + displayDeviceConfig: DisplayDeviceConfig, + displayToken: IBinder, + uniqueDisplayId: String = "displayId", + thermalThrottlingDataId: String = "thermalId", + powerThrottlingDataId: String = "powerId", + width: Int = 100, + height: Int = 100, + displayId: Int = Display.DEFAULT_DISPLAY +): DisplayDeviceData { + return DisplayDeviceData( + uniqueDisplayId, + thermalThrottlingDataId, + powerThrottlingDataId, + displayDeviceConfig, + width, + height, + displayToken, + displayId + ) +}
\ No newline at end of file diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrBrightnessModifierTest.kt b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrBrightnessModifierTest.kt new file mode 100644 index 000000000000..e9ec8112c399 --- /dev/null +++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrBrightnessModifierTest.kt @@ -0,0 +1,252 @@ +/* + * 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.display.brightness.clamper + +import android.hardware.display.DisplayManagerInternal +import android.os.IBinder +import android.util.Spline +import android.view.SurfaceControlHdrLayerInfoListener +import androidx.test.filters.SmallTest +import com.android.server.display.DisplayBrightnessState +import com.android.server.display.DisplayDeviceConfig +import com.android.server.display.brightness.clamper.BrightnessClamperController.ClamperChangeListener +import com.android.server.display.brightness.clamper.BrightnessClamperController.ModifiersAggregatedState +import com.android.server.display.brightness.clamper.HdrBrightnessModifier.DEFAULT_MAX_HDR_SDR_RATIO +import com.android.server.display.brightness.clamper.HdrBrightnessModifier.Injector +import com.android.server.display.config.createHdrBrightnessData +import com.android.server.testutils.TestHandler +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@SmallTest +class HdrBrightnessModifierTest { + + private val testHandler = TestHandler(null) + private val testInjector = TestInjector() + private val mockChangeListener = mock<ClamperChangeListener>() + private val mockDisplayDeviceConfig = mock<DisplayDeviceConfig>() + private val mockDisplayBinder = mock<IBinder>() + private val mockDisplayBinderOther = mock<IBinder>() + private val mockSpline = mock<Spline>() + private val mockRequest = mock<DisplayManagerInternal.DisplayPowerRequest>() + + private lateinit var modifier: HdrBrightnessModifier + private val dummyData = createDisplayDeviceData(mockDisplayDeviceConfig, mockDisplayBinder) + private val dummyHdrData = createHdrBrightnessData() + + @Test + fun `change listener is not called on init`() { + initHdrModifier() + + verify(mockChangeListener, never()).onChanged() + } + + @Test + fun `hdr listener registered on init if hdr data is present`() { + initHdrModifier() + + assertThat(testInjector.registeredHdrListener).isNotNull() + assertThat(testInjector.registeredToken).isEqualTo(mockDisplayBinder) + } + + @Test + fun `hdr listener not registered on init if hdr data is missing`() { + whenever(mockDisplayDeviceConfig.hdrBrightnessData).thenReturn(null) + modifier = HdrBrightnessModifier(testHandler, mockChangeListener, testInjector, dummyData) + + testHandler.flush() + + assertThat(testInjector.registeredHdrListener).isNull() + assertThat(testInjector.registeredToken).isNull() + } + + @Test + fun `unsubscribes hdr listener when display changed with no hdr data`() { + initHdrModifier() + + whenever(mockDisplayDeviceConfig.hdrBrightnessData).thenReturn(null) + modifier.onDisplayChanged(dummyData) + testHandler.flush() + + assertThat(testInjector.registeredHdrListener).isNull() + assertThat(testInjector.registeredToken).isNull() + verify(mockChangeListener, never()).onChanged() + } + + @Test + fun `resubscribes hdr listener when display changed with different token`() { + initHdrModifier() + + modifier.onDisplayChanged( + createDisplayDeviceData(mockDisplayDeviceConfig, mockDisplayBinderOther)) + testHandler.flush() + + assertThat(testInjector.registeredHdrListener).isNotNull() + assertThat(testInjector.registeredToken).isEqualTo(mockDisplayBinderOther) + verify(mockChangeListener, never()).onChanged() + } + + @Test + fun `test NO_HDR mode`() { + initHdrModifier() + + whenever(mockDisplayDeviceConfig.hdrBrightnessData).thenReturn(createHdrBrightnessData( + minimumHdrPercentOfScreenForNbm = 0.5f, + minimumHdrPercentOfScreenForHbm = 0.7f, + sdrToHdrRatioSpline = mockSpline + )) + // screen size = 10_000 + modifier.onDisplayChanged(createDisplayDeviceData( + mockDisplayDeviceConfig, mockDisplayBinder, + width = 100, + height = 100 + )) + testHandler.flush() + // hdr size = 900 + val desiredMaxHdrRatio = 8f + val hdrWidth = 30 + val hdrHeight = 30 + testInjector.registeredHdrListener!!.onHdrInfoChanged( + mockDisplayBinder, 1, hdrWidth, hdrHeight, 0, desiredMaxHdrRatio + ) + testHandler.flush() + + val modifierState = ModifiersAggregatedState() + modifier.applyStateChange(modifierState) + + assertThat(modifierState.mHdrHbmEnabled).isFalse() + assertThat(modifierState.mMaxDesiredHdrRatio).isEqualTo(DEFAULT_MAX_HDR_SDR_RATIO) + assertThat(modifierState.mSdrHdrRatioSpline).isNull() + + val stateBuilder = DisplayBrightnessState.builder() + modifier.apply(mockRequest, stateBuilder) + + verify(mockDisplayDeviceConfig, never()).getHdrBrightnessFromSdr(any(), any(), any()) + assertThat(stateBuilder.hdrBrightness).isEqualTo(DisplayBrightnessState.BRIGHTNESS_NOT_SET) + } + + @Test + fun `test NBM_HDR mode`() { + initHdrModifier() + whenever(mockDisplayDeviceConfig.hdrBrightnessData).thenReturn(createHdrBrightnessData( + minimumHdrPercentOfScreenForNbm = 0.5f, + minimumHdrPercentOfScreenForHbm = 0.7f, + sdrToHdrRatioSpline = mockSpline + )) + // screen size = 10_000 + modifier.onDisplayChanged(createDisplayDeviceData( + mockDisplayDeviceConfig, mockDisplayBinder, + width = 100, + height = 100 + )) + testHandler.flush() + // hdr size = 5_100 + val desiredMaxHdrRatio = 8f + val hdrWidth = 100 + val hdrHeight = 51 + testInjector.registeredHdrListener!!.onHdrInfoChanged( + mockDisplayBinder, 1, hdrWidth, hdrHeight, 0, desiredMaxHdrRatio + ) + testHandler.flush() + + val modifierState = ModifiersAggregatedState() + modifier.applyStateChange(modifierState) + + assertThat(modifierState.mHdrHbmEnabled).isFalse() + assertThat(modifierState.mMaxDesiredHdrRatio).isEqualTo(desiredMaxHdrRatio) + assertThat(modifierState.mSdrHdrRatioSpline).isEqualTo(mockSpline) + + val expectedHdrBrightness = 0.85f + whenever(mockDisplayDeviceConfig.getHdrBrightnessFromSdr( + 0f, desiredMaxHdrRatio, mockSpline)).thenReturn(expectedHdrBrightness) + val stateBuilder = DisplayBrightnessState.builder() + modifier.apply(mockRequest, stateBuilder) + + assertThat(stateBuilder.hdrBrightness).isEqualTo(expectedHdrBrightness) + } + + @Test + fun `test HBM_HDR mode`() { + initHdrModifier() + whenever(mockDisplayDeviceConfig.hdrBrightnessData).thenReturn(createHdrBrightnessData( + minimumHdrPercentOfScreenForNbm = 0.5f, + minimumHdrPercentOfScreenForHbm = 0.7f, + sdrToHdrRatioSpline = mockSpline + )) + // screen size = 10_000 + modifier.onDisplayChanged(createDisplayDeviceData( + mockDisplayDeviceConfig, mockDisplayBinder, + width = 100, + height = 100 + )) + testHandler.flush() + // hdr size = 7_100 + val desiredMaxHdrRatio = 8f + val hdrWidth = 100 + val hdrHeight = 71 + testInjector.registeredHdrListener!!.onHdrInfoChanged( + mockDisplayBinder, 1, hdrWidth, hdrHeight, 0, desiredMaxHdrRatio + ) + testHandler.flush() + + val modifierState = ModifiersAggregatedState() + modifier.applyStateChange(modifierState) + + assertThat(modifierState.mHdrHbmEnabled).isTrue() + assertThat(modifierState.mMaxDesiredHdrRatio).isEqualTo(desiredMaxHdrRatio) + assertThat(modifierState.mSdrHdrRatioSpline).isEqualTo(mockSpline) + + val expectedHdrBrightness = 0.83f + whenever(mockDisplayDeviceConfig.getHdrBrightnessFromSdr( + 0f, desiredMaxHdrRatio, mockSpline)).thenReturn(expectedHdrBrightness) + val stateBuilder = DisplayBrightnessState.builder() + modifier.apply(mockRequest, stateBuilder) + + assertThat(stateBuilder.hdrBrightness).isEqualTo(expectedHdrBrightness) + } + + private fun initHdrModifier() { + whenever(mockDisplayDeviceConfig.hdrBrightnessData).thenReturn(dummyHdrData) + modifier = HdrBrightnessModifier(testHandler, mockChangeListener, testInjector, dummyData) + testHandler.flush() + } + + + internal class TestInjector : Injector() { + var registeredHdrListener: SurfaceControlHdrLayerInfoListener? = null + var registeredToken: IBinder? = null + + override fun registerHdrListener( + listener: SurfaceControlHdrLayerInfoListener, token: IBinder + ) { + registeredHdrListener = listener + registeredToken = token + } + + override fun unregisterHdrListener( + listener: SurfaceControlHdrLayerInfoListener, token: IBinder + ) { + registeredHdrListener = null + registeredToken = null + } + } +}
\ No newline at end of file diff --git a/services/tests/mockingservicestests/src/com/android/server/alarm/UserWakeupStoreTest.java b/services/tests/mockingservicestests/src/com/android/server/alarm/UserWakeupStoreTest.java index 72883e269a65..5bd919f28e6a 100644 --- a/services/tests/mockingservicestests/src/com/android/server/alarm/UserWakeupStoreTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/alarm/UserWakeupStoreTest.java @@ -23,6 +23,7 @@ import static com.android.server.alarm.UserWakeupStore.USER_START_TIME_DEVIATION import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import static org.testng.AssertJUnit.assertFalse; import android.os.Environment; import android.os.FileUtils; @@ -51,6 +52,7 @@ public class UserWakeupStoreTest { private static final int USER_ID_1 = 10; private static final int USER_ID_2 = 11; private static final int USER_ID_3 = 12; + private static final int USER_ID_SYSTEM = 0; private static final long TEST_TIMESTAMP = 150_000; private static final File TEST_SYSTEM_DIR = new File(InstrumentationRegistry .getInstrumentation().getContext().getDataDir(), "alarmsTestDir"); @@ -110,6 +112,14 @@ public class UserWakeupStoreTest { } @Test + public void testAddWakeupForSystemUser_shouldDoNothing() { + mUserWakeupStore.addUserWakeup(USER_ID_SYSTEM, TEST_TIMESTAMP - 19_000); + assertEquals(0, mUserWakeupStore.getUserIdsToWakeup(TEST_TIMESTAMP).length); + final File file = new File(ROOT_DIR , "usersWithAlarmClocks.xml"); + assertFalse(file.exists()); + } + + @Test public void testAddMultipleWakeupsForUser_ensureOnlyLastWakeupRemains() { final long finalAlarmTime = TEST_TIMESTAMP - 13_000; mUserWakeupStore.addUserWakeup(USER_ID_1, TEST_TIMESTAMP - 29_000); diff --git a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java index 1dbd5320cac6..8656b991b5fc 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java @@ -500,6 +500,13 @@ public class MockingOomAdjusterTests { updateOomAdj(app); assertProcStates(app, PROCESS_STATE_TOP, VISIBLE_APP_ADJ, SCHED_GROUP_TOP_APP); assertEquals("resumed-split-screen-activity", app.mState.getAdjType()); + + doReturn(WindowProcessController.ACTIVITY_STATE_FLAG_IS_VISIBLE + | WindowProcessController.ACTIVITY_STATE_FLAG_PERCEPTIBLE_FREEFORM) + .when(wpc).getActivityStateFlags(); + updateOomAdj(app); + assertProcStates(app, PROCESS_STATE_TOP, VISIBLE_APP_ADJ, SCHED_GROUP_TOP_APP); + assertEquals("perceptible-freeform-activity", app.mState.getAdjType()); } @SuppressWarnings("GuardedBy") diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsAtomTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsAtomTest.java index 62efbc3cfa35..37d8f2f74850 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsAtomTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsAtomTest.java @@ -488,6 +488,8 @@ public class BatteryUsageStatsAtomTest { new BatteryUsageStats.Builder(new String[]{"CustomConsumer1", "CustomConsumer2"}, /* includePowerModels */ true, /* includeProcessStats */ true, + /* includeScreenStateData */ false, + /* includePowerStateData */ false, /* minConsumedPowerThreshold */ 0) .setDischargePercentage(20) .setDischargedPowerRange(1000, 2000) @@ -574,7 +576,7 @@ public class BatteryUsageStatsAtomTest { @Test public void testLargeAtomTruncated() { final BatteryUsageStats.Builder builder = - new BatteryUsageStats.Builder(new String[0], true, false, 0); + new BatteryUsageStats.Builder(new String[0], true, false, false, false, 0); // If not truncated, this BatteryUsageStats object would generate a proto buffer // significantly larger than 50 Kb for (int i = 0; i < 3000; i++) { diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java index 6edfedee9e5b..624b18948c49 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java @@ -397,10 +397,14 @@ public class BatteryUsageStatsRule implements TestRule { & BatteryUsageStatsQuery.FLAG_BATTERY_USAGE_STATS_INCLUDE_POWER_MODELS) != 0; final boolean includeProcessStateData = (query.getFlags() & BatteryUsageStatsQuery.FLAG_BATTERY_USAGE_STATS_INCLUDE_PROCESS_STATE_DATA) != 0; + final boolean includeScreenStateData = (query.getFlags() + & BatteryUsageStatsQuery.FLAG_BATTERY_USAGE_STATS_INCLUDE_SCREEN_STATE) != 0; + final boolean includePowerStateData = (query.getFlags() + & BatteryUsageStatsQuery.FLAG_BATTERY_USAGE_STATS_INCLUDE_POWER_STATE) != 0; final double minConsumedPowerThreshold = query.getMinConsumedPowerThreshold(); BatteryUsageStats.Builder builder = new BatteryUsageStats.Builder( customPowerComponentNames, includePowerModels, includeProcessStateData, - minConsumedPowerThreshold); + includeScreenStateData, includePowerStateData, minConsumedPowerThreshold); SparseArray<? extends BatteryStats.Uid> uidStats = mBatteryStats.getUidStats(); for (int i = 0; i < uidStats.size(); i++) { builder.getOrCreateUidBatteryConsumerBuilder(uidStats.valueAt(i)); diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsTest.java index a3f0770ec8ba..52bb5e839ca2 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsTest.java @@ -31,6 +31,8 @@ import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static java.util.regex.Pattern.quote; + import android.os.AggregateBatteryConsumer; import android.os.BatteryConsumer; import android.os.BatteryUsageStats; @@ -91,7 +93,7 @@ public class BatteryUsageStatsTest { final Parcel parcel = Parcel.obtain(); parcel.writeParcelable(outBatteryUsageStats, 0); - assertThat(parcel.dataSize()).isLessThan(12000); + assertThat(parcel.dataSize()).isLessThan(100000); parcel.setDataPosition(0); @@ -161,15 +163,47 @@ public class BatteryUsageStatsTest { assertThat(dump).contains("Computed drain: 30000"); assertThat(dump).contains("actual drain: 1000-2000"); assertThat(dump).contains("cpu: 20100 apps: 10100 duration: 20s 300ms"); - assertThat(dump).contains("cpu(fg): 2333 apps: 1333 duration: 3s 332ms"); - assertThat(dump).contains("cpu(bg): 2444 apps: 1444 duration: 4s 442ms"); - assertThat(dump).contains("cpu(fgs): 2555 apps: 1555 duration: 5s 552ms"); - assertThat(dump).contains("cpu(cached): 123 apps: 123 duration: 456ms"); assertThat(dump).contains("FOO: 20200 apps: 10200 duration: 20s 400ms"); - assertThat(dump).contains("UID 271: 1200 fg: 1777 bg: 1888 fgs: 1999 cached: 123 " - + "( screen=300 cpu=400 (600ms) cpu:fg=1777 (7s 771ms) cpu:bg=1888 (8s 881ms) " - + "cpu:fgs=1999 (9s 991ms) cpu:cached=123 (456ms) FOO=500 )"); - assertThat(dump).contains("User 42: 30.0 ( cpu=10.0 (30ms) FOO=20.0 )"); + assertThat(dump).containsMatch(quote("(on battery, screen on)") + "\\s*" + + "cpu: 2333 apps: 1333 duration: 3s 332ms"); + assertThat(dump).containsMatch(quote("(not on battery, screen on)") + "\\s*" + + "cpu: 2555 apps: 1555 duration: 5s 552ms"); + assertThat(dump).containsMatch(quote("(on battery, screen off/doze)") + "\\s*" + + "cpu: 2444 apps: 1444 duration: 4s 442ms"); + assertThat(dump).containsMatch(quote("(not on battery, screen off/doze)") + "\\s*" + + "cpu: 123 apps: 123 duration: 456ms"); + assertThat(dump).containsMatch( + "UID 271: 1200 fg: 1777 bg: 1888 fgs: 1999 cached: 123\\s*" + + quote("screen=300 cpu=5787 (27s 99ms) cpu:fg=1777 (7s 771ms) " + + "cpu:bg=1888 (8s 881ms) cpu:fgs=1999 (9s 991ms) " + + "cpu:cached=123 (456ms) FOO=500") + "\\s*" + + quote("(on battery, screen on)") + "\\s*" + + quote("cpu:fg=1777 (7s 771ms)")); + assertThat(dump).containsMatch("User 42: 30.0\\s*" + + quote("cpu=10.0 (30ms) FOO=20.0")); + } + + @Test + public void testDumpNoScreenOrPowerState() { + final BatteryUsageStats stats = buildBatteryUsageStats1(true, false, false).build(); + final StringWriter out = new StringWriter(); + try (PrintWriter pw = new PrintWriter(out)) { + stats.dump(pw, " "); + } + final String dump = out.toString(); + + assertThat(dump).contains("Capacity: 4000"); + assertThat(dump).contains("Computed drain: 30000"); + assertThat(dump).contains("actual drain: 1000-2000"); + assertThat(dump).contains("cpu: 20100 apps: 10100 duration: 20s 300ms"); + assertThat(dump).contains("FOO: 20200 apps: 10200 duration: 20s 400ms"); + assertThat(dump).containsMatch( + "UID 271: 1200 fg: 1777 bg: 1888 fgs: 1999 cached: 123\\s*" + + quote("screen=300 cpu=5787 (600ms) cpu:fg=1777 (7s 771ms) " + + "cpu:bg=1888 (8s 881ms) cpu:fgs=1999 (9s 991ms) " + + "cpu:cached=123 (456ms) FOO=500")); + assertThat(dump).containsMatch("User 42: 30.0\\s*" + + quote("cpu=10.0 (30ms) FOO=20.0")); } @Test @@ -186,9 +220,8 @@ public class BatteryUsageStatsTest { public void testAdd() { final BatteryUsageStats stats1 = buildBatteryUsageStats1(false).build(); final BatteryUsageStats stats2 = buildBatteryUsageStats2(new String[]{"FOO"}, true).build(); - final BatteryUsageStats sum = - new BatteryUsageStats.Builder(new String[]{"FOO"}, true, true, 0) + new BatteryUsageStats.Builder(new String[]{"FOO"}, true, true, true, true, 0) .add(stats1) .add(stats2) .build(); @@ -200,14 +233,14 @@ public class BatteryUsageStatsTest { for (UidBatteryConsumer uidBatteryConsumer : uidBatteryConsumers) { if (uidBatteryConsumer.getUid() == APP_UID1) { assertUidBatteryConsumer(uidBatteryConsumer, 2124, null, - 5321, 6900, 532, 423, BatteryConsumer.POWER_MODEL_POWER_PROFILE, 745, + 5321, 6900, 532, 423, BatteryConsumer.POWER_MODEL_POWER_PROFILE, 11772, POWER_MODEL_UNDEFINED, 956, 1167, 1478, true, 3554, 3776, 3998, 444, 3554, 15542, 3776, 17762, 3998, 19982, 444, 1110); } else if (uidBatteryConsumer.getUid() == APP_UID2) { assertUidBatteryConsumer(uidBatteryConsumer, 1332, "bar", - 1111, 2220, 2, 333, BatteryConsumer.POWER_MODEL_POWER_PROFILE, 444, + 1111, 2220, 2, 333, BatteryConsumer.POWER_MODEL_POWER_PROFILE, 5985, BatteryConsumer.POWER_MODEL_POWER_PROFILE, 555, 666, 777, true, 1777, 1888, 1999, 321, 1777, 7771, 1888, 8881, 1999, 9991, @@ -229,7 +262,7 @@ public class BatteryUsageStatsTest { @Test public void testAdd_customComponentMismatch() { final BatteryUsageStats.Builder builder = - new BatteryUsageStats.Builder(new String[]{"FOO"}, true, true, 0); + new BatteryUsageStats.Builder(new String[]{"FOO"}, true, true, true, true, 0); final BatteryUsageStats stats = buildBatteryUsageStats2(new String[]{"BAR"}, false).build(); assertThrows(IllegalArgumentException.class, () -> builder.add(stats)); @@ -238,7 +271,7 @@ public class BatteryUsageStatsTest { @Test public void testAdd_processStateDataMismatch() { final BatteryUsageStats.Builder builder = - new BatteryUsageStats.Builder(new String[]{"FOO"}, true, true, 0); + new BatteryUsageStats.Builder(new String[]{"FOO"}, true, true, true, true, 0); final BatteryUsageStats stats = buildBatteryUsageStats2(new String[]{"FOO"}, false).build(); assertThrows(IllegalArgumentException.class, () -> builder.add(stats)); @@ -259,15 +292,23 @@ public class BatteryUsageStatsTest { parser.setInput(in, StandardCharsets.UTF_8.name()); final BatteryUsageStats fromXml = BatteryUsageStats.createFromXml(parser); + System.out.println("stats = " + stats); + System.out.println("fromXml = " + fromXml); assertBatteryUsageStats1(fromXml, true); } private BatteryUsageStats.Builder buildBatteryUsageStats1(boolean includeUserBatteryConsumer) { + return buildBatteryUsageStats1(includeUserBatteryConsumer, true, true); + } + + private BatteryUsageStats.Builder buildBatteryUsageStats1(boolean includeUserBatteryConsumer, + boolean includeScreenState, boolean includePowerState) { final MockClock clocks = new MockClock(); final MockBatteryStatsImpl batteryStats = new MockBatteryStatsImpl(clocks); final BatteryUsageStats.Builder builder = - new BatteryUsageStats.Builder(new String[]{"FOO"}, true, true, 0) + new BatteryUsageStats.Builder(new String[]{"FOO"}, true, true, + includeScreenState, includePowerState, 0) .setBatteryCapacity(4000) .setDischargePercentage(20) .setDischargedPowerRange(1000, 2000) @@ -312,7 +353,7 @@ public class BatteryUsageStatsTest { final BatteryUsageStats.Builder builder = new BatteryUsageStats.Builder(customPowerComponentNames, true, - includeProcessStateData, 0); + includeProcessStateData, true, true, 0); builder.setDischargePercentage(30) .setDischargedPowerRange(1234, 2345) .setStatsStartTimestamp(2000) @@ -371,9 +412,15 @@ public class BatteryUsageStatsTest { .setUsageDurationForCustomComponentMillis( BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID, customComponentDuration); if (builder.isProcessStateDataNeeded()) { - final BatteryConsumer.Key cpuFgKey = uidBuilder.getKey( - BatteryConsumer.POWER_COMPONENT_CPU, - BatteryConsumer.PROCESS_STATE_FOREGROUND); + final BatteryConsumer.Key cpuFgKey = builder.isScreenStateDataNeeded() + ? uidBuilder.getKey( + BatteryConsumer.POWER_COMPONENT_CPU, + BatteryConsumer.PROCESS_STATE_FOREGROUND, + BatteryConsumer.SCREEN_STATE_ON, + BatteryConsumer.POWER_STATE_BATTERY) + : uidBuilder.getKey( + BatteryConsumer.POWER_COMPONENT_CPU, + BatteryConsumer.PROCESS_STATE_FOREGROUND); final BatteryConsumer.Key cpuBgKey = uidBuilder.getKey( BatteryConsumer.POWER_COMPONENT_CPU, BatteryConsumer.PROCESS_STATE_BACKGROUND); @@ -401,9 +448,9 @@ public class BatteryUsageStatsTest { private void addAggregateBatteryConsumer(BatteryUsageStats.Builder builder, int scope, double consumedPower, int cpuPower, int customComponentPower, int cpuDuration, - int customComponentDuration, double cpuPowerForeground, long cpuDurationForeground, - double cpuPowerBackground, long cpuDurationBackground, double cpuPowerFgs, - long cpuDurationFgs, double cpuPowerCached, long cpuDurationCached) { + int customComponentDuration, double cpuPowerBatScrOn, long cpuDurationBatScrOn, + double cpuPowerBatScrOff, long cpuDurationBatScrOff, double cpuPowerChgScrOn, + long cpuDurationChgScrOn, double cpuPowerChgScrOff, long cpuDurationChgScrOff) { final AggregateBatteryConsumer.Builder aggBuilder = builder.getAggregateBatteryConsumerBuilder(scope) .setConsumedPower(consumedPower) @@ -417,32 +464,40 @@ public class BatteryUsageStatsTest { .setUsageDurationForCustomComponentMillis( BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID, customComponentDuration); - if (builder.isProcessStateDataNeeded()) { - final BatteryConsumer.Key cpuFgKey = aggBuilder.getKey( + if (builder.isPowerStateDataNeeded() || builder.isScreenStateDataNeeded()) { + final BatteryConsumer.Key cpuBatScrOn = aggBuilder.getKey( BatteryConsumer.POWER_COMPONENT_CPU, - BatteryConsumer.PROCESS_STATE_FOREGROUND); - final BatteryConsumer.Key cpuBgKey = aggBuilder.getKey( + BatteryConsumer.PROCESS_STATE_UNSPECIFIED, + BatteryConsumer.SCREEN_STATE_ON, + BatteryConsumer.POWER_STATE_BATTERY); + final BatteryConsumer.Key cpuBatScrOff = aggBuilder.getKey( BatteryConsumer.POWER_COMPONENT_CPU, - BatteryConsumer.PROCESS_STATE_BACKGROUND); - final BatteryConsumer.Key cpuFgsKey = aggBuilder.getKey( + BatteryConsumer.PROCESS_STATE_UNSPECIFIED, + BatteryConsumer.SCREEN_STATE_OTHER, + BatteryConsumer.POWER_STATE_BATTERY); + final BatteryConsumer.Key cpuChgScrOn = aggBuilder.getKey( BatteryConsumer.POWER_COMPONENT_CPU, - BatteryConsumer.PROCESS_STATE_FOREGROUND_SERVICE); - final BatteryConsumer.Key cpuCachedKey = aggBuilder.getKey( + BatteryConsumer.PROCESS_STATE_UNSPECIFIED, + BatteryConsumer.SCREEN_STATE_ON, + BatteryConsumer.POWER_STATE_OTHER); + final BatteryConsumer.Key cpuChgScrOff = aggBuilder.getKey( BatteryConsumer.POWER_COMPONENT_CPU, - BatteryConsumer.PROCESS_STATE_CACHED); + BatteryConsumer.PROCESS_STATE_UNSPECIFIED, + BatteryConsumer.SCREEN_STATE_OTHER, + BatteryConsumer.POWER_STATE_OTHER); aggBuilder - .setConsumedPower(cpuFgKey, cpuPowerForeground, + .setConsumedPower(cpuBatScrOn, cpuPowerBatScrOn, BatteryConsumer.POWER_MODEL_POWER_PROFILE) - .setUsageDurationMillis(cpuFgKey, cpuDurationForeground) - .setConsumedPower(cpuBgKey, cpuPowerBackground, + .setUsageDurationMillis(cpuBatScrOn, cpuDurationBatScrOn) + .setConsumedPower(cpuBatScrOff, cpuPowerBatScrOff, BatteryConsumer.POWER_MODEL_POWER_PROFILE) - .setUsageDurationMillis(cpuBgKey, cpuDurationBackground) - .setConsumedPower(cpuFgsKey, cpuPowerFgs, + .setUsageDurationMillis(cpuBatScrOff, cpuDurationBatScrOff) + .setConsumedPower(cpuChgScrOn, cpuPowerChgScrOn, BatteryConsumer.POWER_MODEL_POWER_PROFILE) - .setUsageDurationMillis(cpuFgsKey, cpuDurationFgs) - .setConsumedPower(cpuCachedKey, cpuPowerCached, + .setUsageDurationMillis(cpuChgScrOn, cpuDurationChgScrOn) + .setConsumedPower(cpuChgScrOff, cpuPowerChgScrOff, BatteryConsumer.POWER_MODEL_POWER_PROFILE) - .setUsageDurationMillis(cpuCachedKey, cpuDurationCached); + .setUsageDurationMillis(cpuChgScrOff, cpuDurationChgScrOff); } } @@ -456,7 +511,7 @@ public class BatteryUsageStatsTest { for (UidBatteryConsumer uidBatteryConsumer : uidBatteryConsumers) { if (uidBatteryConsumer.getUid() == APP_UID1) { assertUidBatteryConsumer(uidBatteryConsumer, 1200, "foo", - 1000, 1500, 500, 300, BatteryConsumer.POWER_MODEL_POWER_PROFILE, 400, + 1000, 1500, 500, 300, BatteryConsumer.POWER_MODEL_POWER_PROFILE, 5787, BatteryConsumer.POWER_MODEL_POWER_PROFILE, 500, 600, 800, true, 1777, 1888, 1999, 123, 1777, 7771, 1888, 8881, 1999, 9991, 123, 456); @@ -568,54 +623,53 @@ public class BatteryUsageStatsTest { .isEqualTo(totalPowerCached); } - final BatteryConsumer.Key cpuFgKey = uidBatteryConsumer.getKey( + final BatteryConsumer.Dimensions cpuFg = new BatteryConsumer.Dimensions( BatteryConsumer.POWER_COMPONENT_CPU, BatteryConsumer.PROCESS_STATE_FOREGROUND); if (processStateDataIncluded) { - assertThat(cpuFgKey).isNotNull(); - assertThat(uidBatteryConsumer.getConsumedPower(cpuFgKey)) + assertThat(uidBatteryConsumer.getConsumedPower(cpuFg)) .isEqualTo(cpuPowerForeground); - assertThat(uidBatteryConsumer.getUsageDurationMillis(cpuFgKey)) + assertThat(uidBatteryConsumer.getUsageDurationMillis(cpuFg)) .isEqualTo(cpuDurationForeground); } else { - assertThat(cpuFgKey).isNull(); + assertThat(uidBatteryConsumer.getConsumedPower(cpuFg)).isEqualTo(0); } - final BatteryConsumer.Key cpuBgKey = uidBatteryConsumer.getKey( + final BatteryConsumer.Dimensions cpuBg = new BatteryConsumer.Dimensions( BatteryConsumer.POWER_COMPONENT_CPU, BatteryConsumer.PROCESS_STATE_BACKGROUND); if (processStateDataIncluded) { - assertThat(cpuBgKey).isNotNull(); - assertThat(uidBatteryConsumer.getConsumedPower(cpuBgKey)) + assertThat(uidBatteryConsumer.getConsumedPower(cpuBg)) .isEqualTo(cpuPowerBackground); - assertThat(uidBatteryConsumer.getUsageDurationMillis(cpuBgKey)) + assertThat(uidBatteryConsumer.getUsageDurationMillis(cpuBg)) .isEqualTo(cpuDurationBackground); } else { - assertThat(cpuBgKey).isNull(); + assertThat(uidBatteryConsumer.getConsumedPower(cpuBg)) + .isEqualTo(0); } - final BatteryConsumer.Key cpuFgsKey = uidBatteryConsumer.getKey( + final BatteryConsumer.Dimensions cpuFgs = new BatteryConsumer.Dimensions( BatteryConsumer.POWER_COMPONENT_CPU, BatteryConsumer.PROCESS_STATE_FOREGROUND_SERVICE); if (processStateDataIncluded) { - assertThat(cpuFgsKey).isNotNull(); - assertThat(uidBatteryConsumer.getConsumedPower(cpuFgsKey)) + assertThat(uidBatteryConsumer.getConsumedPower(cpuFgs)) .isEqualTo(cpuPowerFgs); - assertThat(uidBatteryConsumer.getUsageDurationMillis(cpuFgsKey)) + assertThat(uidBatteryConsumer.getUsageDurationMillis(cpuFgs)) .isEqualTo(cpuDurationFgs); } else { - assertThat(cpuFgsKey).isNotNull(); + assertThat(uidBatteryConsumer.getConsumedPower(cpuFgs)) + .isEqualTo(0); } - final BatteryConsumer.Key cachedKey = uidBatteryConsumer.getKey( + final BatteryConsumer.Dimensions cached = new BatteryConsumer.Dimensions( BatteryConsumer.POWER_COMPONENT_CPU, BatteryConsumer.PROCESS_STATE_CACHED); if (processStateDataIncluded) { - assertThat(cachedKey).isNotNull(); - assertThat(uidBatteryConsumer.getConsumedPower(cachedKey)) + assertThat(uidBatteryConsumer.getConsumedPower(cached)) .isEqualTo(cpuPowerCached); - assertThat(uidBatteryConsumer.getUsageDurationMillis(cachedKey)) + assertThat(uidBatteryConsumer.getUsageDurationMillis(cached)) .isEqualTo(cpuDurationCached); } else { - assertThat(cpuFgsKey).isNotNull(); + assertThat(uidBatteryConsumer.getConsumedPower(cached)) + .isEqualTo(0); } } diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsExporterTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsExporterTest.java index 32bfb2cd3507..7f7967ba4d7b 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsExporterTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsExporterTest.java @@ -19,7 +19,6 @@ package com.android.server.power.stats; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; -import static org.junit.Assert.assertThrows; import static org.mockito.Mockito.mock; import android.os.AggregateBatteryConsumer; @@ -131,9 +130,20 @@ public class PowerStatsExporterTest { @Test public void breakdownByProcState_fullRange() throws Exception { + breakdownByProcState_fullRange(false, false); + } + + @Test + public void breakdownByProcStateScreenAndPower_fullRange() throws Exception { + breakdownByProcState_fullRange(true, true); + } + + private void breakdownByProcState_fullRange(boolean includeScreenStateData, + boolean includePowerStateData) throws Exception { BatteryUsageStats.Builder builder = new BatteryUsageStats.Builder( new String[]{"cu570m"}, /* includePowerModels */ false, - /* includeProcessStateData */ true, /* powerThreshold */ 0); + /* includeProcessStateData */ true, includeScreenStateData, + includePowerStateData, /* powerThreshold */ 0); exportAggregatedPowerStats(builder, 1000, 10000); BatteryUsageStats actual = builder.build(); @@ -177,7 +187,7 @@ public class PowerStatsExporterTest { public void breakdownByProcState_subRange() throws Exception { BatteryUsageStats.Builder builder = new BatteryUsageStats.Builder( new String[]{"cu570m"}, /* includePowerModels */ false, - /* includeProcessStateData */ true, /* powerThreshold */ 0); + /* includeProcessStateData */ true, true, true, /* powerThreshold */ 0); exportAggregatedPowerStats(builder, 3700, 6700); BatteryUsageStats actual = builder.build(); @@ -209,7 +219,7 @@ public class PowerStatsExporterTest { public void combinedProcessStates() throws Exception { BatteryUsageStats.Builder builder = new BatteryUsageStats.Builder( new String[]{"cu570m"}, /* includePowerModels */ false, - /* includeProcessStateData */ false, /* powerThreshold */ 0); + /* includeProcessStateData */ false, true, true, /* powerThreshold */ 0); exportAggregatedPowerStats(builder, 1000, 10000); BatteryUsageStats actual = builder.build(); @@ -229,13 +239,13 @@ public class PowerStatsExporterTest { UidBatteryConsumer uidScope = actual.getUidBatteryConsumers().stream() .filter(us -> us.getUid() == APP_UID1).findFirst().orElse(null); // There shouldn't be any per-procstate data - assertThrows( - IllegalArgumentException.class, - () -> uidScope.getConsumedPower(new BatteryConsumer.Dimensions( + for (int procState = 0; procState < BatteryConsumer.PROCESS_STATE_COUNT; procState++) { + if (procState != BatteryConsumer.PROCESS_STATE_UNSPECIFIED) { + assertThat(uidScope.getConsumedPower(new BatteryConsumer.Dimensions( BatteryConsumer.POWER_COMPONENT_CPU, - BatteryConsumer.PROCESS_STATE_FOREGROUND))); - - + BatteryConsumer.PROCESS_STATE_FOREGROUND))).isEqualTo(0); + } + } actual.close(); } diff --git a/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java b/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java index 758c84a26dcd..ef9580c54de6 100644 --- a/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java +++ b/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java @@ -101,7 +101,7 @@ public class AbsoluteVolumeBehaviorTest { mAudioService = new AudioService(mContext, mSpyAudioSystem, mSystemServer, mSettingsAdapter, mAudioVolumeGroupHelper, mMockAudioPolicy, mTestLooper.getLooper(), mock(AppOpsManager.class), mock(PermissionEnforcer.class), - mock(AudioServerPermissionProvider.class)) { + mock(AudioServerPermissionProvider.class), r -> r.run()) { @Override public int getDeviceForStream(int stream) { return AudioSystem.DEVICE_OUT_SPEAKER; diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java index 2cb02bdd2806..464515632997 100644 --- a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java @@ -78,7 +78,7 @@ public class AudioDeviceVolumeManagerTest { mAudioService = new AudioService(mContext, mSpyAudioSystem, mSystemServer, mSettingsAdapter, mAudioVolumeGroupHelper, mAudioPolicyMock, mTestLooper.getLooper(), mock(AppOpsManager.class), mock(PermissionEnforcer.class), - mock(AudioServerPermissionProvider.class)) { + mock(AudioServerPermissionProvider.class), r -> r.run()) { @Override public int getDeviceForStream(int stream) { return AudioSystem.DEVICE_OUT_SPEAKER; diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java index 037c3c00443c..b7100ea00a40 100644 --- a/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java @@ -87,7 +87,7 @@ public class AudioServiceTest { .thenReturn(AppOpsManager.MODE_ALLOWED); mAudioService = new AudioService(mContext, mSpyAudioSystem, mSpySystemServer, mSettingsAdapter, mAudioVolumeGroupHelper, mMockAudioPolicy, null, - mMockAppOpsManager, mMockPermissionEnforcer, mMockPermissionProvider); + mMockAppOpsManager, mMockPermissionEnforcer, mMockPermissionProvider, r -> r.run()); } /** diff --git a/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java b/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java index 27b552fa7cdd..746645a8c585 100644 --- a/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java +++ b/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java @@ -78,7 +78,7 @@ public class DeviceVolumeBehaviorTest { mAudioService = new AudioService(mContext, mAudioSystem, mSystemServer, mSettingsAdapter, mAudioVolumeGroupHelper, mAudioPolicyMock, mTestLooper.getLooper(), mock(AppOpsManager.class), mock(PermissionEnforcer.class), - mock(AudioServerPermissionProvider.class)); + mock(AudioServerPermissionProvider.class), r -> r.run()); mTestLooper.dispatchAll(); } diff --git a/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java b/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java index 8e34ee1b6a42..e45ab319146c 100644 --- a/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java +++ b/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java @@ -160,7 +160,7 @@ public class VolumeHelperTest { @NonNull PermissionEnforcer enforcer, AudioServerPermissionProvider permissionProvider) { super(context, audioSystem, systemServer, settings, audioVolumeGroupHelper, - audioPolicy, looper, appOps, enforcer, permissionProvider); + audioPolicy, looper, appOps, enforcer, permissionProvider, r -> r.run()); } public void setDeviceForStream(int stream, int device) { diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/NetworkEventTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/NetworkEventTest.java index 8a9538f2374a..ebdde94237eb 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/NetworkEventTest.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/NetworkEventTest.java @@ -66,7 +66,7 @@ public class NetworkEventTest extends DpmTestBase { any(UserHandle.class)); mDpmTestable = new DevicePolicyManagerServiceTestable(getServices(), mSpiedDpmMockContext); setUpPackageManagerForAdmin(admin1, DpmMockContext.CALLER_UID); - mDpmTestable.setActiveAdmin(admin1, true, DpmMockContext.CALLER_USER_HANDLE); + mDpmTestable.setActiveAdmin(admin1, true, DpmMockContext.CALLER_USER_HANDLE, null); } @Test diff --git a/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java b/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java index 54d11387752c..cbf79356e9c4 100644 --- a/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java +++ b/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java @@ -16,7 +16,6 @@ package com.android.server.webkit; -import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager.NameNotFoundException; @@ -66,10 +65,12 @@ public class TestSystemImpl implements SystemInterface { } @Override - public String getUserChosenWebViewProvider(Context context) { return mUserProvider; } + public String getUserChosenWebViewProvider() { + return mUserProvider; + } @Override - public void updateUserSetting(Context context, String newProviderName) { + public void updateUserSetting(String newProviderName) { mUserProvider = newProviderName; } @@ -77,14 +78,14 @@ public class TestSystemImpl implements SystemInterface { public void killPackageDependents(String packageName) {} @Override - public void enablePackageForAllUsers(Context context, String packageName, boolean enable) { + public void enablePackageForAllUsers(String packageName, boolean enable) { for(int userId : mUsers) { enablePackageForUser(packageName, enable, userId); } } @Override - public void installExistingPackageForAllUsers(Context context, String packageName) { + public void installExistingPackageForAllUsers(String packageName) { for (int userId : mUsers) { installPackageForUser(packageName, userId); } @@ -131,8 +132,7 @@ public class TestSystemImpl implements SystemInterface { } @Override - public List<UserPackage> getPackageInfoForProviderAllUsers( - Context context, WebViewProviderInfo info) { + public List<UserPackage> getPackageInfoForProviderAllUsers(WebViewProviderInfo info) { Map<Integer, PackageInfo> userPackages = mPackages.get(info.packageName); List<UserPackage> ret = new ArrayList(); // Loop over defined users, and find the corresponding package for each user. @@ -185,12 +185,12 @@ public class TestSystemImpl implements SystemInterface { } @Override - public int getMultiProcessSetting(Context context) { + public int getMultiProcessSetting() { return mMultiProcessSetting; } @Override - public void setMultiProcessSetting(Context context, int value) { + public void setMultiProcessSetting(int value) { mMultiProcessSetting = value; } diff --git a/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java b/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java index e181a513b637..06479c84bfc7 100644 --- a/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java @@ -104,10 +104,10 @@ public class WebViewUpdateServiceTest { mTestSystemImpl = Mockito.spy(testing); if (updateServiceV2()) { mWebViewUpdateServiceImpl = - new WebViewUpdateServiceImpl2(null /*Context*/, mTestSystemImpl); + new WebViewUpdateServiceImpl2(mTestSystemImpl); } else { mWebViewUpdateServiceImpl = - new WebViewUpdateServiceImpl(null /*Context*/, mTestSystemImpl); + new WebViewUpdateServiceImpl(mTestSystemImpl); } } @@ -140,7 +140,7 @@ public class WebViewUpdateServiceTest { WebViewProviderInfo[] webviewPackages, int numRelros, String userSetting) { setupWithPackagesAndRelroCount(webviewPackages, numRelros); if (userSetting != null) { - mTestSystemImpl.updateUserSetting(null, userSetting); + mTestSystemImpl.updateUserSetting(userSetting); } // Add (enabled and valid) package infos for each provider setEnabledAndValidPackageInfos(webviewPackages); @@ -313,7 +313,7 @@ public class WebViewUpdateServiceTest { }; setupWithPackagesNonDebuggable(packages); // Start with the setting pointing to the invalid package - mTestSystemImpl.updateUserSetting(null, invalidPackage); + mTestSystemImpl.updateUserSetting(invalidPackage); mTestSystemImpl.setPackageInfo(createPackageInfo(invalidPackage, true /* enabled */, true /* valid */, true /* installed */, new Signature[]{invalidPackageSignature} , 0 /* updateTime */)); @@ -481,7 +481,7 @@ public class WebViewUpdateServiceTest { new WebViewProviderInfo(secondPackage, "", true, false, null)}; setupWithPackages(packages); // Start with the setting pointing to the second package - mTestSystemImpl.updateUserSetting(null, secondPackage); + mTestSystemImpl.updateUserSetting(secondPackage); // Have all packages be enabled, so that we can change provider however we want to setEnabledAndValidPackageInfos(packages); @@ -572,7 +572,7 @@ public class WebViewUpdateServiceTest { // Check that the boot time logic re-enables the fallback package. runWebViewBootPreparationOnMainSync(); Mockito.verify(mTestSystemImpl).enablePackageForAllUsers( - Matchers.anyObject(), Mockito.eq(testPackage), Mockito.eq(true)); + Mockito.eq(testPackage), Mockito.eq(true)); // Fake the message about the enabling having changed the package state, // and check we now use that package. @@ -657,7 +657,7 @@ public class WebViewUpdateServiceTest { null)}; setupWithPackages(packages); // Start with the setting pointing to the secondary package - mTestSystemImpl.updateUserSetting(null, secondaryPackage); + mTestSystemImpl.updateUserSetting(secondaryPackage); int secondaryUserId = 10; int userIdToChangePackageFor = multiUser ? secondaryUserId : TestSystemImpl.PRIMARY_USER_ID; if (multiUser) { @@ -710,7 +710,7 @@ public class WebViewUpdateServiceTest { null)}; setupWithPackages(packages); // Start with the setting pointing to the secondary package - mTestSystemImpl.updateUserSetting(null, secondaryPackage); + mTestSystemImpl.updateUserSetting(secondaryPackage); setEnabledAndValidPackageInfosForUser(TestSystemImpl.PRIMARY_USER_ID, packages); int newUser = 100; mTestSystemImpl.addUser(newUser); @@ -832,14 +832,13 @@ public class WebViewUpdateServiceTest { true /* installed */)); // Set user-chosen package - mTestSystemImpl.updateUserSetting(null, chosenPackage); + mTestSystemImpl.updateUserSetting(chosenPackage); runWebViewBootPreparationOnMainSync(); // Verify that we switch the setting to point to the current package - Mockito.verify(mTestSystemImpl).updateUserSetting( - Mockito.anyObject(), Mockito.eq(nonChosenPackage)); - assertEquals(nonChosenPackage, mTestSystemImpl.getUserChosenWebViewProvider(null)); + Mockito.verify(mTestSystemImpl).updateUserSetting(Mockito.eq(nonChosenPackage)); + assertEquals(nonChosenPackage, mTestSystemImpl.getUserChosenWebViewProvider()); checkPreparationPhasesForPackage(nonChosenPackage, 1); } @@ -976,7 +975,7 @@ public class WebViewUpdateServiceTest { setEnabledAndValidPackageInfos(packages); // Start with the setting pointing to the third package - mTestSystemImpl.updateUserSetting(null, thirdPackage); + mTestSystemImpl.updateUserSetting(thirdPackage); runWebViewBootPreparationOnMainSync(); checkPreparationPhasesForPackage(thirdPackage, 1); @@ -1167,7 +1166,7 @@ public class WebViewUpdateServiceTest { setupWithPackages(webviewPackages); // Start with the setting pointing to the uninstalled package - mTestSystemImpl.updateUserSetting(null, uninstalledPackage); + mTestSystemImpl.updateUserSetting(uninstalledPackage); int secondaryUserId = 5; if (multiUser) { mTestSystemImpl.addUser(secondaryUserId); @@ -1220,7 +1219,7 @@ public class WebViewUpdateServiceTest { setupWithPackages(webviewPackages); // Start with the setting pointing to the uninstalled package - mTestSystemImpl.updateUserSetting(null, uninstalledPackage); + mTestSystemImpl.updateUserSetting(uninstalledPackage); int secondaryUserId = 412; mTestSystemImpl.addUser(secondaryUserId); @@ -1277,7 +1276,7 @@ public class WebViewUpdateServiceTest { setupWithPackages(webviewPackages); // Start with the setting pointing to the uninstalled package - mTestSystemImpl.updateUserSetting(null, uninstalledPackage); + mTestSystemImpl.updateUserSetting(uninstalledPackage); int secondaryUserId = 4; mTestSystemImpl.addUser(secondaryUserId); @@ -1290,7 +1289,7 @@ public class WebViewUpdateServiceTest { 0 /* updateTime */, (testHidden ? true : false) /* hidden */)); // Start with the setting pointing to the uninstalled package - mTestSystemImpl.updateUserSetting(null, uninstalledPackage); + mTestSystemImpl.updateUserSetting(uninstalledPackage); runWebViewBootPreparationOnMainSync(); @@ -1458,7 +1457,7 @@ public class WebViewUpdateServiceTest { runWebViewBootPreparationOnMainSync(); checkPreparationPhasesForPackage(primaryPackage, 1 /* first preparation phase */); - mTestSystemImpl.setMultiProcessSetting(null /* context */, settingValue); + mTestSystemImpl.setMultiProcessSetting(settingValue); assertEquals(expectEnabled, mWebViewUpdateServiceImpl.isMultiProcessEnabled()); } @@ -1492,7 +1491,7 @@ public class WebViewUpdateServiceTest { }; setupWithPackages(packages); // Start with the setting pointing to the invalid package - mTestSystemImpl.updateUserSetting(null, oldSdkPackage.packageName); + mTestSystemImpl.updateUserSetting(oldSdkPackage.packageName); mTestSystemImpl.setPackageInfo(newSdkPackage); mTestSystemImpl.setPackageInfo(currentSdkPackage); @@ -1545,8 +1544,7 @@ public class WebViewUpdateServiceTest { // Check that the boot time logic re-enables the default package. runWebViewBootPreparationOnMainSync(); Mockito.verify(mTestSystemImpl) - .enablePackageForAllUsers( - Matchers.anyObject(), Mockito.eq(testPackage), Mockito.eq(true)); + .enablePackageForAllUsers(Mockito.eq(testPackage), Mockito.eq(true)); } @Test @@ -1570,8 +1568,7 @@ public class WebViewUpdateServiceTest { // Check that the boot time logic tries to install the default package. runWebViewBootPreparationOnMainSync(); Mockito.verify(mTestSystemImpl) - .installExistingPackageForAllUsers( - Matchers.anyObject(), Mockito.eq(testPackage)); + .installExistingPackageForAllUsers(Mockito.eq(testPackage)); } @Test @@ -1598,8 +1595,7 @@ public class WebViewUpdateServiceTest { // Check that we try to re-install the default package. Mockito.verify(mTestSystemImpl) - .installExistingPackageForAllUsers( - Matchers.anyObject(), Mockito.eq(testPackage)); + .installExistingPackageForAllUsers(Mockito.eq(testPackage)); } /** @@ -1632,8 +1628,7 @@ public class WebViewUpdateServiceTest { // Check that we try to re-install the default package for all users. Mockito.verify(mTestSystemImpl) - .installExistingPackageForAllUsers( - Matchers.anyObject(), Mockito.eq(testPackage)); + .installExistingPackageForAllUsers(Mockito.eq(testPackage)); } private void testDefaultPackageChosen(PackageInfo packageInfo) { 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 4bbbc2b28dad..b07940ad8de3 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java @@ -186,6 +186,9 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.xmlpull.v1.XmlPullParserException; +import platform.test.runner.parameterized.ParameterizedAndroidJunit4; +import platform.test.runner.parameterized.Parameters; + import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; @@ -205,9 +208,6 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import platform.test.runner.parameterized.ParameterizedAndroidJunit4; -import platform.test.runner.parameterized.Parameters; - @SmallTest @SuppressLint("GuardedBy") // It's ok for this test to access guarded methods from the service. @RunWith(ParameterizedAndroidJunit4.class) @@ -5022,6 +5022,34 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test + @EnableFlags(FLAG_MODES_API) + public void updateAutomaticZenRule_ruleChangedByUser_doesNotDeactivateRule_forWatch() { + when(mContext.getPackageManager()).thenReturn(mPackageManager); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(true); + assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF); + AutomaticZenRule rule = + new AutomaticZenRule.Builder("rule", CONDITION_ID) + .setConfigurationActivity(new ComponentName(mPkg, "cls")) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .build(); + String ruleId = + mZenModeHelper.addAutomaticZenRule( + mPkg, rule, UPDATE_ORIGIN_APP, "reason", CUSTOM_PKG_UID); + mZenModeHelper.setAutomaticZenRuleState( + ruleId, CONDITION_TRUE, UPDATE_ORIGIN_APP, CUSTOM_PKG_UID); + assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_IMPORTANT_INTERRUPTIONS); + + AutomaticZenRule updateWithDiff = + new AutomaticZenRule.Builder(rule).setTriggerDescription("Whenever").build(); + mZenModeHelper.updateAutomaticZenRule(ruleId, updateWithDiff, UPDATE_ORIGIN_USER, "reason", + CUSTOM_PKG_UID); + + assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_IMPORTANT_INTERRUPTIONS); + assertThat(mZenModeHelper.mConfig.automaticRules.get(ruleId).condition).isEqualTo( + CONDITION_TRUE); + } + + @Test @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) public void updateAutomaticZenRule_ruleDisabledByUser_doesNotReactivateOnReenable() { assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF); diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java index b4505fad1b20..24fc7ee0c392 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -2955,7 +2955,8 @@ public class ActivityRecordTests extends WindowTestsBase { @Test public void testStartingWindowInTaskFragment() { - final ActivityRecord activity1 = new ActivityBuilder(mAtm).setCreateTask(true).build(); + final ActivityRecord activity1 = new ActivityBuilder(mAtm).setCreateTask(true) + .setVisible(false).build(); final WindowState startingWindow = createWindowState( new WindowManager.LayoutParams(TYPE_APPLICATION_STARTING), activity1); activity1.addWindow(startingWindow); @@ -3011,6 +3012,28 @@ public class ActivityRecordTests extends WindowTestsBase { } @Test + public void testStartingWindowInTaskFragmentWithVisibleTask() { + final ActivityRecord activity1 = new ActivityBuilder(mAtm).setCreateTask(true).build(); + final Task task = activity1.getTask(); + final Rect taskBounds = task.getBounds(); + final Rect tfBounds = new Rect(taskBounds.left, taskBounds.top, + taskBounds.left + taskBounds.width() / 2, taskBounds.bottom); + final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm).setParentTask(task) + .setBounds(tfBounds).build(); + + final ActivityRecord activity2 = new ActivityBuilder(mAtm).build(); + final WindowState startingWindow = createWindowState( + new WindowManager.LayoutParams(TYPE_APPLICATION_STARTING), activity1); + taskFragment.addChild(activity2); + activity2.addWindow(startingWindow); + activity2.mStartingData = mock(StartingData.class); + activity2.attachStartingWindow(startingWindow); + + assertNull(activity2.mStartingData.mAssociatedTask); + assertNull(task.mSharedStartingData); + } + + @Test public void testTransitionAnimationBounds() { removeGlobalMinSizeRestriction(); final Task task = new TaskBuilder(mSupervisor) diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRefresherTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRefresherTests.java index a3252f87dc9d..6ad1044d2012 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRefresherTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRefresherTests.java @@ -53,7 +53,7 @@ import org.junit.runner.RunWith; @RunWith(WindowTestRunner.class) public class ActivityRefresherTests extends WindowTestsBase { private Handler mMockHandler; - private LetterboxConfiguration mLetterboxConfiguration; + private AppCompatConfiguration mAppCompatConfiguration; private ActivityRecord mActivity; private ActivityRefresher mActivityRefresher; @@ -69,13 +69,13 @@ public class ActivityRefresherTests extends WindowTestsBase { @Before public void setUp() throws Exception { - mLetterboxConfiguration = mDisplayContent.mWmService.mLetterboxConfiguration; - spyOn(mLetterboxConfiguration); - when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled()) + mAppCompatConfiguration = mDisplayContent.mWmService.mAppCompatConfiguration; + spyOn(mAppCompatConfiguration); + when(mAppCompatConfiguration.isCameraCompatTreatmentEnabled()) .thenReturn(true); - when(mLetterboxConfiguration.isCameraCompatRefreshEnabled()) + when(mAppCompatConfiguration.isCameraCompatRefreshEnabled()) .thenReturn(true); - when(mLetterboxConfiguration.isCameraCompatRefreshCycleThroughStopEnabled()) + when(mAppCompatConfiguration.isCameraCompatRefreshCycleThroughStopEnabled()) .thenReturn(true); mMockHandler = mock(Handler.class); @@ -90,7 +90,7 @@ public class ActivityRefresherTests extends WindowTestsBase { @Test public void testShouldRefreshActivity_refreshDisabled() throws Exception { - when(mLetterboxConfiguration.isCameraCompatRefreshEnabled()) + when(mAppCompatConfiguration.isCameraCompatRefreshEnabled()) .thenReturn(false); configureActivityAndDisplay(); mActivityRefresher.addEvaluator(mEvaluatorTrue); @@ -146,7 +146,7 @@ public class ActivityRefresherTests extends WindowTestsBase { public void testOnActivityConfigurationChanging_cycleThroughStopDisabled() throws Exception { mActivityRefresher.addEvaluator(mEvaluatorTrue); - when(mLetterboxConfiguration.isCameraCompatRefreshCycleThroughStopEnabled()) + when(mAppCompatConfiguration.isCameraCompatRefreshCycleThroughStopEnabled()) .thenReturn(false); configureActivityAndDisplay(); 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 2d94b34c52f1..d8c7fb3594c1 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraOverridesTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraOverridesTest.java @@ -290,7 +290,7 @@ public class AppCompatCameraOverridesTest extends WindowTestsBase { * Runs a test scenario providing a Robot. */ void runTestScenario(@NonNull Consumer<CameraOverridesRobotTest> consumer) { - spyOn(mWm.mLetterboxConfiguration); + spyOn(mWm.mAppCompatConfiguration); final CameraOverridesRobotTest robot = new CameraOverridesRobotTest(mWm, mAtm, mSupervisor); consumer.accept(robot); } 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 974a8e8712d8..0b1bb0f75a09 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java @@ -112,7 +112,7 @@ public class AppCompatCameraPolicyTest extends WindowTestsBase { * Runs a test scenario providing a Robot. */ void runTestScenario(@NonNull Consumer<DisplayRotationPolicyRobotTest> consumer) { - spyOn(mWm.mLetterboxConfiguration); + spyOn(mWm.mAppCompatConfiguration); final DisplayRotationPolicyRobotTest robot = new DisplayRotationPolicyRobotTest(mWm, mAtm, mSupervisor); consumer.accept(robot); diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationPersisterTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationPersisterTest.java index 3fcec963593c..c952e2f682c3 100644 --- a/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationPersisterTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationPersisterTest.java @@ -18,8 +18,8 @@ package com.android.server.wm; import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; -import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT; -import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP; +import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT; +import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP; import android.annotation.NonNull; import android.annotation.Nullable; @@ -44,14 +44,14 @@ import java.util.function.Consumer; import java.util.function.Supplier; /** - * Tests for the {@link LetterboxConfigurationPersister} class. + * Tests for the {@link AppCompatConfigurationPersister} class. * * Build/Install/Run: - * atest WmTests:LetterboxConfigurationPersisterTest + * atest WmTests:AppCompatConfigurationPersisterTest */ @SmallTest @Presubmit -public class LetterboxConfigurationPersisterTest { +public class AppCompatConfigurationPersisterTest { private static final long TIMEOUT = 2000L; // 2 secs @@ -61,7 +61,7 @@ public class LetterboxConfigurationPersisterTest { private static final String LETTERBOX_CONFIGURATION_TEST_FILENAME = "letterbox_config_test"; - private LetterboxConfigurationPersister mLetterboxConfigurationPersister; + private AppCompatConfigurationPersister mAppCompatConfigurationPersister; private Context mContext; private PersisterQueue mPersisterQueue; private QueueState mQueueState; @@ -74,7 +74,7 @@ public class LetterboxConfigurationPersisterTest { mConfigFolder = mContext.getFilesDir(); mPersisterQueue = new PersisterQueue(); mQueueState = new QueueState(); - mLetterboxConfigurationPersister = new LetterboxConfigurationPersister( + mAppCompatConfigurationPersister = new AppCompatConfigurationPersister( () -> mContext.getResources().getInteger( R.integer.config_letterboxDefaultPositionForHorizontalReachability), () -> mContext.getResources().getInteger( @@ -88,12 +88,12 @@ public class LetterboxConfigurationPersisterTest { LETTERBOX_CONFIGURATION_TEST_FILENAME); mQueueListener = queueEmpty -> mQueueState.onItemAdded(); mPersisterQueue.addListener(mQueueListener); - mLetterboxConfigurationPersister.start(); + mAppCompatConfigurationPersister.start(); } @After public void tearDown() throws InterruptedException { - deleteConfiguration(mLetterboxConfigurationPersister, mPersisterQueue); + deleteConfiguration(mAppCompatConfigurationPersister, mPersisterQueue); waitForCompletion(mPersisterQueue); mPersisterQueue.removeListener(mQueueListener); stopPersisterSafe(mPersisterQueue); @@ -102,7 +102,7 @@ public class LetterboxConfigurationPersisterTest { @Test public void test_whenStoreIsCreated_valuesAreDefaults() { final int positionForHorizontalReachability = - mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability( + mAppCompatConfigurationPersister.getLetterboxPositionForHorizontalReachability( false); final int defaultPositionForHorizontalReachability = mContext.getResources().getInteger( @@ -110,7 +110,7 @@ public class LetterboxConfigurationPersisterTest { Assert.assertEquals(defaultPositionForHorizontalReachability, positionForHorizontalReachability); final int positionForVerticalReachability = - mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability(false); + mAppCompatConfigurationPersister.getLetterboxPositionForVerticalReachability(false); final int defaultPositionForVerticalReachability = mContext.getResources().getInteger( R.integer.config_letterboxDefaultPositionForVerticalReachability); @@ -120,16 +120,16 @@ public class LetterboxConfigurationPersisterTest { @Test public void test_whenUpdatedWithNewValues_valuesAreWritten() { - mLetterboxConfigurationPersister.setLetterboxPositionForHorizontalReachability(false, + mAppCompatConfigurationPersister.setLetterboxPositionForHorizontalReachability(false, LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT); - mLetterboxConfigurationPersister.setLetterboxPositionForVerticalReachability(false, + mAppCompatConfigurationPersister.setLetterboxPositionForVerticalReachability(false, LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP); waitForCompletion(mPersisterQueue); final int newPositionForHorizontalReachability = - mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability( + mAppCompatConfigurationPersister.getLetterboxPositionForHorizontalReachability( false); final int newPositionForVerticalReachability = - mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability(false); + mAppCompatConfigurationPersister.getLetterboxPositionForVerticalReachability(false); Assert.assertEquals(LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT, newPositionForHorizontalReachability); Assert.assertEquals(LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP, @@ -139,7 +139,7 @@ public class LetterboxConfigurationPersisterTest { @Test public void test_whenUpdatedWithNewValues_valuesAreReadAfterRestart() { final PersisterQueue firstPersisterQueue = new PersisterQueue(); - final LetterboxConfigurationPersister firstPersister = new LetterboxConfigurationPersister( + final AppCompatConfigurationPersister firstPersister = new AppCompatConfigurationPersister( DEFAULT_REACHABILITY_SUPPLIER_TEST, DEFAULT_REACHABILITY_SUPPLIER_TEST, DEFAULT_REACHABILITY_SUPPLIER_TEST, DEFAULT_REACHABILITY_SUPPLIER_TEST, mContext.getFilesDir(), firstPersisterQueue, mQueueState, @@ -152,7 +152,7 @@ public class LetterboxConfigurationPersisterTest { waitForCompletion(firstPersisterQueue); stopPersisterSafe(firstPersisterQueue); final PersisterQueue secondPersisterQueue = new PersisterQueue(); - final LetterboxConfigurationPersister secondPersister = new LetterboxConfigurationPersister( + final AppCompatConfigurationPersister secondPersister = new AppCompatConfigurationPersister( DEFAULT_REACHABILITY_SUPPLIER_TEST, DEFAULT_REACHABILITY_SUPPLIER_TEST, DEFAULT_REACHABILITY_SUPPLIER_TEST, DEFAULT_REACHABILITY_SUPPLIER_TEST, mContext.getFilesDir(), secondPersisterQueue, mQueueState, @@ -174,7 +174,7 @@ public class LetterboxConfigurationPersisterTest { @Test public void test_whenUpdatedWithNewValuesAndDeleted_valuesAreDefaults() { final PersisterQueue firstPersisterQueue = new PersisterQueue(); - final LetterboxConfigurationPersister firstPersister = new LetterboxConfigurationPersister( + final AppCompatConfigurationPersister firstPersister = new AppCompatConfigurationPersister( DEFAULT_REACHABILITY_SUPPLIER_TEST, DEFAULT_REACHABILITY_SUPPLIER_TEST, DEFAULT_REACHABILITY_SUPPLIER_TEST, DEFAULT_REACHABILITY_SUPPLIER_TEST, mContext.getFilesDir(), firstPersisterQueue, mQueueState, @@ -198,7 +198,7 @@ public class LetterboxConfigurationPersisterTest { stopPersisterSafe(firstPersisterQueue); final PersisterQueue secondPersisterQueue = new PersisterQueue(); - final LetterboxConfigurationPersister secondPersister = new LetterboxConfigurationPersister( + final AppCompatConfigurationPersister secondPersister = new AppCompatConfigurationPersister( DEFAULT_REACHABILITY_SUPPLIER_TEST, DEFAULT_REACHABILITY_SUPPLIER_TEST, DEFAULT_REACHABILITY_SUPPLIER_TEST, DEFAULT_REACHABILITY_SUPPLIER_TEST, mContext.getFilesDir(), secondPersisterQueue, mQueueState, @@ -245,7 +245,7 @@ public class LetterboxConfigurationPersisterTest { return mQueueState.isEmpty(); } - private void deleteConfiguration(LetterboxConfigurationPersister persister, + private void deleteConfiguration(AppCompatConfigurationPersister persister, PersisterQueue persisterQueue) { final AtomicFile fileToDelete = new AtomicFile( new File(mConfigFolder, LETTERBOX_CONFIGURATION_TEST_FILENAME)); diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatLetterboxConfigurationRobot.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationRobot.java index e1da913872d8..cb3cf6bd2a5c 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatLetterboxConfigurationRobot.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationRobot.java @@ -24,46 +24,46 @@ import static org.mockito.Mockito.when; import androidx.annotation.NonNull; /** - * Robot implementation for {@link LetterboxConfiguration}. + * Robot implementation for {@link AppCompatConfiguration}. */ -class AppCompatLetterboxConfigurationRobot { +class AppCompatConfigurationRobot { @NonNull - private final LetterboxConfiguration mLetterboxConfiguration; + private final AppCompatConfiguration mAppCompatConfiguration; - AppCompatLetterboxConfigurationRobot(@NonNull LetterboxConfiguration letterboxConfiguration) { - mLetterboxConfiguration = letterboxConfiguration; - spyOn(mLetterboxConfiguration); + AppCompatConfigurationRobot(@NonNull AppCompatConfiguration appCompatConfiguration) { + mAppCompatConfiguration = appCompatConfiguration; + spyOn(mAppCompatConfiguration); } void enableTranslucentPolicy(boolean enabled) { - when(mLetterboxConfiguration.isTranslucentLetterboxingEnabled()).thenReturn(enabled); + when(mAppCompatConfiguration.isTranslucentLetterboxingEnabled()).thenReturn(enabled); } void enablePolicyForIgnoringRequestedOrientation(boolean enabled) { - doReturn(enabled).when(mLetterboxConfiguration) + doReturn(enabled).when(mAppCompatConfiguration) .isPolicyForIgnoringRequestedOrientationEnabled(); } void enableCameraCompatTreatment(boolean enabled) { - doReturn(enabled).when(mLetterboxConfiguration).isCameraCompatTreatmentEnabled(); + doReturn(enabled).when(mAppCompatConfiguration).isCameraCompatTreatmentEnabled(); } void enableCameraCompatTreatmentAtBuildTime(boolean enabled) { - doReturn(enabled).when(mLetterboxConfiguration) + doReturn(enabled).when(mAppCompatConfiguration) .isCameraCompatTreatmentEnabledAtBuildTime(); } void enableUserAppAspectRatioFullscreen(boolean enabled) { - doReturn(enabled).when(mLetterboxConfiguration).isUserAppAspectRatioFullscreenEnabled(); + doReturn(enabled).when(mAppCompatConfiguration).isUserAppAspectRatioFullscreenEnabled(); } void enableUserAppAspectRatioSettings(boolean enabled) { - doReturn(enabled).when(mLetterboxConfiguration).isUserAppAspectRatioSettingsEnabled(); + doReturn(enabled).when(mAppCompatConfiguration).isUserAppAspectRatioSettingsEnabled(); } void enableCameraCompatSplitScreenAspectRatio(boolean enabled) { - doReturn(enabled).when(mLetterboxConfiguration) + doReturn(enabled).when(mAppCompatConfiguration) .isCameraCompatSplitScreenAspectRatioEnabled(); } diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationTest.java index 79e401c238bd..6efd7de80c55 100644 --- a/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationTest.java @@ -19,12 +19,12 @@ package com.android.server.wm; import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; -import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER; -import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT; -import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT; -import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM; -import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER; -import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP; +import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER; +import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT; +import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT; +import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM; +import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER; +import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP; import static com.android.server.wm.testing.Assert.assertThrows; import static junit.framework.Assert.assertEquals; @@ -52,35 +52,35 @@ import java.util.Arrays; import java.util.function.BiConsumer; /** - * Tests for the {@link LetterboxConfiguration} class. + * Tests for the {@link AppCompatConfiguration} class. * * Build/Install/Run: - * atest WmTests:LetterboxConfigurationTest + * atest WmTests:AppCompatConfigurationTest */ @SmallTest @Presubmit -public class LetterboxConfigurationTest { +public class AppCompatConfigurationTest { private Context mContext; - private LetterboxConfiguration mLetterboxConfiguration; - private LetterboxConfigurationPersister mLetterboxConfigurationPersister; + private AppCompatConfiguration mAppCompatConfiguration; + private AppCompatConfigurationPersister mAppCompatConfigurationPersister; @Before public void setUp() throws Exception { mContext = getInstrumentation().getTargetContext(); - mLetterboxConfigurationPersister = mock(LetterboxConfigurationPersister.class); - mLetterboxConfiguration = new LetterboxConfiguration(mContext, - mLetterboxConfigurationPersister); + mAppCompatConfigurationPersister = mock(AppCompatConfigurationPersister.class); + mAppCompatConfiguration = new AppCompatConfiguration(mContext, + mAppCompatConfigurationPersister); } @Test public void test_whenReadingValues_storeIsInvoked() { for (boolean halfFoldPose : Arrays.asList(false, true)) { - mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability(halfFoldPose); - verify(mLetterboxConfigurationPersister).getLetterboxPositionForHorizontalReachability( + mAppCompatConfiguration.getLetterboxPositionForHorizontalReachability(halfFoldPose); + verify(mAppCompatConfigurationPersister).getLetterboxPositionForHorizontalReachability( halfFoldPose); - mLetterboxConfiguration.getLetterboxPositionForVerticalReachability(halfFoldPose); - verify(mLetterboxConfigurationPersister).getLetterboxPositionForVerticalReachability( + mAppCompatConfiguration.getLetterboxPositionForVerticalReachability(halfFoldPose); + verify(mAppCompatConfigurationPersister).getLetterboxPositionForVerticalReachability( halfFoldPose); } } @@ -88,13 +88,13 @@ public class LetterboxConfigurationTest { @Test public void test_whenSettingValues_updateConfigurationIsInvoked() { for (boolean halfFoldPose : Arrays.asList(false, true)) { - mLetterboxConfiguration.movePositionForHorizontalReachabilityToNextRightStop( + mAppCompatConfiguration.movePositionForHorizontalReachabilityToNextRightStop( halfFoldPose); - verify(mLetterboxConfigurationPersister).setLetterboxPositionForHorizontalReachability( + verify(mAppCompatConfigurationPersister).setLetterboxPositionForHorizontalReachability( eq(halfFoldPose), anyInt()); - mLetterboxConfiguration.movePositionForVerticalReachabilityToNextBottomStop( + mAppCompatConfiguration.movePositionForVerticalReachabilityToNextBottomStop( halfFoldPose); - verify(mLetterboxConfigurationPersister).setLetterboxPositionForVerticalReachability( + verify(mAppCompatConfigurationPersister).setLetterboxPositionForVerticalReachability( eq(halfFoldPose), anyInt()); } } @@ -107,65 +107,65 @@ public class LetterboxConfigurationTest { /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT, /* expectedTime */ 1, /* halfFoldPose */ false, - LetterboxConfiguration::movePositionForHorizontalReachabilityToNextLeftStop); + AppCompatConfiguration::movePositionForHorizontalReachabilityToNextLeftStop); assertForHorizontalMove( /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER, /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT, /* expectedTime */ 1, /* halfFoldPose */ false, - LetterboxConfiguration::movePositionForHorizontalReachabilityToNextRightStop); + AppCompatConfiguration::movePositionForHorizontalReachabilityToNextRightStop); // Starting from left assertForHorizontalMove( /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT, /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT, /* expectedTime */ 2, /* halfFoldPose */ false, - LetterboxConfiguration::movePositionForHorizontalReachabilityToNextLeftStop); + AppCompatConfiguration::movePositionForHorizontalReachabilityToNextLeftStop); assertForHorizontalMove( /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT, /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER, /* expectedTime */ 1, /* halfFoldPose */ false, - LetterboxConfiguration::movePositionForHorizontalReachabilityToNextRightStop); + AppCompatConfiguration::movePositionForHorizontalReachabilityToNextRightStop); // Starting from right assertForHorizontalMove( /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT, /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT, /* expectedTime */ 2, /* halfFoldPose */ false, - LetterboxConfiguration::movePositionForHorizontalReachabilityToNextRightStop); + AppCompatConfiguration::movePositionForHorizontalReachabilityToNextRightStop); assertForHorizontalMove( /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT, /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER, /* expectedTime */ 2, /* halfFoldPose */ false, - LetterboxConfiguration::movePositionForHorizontalReachabilityToNextLeftStop); + AppCompatConfiguration::movePositionForHorizontalReachabilityToNextLeftStop); // Starting from left - book mode assertForHorizontalMove( /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT, /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT, /* expectedTime */ 1, /* halfFoldPose */ true, - LetterboxConfiguration::movePositionForHorizontalReachabilityToNextLeftStop); + AppCompatConfiguration::movePositionForHorizontalReachabilityToNextLeftStop); assertForHorizontalMove( /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT, /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT, /* expectedTime */ 1, /* halfFoldPose */ true, - LetterboxConfiguration::movePositionForHorizontalReachabilityToNextRightStop); + AppCompatConfiguration::movePositionForHorizontalReachabilityToNextRightStop); // Starting from right - book mode assertForHorizontalMove( /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT, /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT, /* expectedTime */ 2, /* halfFoldPose */ true, - LetterboxConfiguration::movePositionForHorizontalReachabilityToNextRightStop); + AppCompatConfiguration::movePositionForHorizontalReachabilityToNextRightStop); assertForHorizontalMove( /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT, /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT, /* expectedTime */ 2, /* halfFoldPose */ true, - LetterboxConfiguration::movePositionForHorizontalReachabilityToNextLeftStop); + AppCompatConfiguration::movePositionForHorizontalReachabilityToNextLeftStop); } @Test @@ -176,85 +176,85 @@ public class LetterboxConfigurationTest { /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM, /* expectedTime */ 1, /* halfFoldPose */ false, - LetterboxConfiguration::movePositionForVerticalReachabilityToNextBottomStop); + AppCompatConfiguration::movePositionForVerticalReachabilityToNextBottomStop); assertForVerticalMove( /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER, /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP, /* expectedTime */ 1, /* halfFoldPose */ false, - LetterboxConfiguration::movePositionForVerticalReachabilityToNextTopStop); + AppCompatConfiguration::movePositionForVerticalReachabilityToNextTopStop); // Starting from top assertForVerticalMove( /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP, /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER, /* expectedTime */ 1, /* halfFoldPose */ false, - LetterboxConfiguration::movePositionForVerticalReachabilityToNextBottomStop); + AppCompatConfiguration::movePositionForVerticalReachabilityToNextBottomStop); assertForVerticalMove( /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP, /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP, /* expectedTime */ 2, /* halfFoldPose */ false, - LetterboxConfiguration::movePositionForVerticalReachabilityToNextTopStop); + AppCompatConfiguration::movePositionForVerticalReachabilityToNextTopStop); // Starting from bottom assertForVerticalMove( /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM, /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER, /* expectedTime */ 2, /* halfFoldPose */ false, - LetterboxConfiguration::movePositionForVerticalReachabilityToNextTopStop); + AppCompatConfiguration::movePositionForVerticalReachabilityToNextTopStop); assertForVerticalMove( /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM, /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM, /* expectedTime */ 2, /* halfFoldPose */ false, - LetterboxConfiguration::movePositionForVerticalReachabilityToNextBottomStop); + AppCompatConfiguration::movePositionForVerticalReachabilityToNextBottomStop); // Starting from top - tabletop mode assertForVerticalMove( /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP, /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM, /* expectedTime */ 1, /* halfFoldPose */ true, - LetterboxConfiguration::movePositionForVerticalReachabilityToNextBottomStop); + AppCompatConfiguration::movePositionForVerticalReachabilityToNextBottomStop); assertForVerticalMove( /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP, /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP, /* expectedTime */ 1, /* halfFoldPose */ true, - LetterboxConfiguration::movePositionForVerticalReachabilityToNextTopStop); + AppCompatConfiguration::movePositionForVerticalReachabilityToNextTopStop); // Starting from bottom - tabletop mode assertForVerticalMove( /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM, /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP, /* expectedTime */ 2, /* halfFoldPose */ true, - LetterboxConfiguration::movePositionForVerticalReachabilityToNextTopStop); + AppCompatConfiguration::movePositionForVerticalReachabilityToNextTopStop); assertForVerticalMove( /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM, /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM, /* expectedTime */ 2, /* halfFoldPose */ true, - LetterboxConfiguration::movePositionForVerticalReachabilityToNextBottomStop); + AppCompatConfiguration::movePositionForVerticalReachabilityToNextBottomStop); } private void assertForHorizontalMove(int from, int expected, int expectedTime, - boolean halfFoldPose, BiConsumer<LetterboxConfiguration, Boolean> move) { + boolean halfFoldPose, BiConsumer<AppCompatConfiguration, Boolean> move) { // We are in the current position - when(mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability(halfFoldPose)) + when(mAppCompatConfiguration.getLetterboxPositionForHorizontalReachability(halfFoldPose)) .thenReturn(from); - move.accept(mLetterboxConfiguration, halfFoldPose); - verify(mLetterboxConfigurationPersister, + move.accept(mAppCompatConfiguration, halfFoldPose); + verify(mAppCompatConfigurationPersister, times(expectedTime)).setLetterboxPositionForHorizontalReachability(halfFoldPose, expected); } private void assertForVerticalMove(int from, int expected, int expectedTime, - boolean halfFoldPose, BiConsumer<LetterboxConfiguration, Boolean> move) { + boolean halfFoldPose, BiConsumer<AppCompatConfiguration, Boolean> move) { // We are in the current position - when(mLetterboxConfiguration.getLetterboxPositionForVerticalReachability(halfFoldPose)) + when(mAppCompatConfiguration.getLetterboxPositionForVerticalReachability(halfFoldPose)) .thenReturn(from); - move.accept(mLetterboxConfiguration, halfFoldPose); - verify(mLetterboxConfigurationPersister, + move.accept(mAppCompatConfiguration, halfFoldPose); + verify(mAppCompatConfigurationPersister, times(expectedTime)).setLetterboxPositionForVerticalReachability(halfFoldPose, expected); } @@ -262,20 +262,20 @@ public class LetterboxConfigurationTest { @Test public void test_letterboxPositionWhenReachabilityEnabledIsReset() { // Check that horizontal reachability is set with correct arguments - mLetterboxConfiguration.resetPersistentLetterboxPositionForHorizontalReachability(); - verify(mLetterboxConfigurationPersister).setLetterboxPositionForHorizontalReachability( + mAppCompatConfiguration.resetPersistentLetterboxPositionForHorizontalReachability(); + verify(mAppCompatConfigurationPersister).setLetterboxPositionForHorizontalReachability( false /* forBookMode */, LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER); - verify(mLetterboxConfigurationPersister).setLetterboxPositionForHorizontalReachability( + verify(mAppCompatConfigurationPersister).setLetterboxPositionForHorizontalReachability( true /* forBookMode */, LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT); // Check that vertical reachability is set with correct arguments - mLetterboxConfiguration.resetPersistentLetterboxPositionForVerticalReachability(); - verify(mLetterboxConfigurationPersister).setLetterboxPositionForVerticalReachability( + mAppCompatConfiguration.resetPersistentLetterboxPositionForVerticalReachability(); + verify(mAppCompatConfigurationPersister).setLetterboxPositionForVerticalReachability( false /* forTabletopMode */, LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER); - verify(mLetterboxConfigurationPersister).setLetterboxPositionForVerticalReachability( + verify(mAppCompatConfigurationPersister).setLetterboxPositionForVerticalReachability( true /* forTabletopMode */, LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP); } @@ -283,16 +283,16 @@ public class LetterboxConfigurationTest { @Test public void test_letterboxPositionWhenReachabilityEnabledIsSet() { // Check that horizontal reachability is set with correct arguments - mLetterboxConfiguration.setPersistentLetterboxPositionForHorizontalReachability( + mAppCompatConfiguration.setPersistentLetterboxPositionForHorizontalReachability( false /* forBookMode */, LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT); - verify(mLetterboxConfigurationPersister).setLetterboxPositionForHorizontalReachability( + verify(mAppCompatConfigurationPersister).setLetterboxPositionForHorizontalReachability( false /* forBookMode */, LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT); // Check that vertical reachability is set with correct arguments - mLetterboxConfiguration.setPersistentLetterboxPositionForVerticalReachability( + mAppCompatConfiguration.setPersistentLetterboxPositionForVerticalReachability( false /* forTabletopMode */, LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP); - verify(mLetterboxConfigurationPersister).setLetterboxPositionForVerticalReachability( + verify(mAppCompatConfigurationPersister).setLetterboxPositionForVerticalReachability( false /* forTabletopMode */, LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP); } @@ -300,60 +300,60 @@ public class LetterboxConfigurationTest { @Test public void test_setLetterboxHorizontalPositionMultiplier_validValues() { assertThrows(IllegalArgumentException.class, - () -> mLetterboxConfiguration.setLetterboxHorizontalPositionMultiplier(-1)); + () -> mAppCompatConfiguration.setLetterboxHorizontalPositionMultiplier(-1)); assertThrows(IllegalArgumentException.class, - () -> mLetterboxConfiguration.setLetterboxHorizontalPositionMultiplier(2)); + () -> mAppCompatConfiguration.setLetterboxHorizontalPositionMultiplier(2)); // Does not throw an exception for values [0,1]. - mLetterboxConfiguration.setLetterboxHorizontalPositionMultiplier(0); - mLetterboxConfiguration.setLetterboxHorizontalPositionMultiplier(0.5f); - mLetterboxConfiguration.setLetterboxHorizontalPositionMultiplier(1); + mAppCompatConfiguration.setLetterboxHorizontalPositionMultiplier(0); + mAppCompatConfiguration.setLetterboxHorizontalPositionMultiplier(0.5f); + mAppCompatConfiguration.setLetterboxHorizontalPositionMultiplier(1); } @Test public void test_setLetterboxVerticalPositionMultiplier_validValues() { assertThrows(IllegalArgumentException.class, - () -> mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(-1)); + () -> mAppCompatConfiguration.setLetterboxVerticalPositionMultiplier(-1)); assertThrows(IllegalArgumentException.class, - () -> mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(2)); + () -> mAppCompatConfiguration.setLetterboxVerticalPositionMultiplier(2)); // Does not throw an exception for values [0,1]. - mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(0); - mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(0.5f); - mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(1); + mAppCompatConfiguration.setLetterboxVerticalPositionMultiplier(0); + mAppCompatConfiguration.setLetterboxVerticalPositionMultiplier(0.5f); + mAppCompatConfiguration.setLetterboxVerticalPositionMultiplier(1); } @Test public void test_setLetterboxBookModePositionMultiplier_validValues() { assertThrows(IllegalArgumentException.class, - () -> mLetterboxConfiguration.setLetterboxBookModePositionMultiplier(-1)); + () -> mAppCompatConfiguration.setLetterboxBookModePositionMultiplier(-1)); assertThrows(IllegalArgumentException.class, - () -> mLetterboxConfiguration.setLetterboxBookModePositionMultiplier(2)); + () -> mAppCompatConfiguration.setLetterboxBookModePositionMultiplier(2)); // Does not throw an exception for values [0,1]. - mLetterboxConfiguration.setLetterboxBookModePositionMultiplier(0); - mLetterboxConfiguration.setLetterboxBookModePositionMultiplier(0.5f); - mLetterboxConfiguration.setLetterboxBookModePositionMultiplier(1); + mAppCompatConfiguration.setLetterboxBookModePositionMultiplier(0); + mAppCompatConfiguration.setLetterboxBookModePositionMultiplier(0.5f); + mAppCompatConfiguration.setLetterboxBookModePositionMultiplier(1); } @Test public void test_setLetterboxTabletopModePositionMultiplier_validValues() { assertThrows(IllegalArgumentException.class, - () -> mLetterboxConfiguration.setLetterboxTabletopModePositionMultiplier(-1)); + () -> mAppCompatConfiguration.setLetterboxTabletopModePositionMultiplier(-1)); assertThrows(IllegalArgumentException.class, - () -> mLetterboxConfiguration.setLetterboxTabletopModePositionMultiplier(2)); + () -> mAppCompatConfiguration.setLetterboxTabletopModePositionMultiplier(2)); // Does not throw an exception for values [0,1]. - mLetterboxConfiguration.setLetterboxTabletopModePositionMultiplier(0); - mLetterboxConfiguration.setLetterboxTabletopModePositionMultiplier(0.5f); - mLetterboxConfiguration.setLetterboxTabletopModePositionMultiplier(1); + mAppCompatConfiguration.setLetterboxTabletopModePositionMultiplier(0); + mAppCompatConfiguration.setLetterboxTabletopModePositionMultiplier(0.5f); + mAppCompatConfiguration.setLetterboxTabletopModePositionMultiplier(1); } @Test public void test_evaluateThinLetterboxWhenDensityChanges() { final Resources rs = mock(Resources.class); final DisplayMetrics dm = mock(DisplayMetrics.class); - final LetterboxConfigurationPersister lp = mock(LetterboxConfigurationPersister.class); + final AppCompatConfigurationPersister lp = mock(AppCompatConfigurationPersister.class); spyOn(mContext); when(rs.getDisplayMetrics()).thenReturn(dm); when(mContext.getResources()).thenReturn(rs); @@ -361,7 +361,7 @@ public class LetterboxConfigurationTest { .thenReturn(100); when(rs.getDimensionPixelSize(R.dimen.config_letterboxThinLetterboxHeightDp)) .thenReturn(200); - final LetterboxConfiguration configuration = new LetterboxConfiguration(mContext, lp); + final AppCompatConfiguration configuration = new AppCompatConfiguration(mContext, lp); // Verify the values are the expected ones dm.density = 100; 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 35c2ee0b83a1..634453fe008c 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationOverridesTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationOverridesTest.java @@ -162,7 +162,7 @@ public class AppCompatOrientationOverridesTest extends WindowTestsBase { * Runs a test scenario providing a Robot. */ void runTestScenario(@NonNull Consumer<OrientationOverridesRobotTest> consumer) { - spyOn(mWm.mLetterboxConfiguration); + spyOn(mWm.mAppCompatConfiguration); final OrientationOverridesRobotTest robot = new OrientationOverridesRobotTest(mWm, mAtm, mSupervisor); consumer.accept(robot); 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 aa520e9cc599..ad34a6b0fc87 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java @@ -514,7 +514,7 @@ public class AppCompatOrientationPolicyTest extends WindowTestsBase { */ void runTestScenario(boolean withActivity, @NonNull Consumer<OrientationPolicyRobotTest> consumer) { - spyOn(mWm.mLetterboxConfiguration); + spyOn(mWm.mAppCompatConfiguration); final OrientationPolicyRobotTest robot = new OrientationPolicyRobotTest(mWm, mAtm, mSupervisor, withActivity); consumer.accept(robot); 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 de16e3888022..92f246be7cdf 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatRobotBase.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatRobotBase.java @@ -31,7 +31,7 @@ abstract class AppCompatRobotBase { @NonNull private final AppCompatActivityRobot mActivityRobot; @NonNull - private final AppCompatLetterboxConfigurationRobot mConfigurationRobot; + private final AppCompatConfigurationRobot mConfigurationRobot; @NonNull private final AppCompatComponentPropRobot mOptPropRobot; @@ -42,7 +42,7 @@ abstract class AppCompatRobotBase { mActivityRobot = new AppCompatActivityRobot(wm, atm, supervisor, displayWidth, displayHeight); mConfigurationRobot = - new AppCompatLetterboxConfigurationRobot(wm.mLetterboxConfiguration); + new AppCompatConfigurationRobot(wm.mAppCompatConfiguration); mOptPropRobot = new AppCompatComponentPropRobot(wm); } @@ -53,12 +53,12 @@ abstract class AppCompatRobotBase { } @NonNull - AppCompatLetterboxConfigurationRobot conf() { + AppCompatConfigurationRobot conf() { return mConfigurationRobot; } @NonNull - void applyOnConf(@NonNull Consumer<AppCompatLetterboxConfigurationRobot> consumer) { + void applyOnConf(@NonNull Consumer<AppCompatConfigurationRobot> consumer) { consumer.accept(mConfigurationRobot); } diff --git a/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java index 11e6d90a5f50..eaa164127551 100644 --- a/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java @@ -88,7 +88,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { private static final String CAMERA_ID_2 = "camera-2"; private CameraManager mMockCameraManager; private Handler mMockHandler; - private LetterboxConfiguration mLetterboxConfiguration; + private AppCompatConfiguration mAppCompatConfiguration; private CameraManager.AvailabilityCallback mCameraAvailabilityCallback; private CameraCompatFreeformPolicy mCameraCompatFreeformPolicy; @@ -98,13 +98,13 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { @Before public void setUp() throws Exception { - mLetterboxConfiguration = mDisplayContent.mWmService.mLetterboxConfiguration; - spyOn(mLetterboxConfiguration); - when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled()) + mAppCompatConfiguration = mDisplayContent.mWmService.mAppCompatConfiguration; + spyOn(mAppCompatConfiguration); + when(mAppCompatConfiguration.isCameraCompatTreatmentEnabled()) .thenReturn(true); - when(mLetterboxConfiguration.isCameraCompatRefreshEnabled()) + when(mAppCompatConfiguration.isCameraCompatRefreshEnabled()) .thenReturn(true); - when(mLetterboxConfiguration.isCameraCompatRefreshCycleThroughStopEnabled()) + when(mAppCompatConfiguration.isCameraCompatRefreshCycleThroughStopEnabled()) .thenReturn(true); mMockCameraManager = mock(CameraManager.class); @@ -228,7 +228,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { @Test public void testOnActivityConfigurationChanging_cycleThroughStopDisabled() throws Exception { - when(mLetterboxConfiguration.isCameraCompatRefreshCycleThroughStopEnabled()) + when(mAppCompatConfiguration.isCameraCompatRefreshCycleThroughStopEnabled()) .thenReturn(false); configureActivity(SCREEN_ORIENTATION_PORTRAIT); diff --git a/services/tests/wmtests/src/com/android/server/wm/CameraStateMonitorTests.java b/services/tests/wmtests/src/com/android/server/wm/CameraStateMonitorTests.java index 1c8dc05c6787..12f5714f91c7 100644 --- a/services/tests/wmtests/src/com/android/server/wm/CameraStateMonitorTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/CameraStateMonitorTests.java @@ -60,7 +60,7 @@ public final class CameraStateMonitorTests extends WindowTestsBase { private static final String TEST_PACKAGE_1_LABEL = "testPackage1"; private CameraManager mMockCameraManager; private Handler mMockHandler; - private LetterboxConfiguration mLetterboxConfiguration; + private AppCompatConfiguration mAppCompatConfiguration; private CameraStateMonitor mCameraStateMonitor; private CameraManager.AvailabilityCallback mCameraAvailabilityCallback; @@ -88,13 +88,13 @@ public final class CameraStateMonitorTests extends WindowTestsBase { @Before public void setUp() throws Exception { - mLetterboxConfiguration = mDisplayContent.mWmService.mLetterboxConfiguration; - spyOn(mLetterboxConfiguration); - when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled()) + mAppCompatConfiguration = mDisplayContent.mWmService.mAppCompatConfiguration; + spyOn(mAppCompatConfiguration); + when(mAppCompatConfiguration.isCameraCompatTreatmentEnabled()) .thenReturn(true); - when(mLetterboxConfiguration.isCameraCompatRefreshEnabled()) + when(mAppCompatConfiguration.isCameraCompatRefreshEnabled()) .thenReturn(true); - when(mLetterboxConfiguration.isCameraCompatRefreshCycleThroughStopEnabled()) + when(mAppCompatConfiguration.isCameraCompatRefreshCycleThroughStopEnabled()) .thenReturn(true); mMockCameraManager = mock(CameraManager.class); 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 23a88a1774f4..b687042edfc3 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java @@ -21,9 +21,19 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.content.pm.ActivityInfo.RESIZE_MODE_RESIZEABLE; +import static android.content.pm.ActivityInfo.RESIZE_MODE_UNRESIZEABLE; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; +import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; +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.server.wm.DesktopModeLaunchParamsModifier.DESKTOP_MODE_INITIAL_BOUNDS_SCALE; +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; import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.PHASE_DISPLAY; import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.RESULT_CONTINUE; import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.RESULT_SKIP; @@ -59,6 +69,10 @@ import org.junit.runner.RunWith; @RunWith(WindowTestRunner.class) public class DesktopModeLaunchParamsModifierTests extends LaunchParamsModifierTestsBase<DesktopModeLaunchParamsModifier> { + private static final Rect LANDSCAPE_DISPLAY_BOUNDS = new Rect(0, 0, 2560, 1600); + private static final Rect PORTRAIT_DISPLAY_BOUNDS = new Rect(0, 0, 1600, 2560); + private static final float LETTERBOX_ASPECT_RATIO = 1.3f; + @Before public void setUp() throws Exception { mActivity = new ActivityBuilder(mAtm).build(); @@ -158,6 +172,7 @@ public class DesktopModeLaunchParamsModifierTests extends @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + @DisableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) public void testUsesDesiredBoundsIfEmptyLayoutAndActivityOptionsBounds() { setupDesktopModeLaunchParamsModifier(); @@ -169,6 +184,209 @@ public class DesktopModeLaunchParamsModifierTests extends (int) (DISPLAY_BOUNDS.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE); final int desiredHeight = (int) (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_resizable_undefinedOrientation() { + setupDesktopModeLaunchParamsModifier(); + + final TestDisplayContent display = createDisplayContent(ORIENTATION_LANDSCAPE, + LANDSCAPE_DISPLAY_BOUNDS); + final Task task = createTask(display, SCREEN_ORIENTATION_UNSPECIFIED, true); + + 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_resizable_landscapeOrientation() { + setupDesktopModeLaunchParamsModifier(); + + final TestDisplayContent display = createDisplayContent(ORIENTATION_LANDSCAPE, + LANDSCAPE_DISPLAY_BOUNDS); + final Task task = createTask(display, SCREEN_ORIENTATION_LANDSCAPE, true); + + 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(); + doReturn(LETTERBOX_ASPECT_RATIO).when(() + -> calculateAspectRatio(any(), any())); + + final TestDisplayContent display = createDisplayContent(ORIENTATION_LANDSCAPE, + LANDSCAPE_DISPLAY_BOUNDS); + final Task task = createTask(display, SCREEN_ORIENTATION_PORTRAIT, true); + + final int desiredWidth = + (int) ((LANDSCAPE_DISPLAY_BOUNDS.height() / LETTERBOX_ASPECT_RATIO) + 0.5f); + 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_unResizable_landscapeOrientation() { + setupDesktopModeLaunchParamsModifier(); + + final TestDisplayContent display = createDisplayContent(ORIENTATION_LANDSCAPE, + LANDSCAPE_DISPLAY_BOUNDS); + final Task task = createTask(display, SCREEN_ORIENTATION_LANDSCAPE, false); + + 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 testUnResizablePortraitBounds_landscapeDevice_unResizable_portraitOrientation() { + setupDesktopModeLaunchParamsModifier(); + doReturn(LETTERBOX_ASPECT_RATIO).when(() + -> calculateAspectRatio(any(), any())); + + final TestDisplayContent display = createDisplayContent(ORIENTATION_LANDSCAPE, + LANDSCAPE_DISPLAY_BOUNDS); + final Task task = createTask(display, SCREEN_ORIENTATION_PORTRAIT, false); + + final int desiredHeight = + (int) (LANDSCAPE_DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE); + final int desiredWidth = (int) (desiredHeight / LETTERBOX_ASPECT_RATIO); + + 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_resizable_undefinedOrientation() { + setupDesktopModeLaunchParamsModifier(); + + final TestDisplayContent display = createDisplayContent(ORIENTATION_PORTRAIT, + PORTRAIT_DISPLAY_BOUNDS); + final Task task = createTask(display, SCREEN_ORIENTATION_UNSPECIFIED, true); + + 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_resizable_portraitOrientation() { + setupDesktopModeLaunchParamsModifier(); + + final TestDisplayContent display = createDisplayContent(ORIENTATION_PORTRAIT, + PORTRAIT_DISPLAY_BOUNDS); + final Task task = createTask(display, SCREEN_ORIENTATION_PORTRAIT, true); + + 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(); + doReturn(LETTERBOX_ASPECT_RATIO).when(() + -> calculateAspectRatio(any(), any())); + + final TestDisplayContent display = createDisplayContent(ORIENTATION_PORTRAIT, + PORTRAIT_DISPLAY_BOUNDS); + final Task task = createTask(display, SCREEN_ORIENTATION_LANDSCAPE, true); + + final int desiredWidth = PORTRAIT_DISPLAY_BOUNDS.width() + - (DESKTOP_MODE_LANDSCAPE_APP_PADDING * 2); + final int desiredHeight = (int) + ((PORTRAIT_DISPLAY_BOUNDS.width() / LETTERBOX_ASPECT_RATIO) + 0.5f); + + 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_unResizable_portraitOrientation() { + setupDesktopModeLaunchParamsModifier(); + + final TestDisplayContent display = createDisplayContent(ORIENTATION_PORTRAIT, + PORTRAIT_DISPLAY_BOUNDS); + final Task task = createTask(display, SCREEN_ORIENTATION_PORTRAIT, false); + + 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 testUnResizableLandscapeBounds_portraitDevice_unResizable_landscapeOrientation() { + setupDesktopModeLaunchParamsModifier(); + doReturn(LETTERBOX_ASPECT_RATIO).when(() + -> calculateAspectRatio(any(), any())); + + final TestDisplayContent display = createDisplayContent(ORIENTATION_PORTRAIT, + PORTRAIT_DISPLAY_BOUNDS); + final Task task = createTask(display, SCREEN_ORIENTATION_LANDSCAPE, false); + + final int desiredWidth = PORTRAIT_DISPLAY_BOUNDS.width() + - (DESKTOP_MODE_LANDSCAPE_APP_PADDING * 2); + final int desiredHeight = (int) (desiredWidth / LETTERBOX_ASPECT_RATIO); + assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate()); assertEquals(desiredWidth, mResult.mBounds.width()); assertEquals(desiredHeight, mResult.mBounds.height()); @@ -192,6 +410,7 @@ public class DesktopModeLaunchParamsModifierTests extends } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) public void testNonEmptyLayoutBounds_CenterToDisplay() { setupDesktopModeLaunchParamsModifier(); @@ -207,6 +426,7 @@ public class DesktopModeLaunchParamsModifierTests extends } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) public void testNonEmptyLayoutBounds_LeftGravity() { setupDesktopModeLaunchParamsModifier(); @@ -222,6 +442,7 @@ public class DesktopModeLaunchParamsModifierTests extends } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) public void testNonEmptyLayoutBounds_TopGravity() { setupDesktopModeLaunchParamsModifier(); @@ -237,6 +458,7 @@ public class DesktopModeLaunchParamsModifierTests extends } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) public void testNonEmptyLayoutBounds_TopLeftGravity() { setupDesktopModeLaunchParamsModifier(); @@ -252,6 +474,7 @@ public class DesktopModeLaunchParamsModifierTests extends } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) public void testNonEmptyLayoutBounds_RightGravity() { setupDesktopModeLaunchParamsModifier(); @@ -267,6 +490,7 @@ public class DesktopModeLaunchParamsModifierTests extends } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) public void testNonEmptyLayoutBounds_BottomGravity() { setupDesktopModeLaunchParamsModifier(); @@ -282,6 +506,7 @@ public class DesktopModeLaunchParamsModifierTests extends } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) public void testNonEmptyLayoutBounds_RightBottomGravity() { setupDesktopModeLaunchParamsModifier(); @@ -297,6 +522,7 @@ public class DesktopModeLaunchParamsModifierTests extends } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) public void testNonEmptyLayoutFractionBounds() { setupDesktopModeLaunchParamsModifier(); @@ -312,6 +538,7 @@ public class DesktopModeLaunchParamsModifierTests extends } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) public void testNonEmptyLayoutBoundsRespectsGravityWithEmptySize_LeftGravity() { setupDesktopModeLaunchParamsModifier(); @@ -327,6 +554,7 @@ public class DesktopModeLaunchParamsModifierTests extends } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) public void testNonEmptyLayoutBoundsRespectsGravityWithEmptySize_TopGravity() { setupDesktopModeLaunchParamsModifier(); @@ -342,6 +570,7 @@ public class DesktopModeLaunchParamsModifierTests extends } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) public void testNonEmptyLayoutBoundsRespectsGravityWithEmptySize_TopLeftGravity() { setupDesktopModeLaunchParamsModifier(); @@ -359,6 +588,7 @@ public class DesktopModeLaunchParamsModifierTests extends } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) public void testNonEmptyLayoutBoundsRespectsGravityWithEmptySize_RightGravity() { setupDesktopModeLaunchParamsModifier(); @@ -374,6 +604,7 @@ public class DesktopModeLaunchParamsModifierTests extends } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) public void testNonEmptyLayoutBoundsRespectsGravityWithEmptySize_BottomGravity() { setupDesktopModeLaunchParamsModifier(); @@ -389,6 +620,7 @@ public class DesktopModeLaunchParamsModifierTests extends } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) public void testNonEmptyLayoutBoundsRespectsGravityWithEmptySize_BottomRightGravity() { setupDesktopModeLaunchParamsModifier(); @@ -422,6 +654,38 @@ public class DesktopModeLaunchParamsModifierTests extends assertEquals(WINDOWING_MODE_FREEFORM, mResult.mWindowingMode); } + private Task createTask(DisplayContent display, int orientation, Boolean isResizeable) { + final int resizeMode = isResizeable ? RESIZE_MODE_RESIZEABLE + : RESIZE_MODE_UNRESIZEABLE; + final Task task = new TaskBuilder(mSupervisor).setActivityType( + ACTIVITY_TYPE_STANDARD).setDisplay(display).build(); + task.setResizeMode(resizeMode); + mActivity = new ActivityBuilder(task.mAtmService) + .setTask(task) + .setScreenOrientation(orientation) + .setOnTop(true).build(); + + mActivity.onDisplayChanged(display); + mActivity.setOccludesParent(true); + mActivity.setVisible(true); + mActivity.setVisibleRequested(true); + mActivity.mDisplayContent.setIgnoreOrientationRequest(/* ignoreOrientationRequest */ true); + + return task; + } + + private TestDisplayContent createDisplayContent(int orientation, Rect displayBounds) { + final TestDisplayContent display = new TestDisplayContent + .Builder(mAtm, displayBounds.width(), displayBounds.height()) + .setPosition(DisplayContent.POSITION_TOP).build(); + display.setBounds(displayBounds); + display.getConfiguration().densityDpi = DENSITY_DEFAULT; + display.getConfiguration().orientation = ORIENTATION_LANDSCAPE; + display.getDefaultTaskDisplayArea().setWindowingMode(orientation); + + return display; + } + private void setupDesktopModeLaunchParamsModifier() { setupDesktopModeLaunchParamsModifier(/*isDesktopModeSupported=*/ true, /*enforceDeviceRestrictions=*/ true); diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java index af0856f6109f..ab0c8d4ed60a 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java @@ -1874,8 +1874,8 @@ public class DisplayContentTests extends WindowTestsBase { @EnableFlags(com.android.window.flags.Flags.FLAG_RESPECT_NON_TOP_VISIBLE_FIXED_ORIENTATION) @Test public void testRespectNonTopVisibleFixedOrientation() { - spyOn(mWm.mLetterboxConfiguration); - doReturn(false).when(mWm.mLetterboxConfiguration).isTranslucentLetterboxingEnabled(); + spyOn(mWm.mAppCompatConfiguration); + doReturn(false).when(mWm.mAppCompatConfiguration).isTranslucentLetterboxingEnabled(); makeDisplayPortrait(mDisplayContent); final ActivityRecord nonTopVisible = new ActivityBuilder(mAtm) .setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT) @@ -2604,7 +2604,7 @@ public class DisplayContentTests extends WindowTestsBase { // test misc display overrides assertEquals(ignoreOrientationRequests, testDisplayContent.mSetIgnoreOrientationRequest); assertEquals(fixedOrientationLetterboxRatio, - mWm.mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio(), + mWm.mAppCompatConfiguration.getFixedOrientationLetterboxAspectRatio(), 0 /* delta */); } @@ -2647,7 +2647,7 @@ public class DisplayContentTests extends WindowTestsBase { // test misc display overrides assertEquals(ignoreOrientationRequests, testDisplayContent.mSetIgnoreOrientationRequest); assertEquals(fixedOrientationLetterboxRatio, - mWm.mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio(), + mWm.mAppCompatConfiguration.getFixedOrientationLetterboxAspectRatio(), 0 /* delta */); } diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java index d7814ac4da30..e9fcc4048fbc 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java @@ -90,7 +90,7 @@ public final class DisplayRotationCompatPolicyTests extends WindowTestsBase { private static final String TEST_PACKAGE_1_LABEL = "testPackage1"; private CameraManager mMockCameraManager; private Handler mMockHandler; - private LetterboxConfiguration mLetterboxConfiguration; + private AppCompatConfiguration mAppCompatConfiguration; private ActivityRefresher mActivityRefresher; private DisplayRotationCompatPolicy mDisplayRotationCompatPolicy; @@ -101,13 +101,13 @@ public final class DisplayRotationCompatPolicyTests extends WindowTestsBase { @Before public void setUp() throws Exception { - mLetterboxConfiguration = mDisplayContent.mWmService.mLetterboxConfiguration; - spyOn(mLetterboxConfiguration); - when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled()) + mAppCompatConfiguration = mDisplayContent.mWmService.mAppCompatConfiguration; + spyOn(mAppCompatConfiguration); + when(mAppCompatConfiguration.isCameraCompatTreatmentEnabled()) .thenReturn(true); - when(mLetterboxConfiguration.isCameraCompatRefreshEnabled()) + when(mAppCompatConfiguration.isCameraCompatRefreshEnabled()) .thenReturn(true); - when(mLetterboxConfiguration.isCameraCompatRefreshCycleThroughStopEnabled()) + when(mAppCompatConfiguration.isCameraCompatRefreshCycleThroughStopEnabled()) .thenReturn(true); mMockCameraManager = mock(CameraManager.class); @@ -185,7 +185,7 @@ public final class DisplayRotationCompatPolicyTests extends WindowTestsBase { @Test public void testOnScreenRotationAnimationFinished_treatmentNotEnabled_doNotShowToast() { - when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled()) + when(mAppCompatConfiguration.isCameraCompatTreatmentEnabled()) .thenReturn(false); mDisplayRotationCompatPolicy.onScreenRotationAnimationFinished(); @@ -239,7 +239,7 @@ public final class DisplayRotationCompatPolicyTests extends WindowTestsBase { @Test public void testTreatmentNotEnabled_noForceRotationOrRefresh() throws Exception { - when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled()) + when(mAppCompatConfiguration.isCameraCompatTreatmentEnabled()) .thenReturn(false); configureActivity(SCREEN_ORIENTATION_PORTRAIT); @@ -253,7 +253,7 @@ public final class DisplayRotationCompatPolicyTests extends WindowTestsBase { @Test public void testTreatmentDisabledViaDeviceConfig_noForceRotationOrRefresh() throws Exception { - when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled()) + when(mAppCompatConfiguration.isCameraCompatTreatmentEnabled()) .thenReturn(false); configureActivity(SCREEN_ORIENTATION_PORTRAIT); @@ -480,7 +480,7 @@ public final class DisplayRotationCompatPolicyTests extends WindowTestsBase { @Test public void testOnActivityConfigurationChanging_refreshDisabledPerApp_noRefresh() throws Exception { - when(mLetterboxConfiguration.isCameraCompatRefreshEnabled()).thenReturn(false); + when(mAppCompatConfiguration.isCameraCompatRefreshEnabled()).thenReturn(false); configureActivity(SCREEN_ORIENTATION_PORTRAIT); @@ -519,7 +519,7 @@ public final class DisplayRotationCompatPolicyTests extends WindowTestsBase { @Test public void testOnActivityConfigurationChanging_cycleThroughStopDisabled() throws Exception { - when(mLetterboxConfiguration.isCameraCompatRefreshCycleThroughStopEnabled()) + when(mAppCompatConfiguration.isCameraCompatRefreshCycleThroughStopEnabled()) .thenReturn(false); configureActivity(SCREEN_ORIENTATION_PORTRAIT); diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationImmersiveAppCompatPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationImmersiveAppCompatPolicyTests.java index b1057032eb36..5e8f347c0c6e 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationImmersiveAppCompatPolicyTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationImmersiveAppCompatPolicyTests.java @@ -55,7 +55,7 @@ public class DisplayRotationImmersiveAppCompatPolicyTests extends WindowTestsBas private DisplayRotationImmersiveAppCompatPolicy mPolicy; - private LetterboxConfiguration mMockLetterboxConfiguration; + private AppCompatConfiguration mMockAppCompatConfiguration; private ActivityRecord mMockActivityRecord; private Task mMockTask; private WindowState mMockWindowState; @@ -77,15 +77,15 @@ public class DisplayRotationImmersiveAppCompatPolicyTests extends WindowTestsBas doReturn(mMockActivityRecord).when(mDisplayContent).topRunningActivity(); when(mDisplayContent.getIgnoreOrientationRequest()).thenReturn(true); - mMockLetterboxConfiguration = mock(LetterboxConfiguration.class); - when(mMockLetterboxConfiguration.isDisplayRotationImmersiveAppCompatPolicyEnabled()) + mMockAppCompatConfiguration = mock(AppCompatConfiguration.class); + when(mMockAppCompatConfiguration.isDisplayRotationImmersiveAppCompatPolicyEnabled()) .thenReturn(true); - when(mMockLetterboxConfiguration + when(mMockAppCompatConfiguration .isDisplayRotationImmersiveAppCompatPolicyEnabledAtBuildTime()) .thenReturn(true); mPolicy = DisplayRotationImmersiveAppCompatPolicy.createIfNeeded( - mMockLetterboxConfiguration, createDisplayRotationMock(), + mMockAppCompatConfiguration, createDisplayRotationMock(), mDisplayContent); } @@ -206,7 +206,7 @@ public class DisplayRotationImmersiveAppCompatPolicyTests extends WindowTestsBas @Test public void testRotationChoiceEnforcedOnly_featureFlagDisabled_lockNotEnforced() { - when(mMockLetterboxConfiguration.isDisplayRotationImmersiveAppCompatPolicyEnabled()) + when(mMockAppCompatConfiguration.isDisplayRotationImmersiveAppCompatPolicyEnabled()) .thenReturn(false); assertIsRotationLockEnforcedReturnsFalseForAllRotations(); 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 c42367e8ae18..d318f0047c08 100644 --- a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java @@ -106,15 +106,15 @@ public class LetterboxUiControllerTest extends WindowTestsBase { private Task mTask; private DisplayContent mDisplayContent; private LetterboxUiController mController; - private LetterboxConfiguration mLetterboxConfiguration; + private AppCompatConfiguration mAppCompatConfiguration; private final Rect mLetterboxedPortraitTaskBounds = new Rect(); @Before public void setUp() throws Exception { mActivity = setUpActivityWithComponent(); - mLetterboxConfiguration = mWm.mLetterboxConfiguration; - spyOn(mLetterboxConfiguration); + mAppCompatConfiguration = mWm.mAppCompatConfiguration; + spyOn(mAppCompatConfiguration); mController = new LetterboxUiController(mWm, mActivity); } @@ -238,7 +238,7 @@ public class LetterboxUiControllerTest extends WindowTestsBase { /*centerX=*/ 1, /*centerY=*/ 1) ); insets.setRoundedCorners(roundedCorners); - mLetterboxConfiguration.setLetterboxActivityCornersRadius(-1); + mAppCompatConfiguration.setLetterboxActivityCornersRadius(-1); assertEquals(expectedRadius, mController.getRoundedCornersRadius(mainWindow)); } @@ -251,7 +251,7 @@ public class LetterboxUiControllerTest extends WindowTestsBase { final WindowState mainWindow = mockForGetCropBoundsAndRoundedCorners(/*taskbar=*/ null); mainWindow.mInvGlobalScale = invGlobalScale; - mLetterboxConfiguration.setLetterboxActivityCornersRadius(configurationRadius); + mAppCompatConfiguration.setLetterboxActivityCornersRadius(configurationRadius); doReturn(true).when(mActivity).isInLetterboxAnimation(); assertEquals(expectedRadius, mController.getRoundedCornersRadius(mainWindow)); @@ -272,7 +272,7 @@ public class LetterboxUiControllerTest extends WindowTestsBase { final int configurationRadius = 15; final WindowState mainWindow = mockForGetCropBoundsAndRoundedCorners(/*taskbar=*/ null); - mLetterboxConfiguration.setLetterboxActivityCornersRadius(configurationRadius); + mAppCompatConfiguration.setLetterboxActivityCornersRadius(configurationRadius); mainWindow.mInvGlobalScale = -1f; assertEquals(configurationRadius, mController.getRoundedCornersRadius(mainWindow)); @@ -310,7 +310,7 @@ public class LetterboxUiControllerTest extends WindowTestsBase { doReturn(true).when(mainWindow).isOnScreen(); doReturn(false).when(mainWindow).isLetterboxedForDisplayCutout(); doReturn(true).when(mainWindow).areAppWindowBoundsLetterboxed(); - doReturn(true).when(mLetterboxConfiguration).isLetterboxActivityCornersRounded(); + doReturn(true).when(mAppCompatConfiguration).isLetterboxActivityCornersRounded(); doReturn(TASKBAR_EXPANDED_HEIGHT).when(resources).getDimensionPixelSize( R.dimen.taskbar_frame_height); @@ -326,7 +326,7 @@ public class LetterboxUiControllerTest extends WindowTestsBase { mockThatProperty(PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE, /* value */ true); - doReturn(false).when(mLetterboxConfiguration).isUserAppAspectRatioFullscreenEnabled(); + doReturn(false).when(mAppCompatConfiguration).isUserAppAspectRatioFullscreenEnabled(); mActivity = setUpActivityWithComponent(); assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides() @@ -420,7 +420,7 @@ public class LetterboxUiControllerTest extends WindowTestsBase { @Test public void testShouldApplyUserMinAspectRatioOverride_trueProperty_returnsFalse() throws Exception { - doReturn(false).when(mLetterboxConfiguration).isUserAppAspectRatioSettingsEnabled(); + doReturn(false).when(mAppCompatConfiguration).isUserAppAspectRatioSettingsEnabled(); mockThatProperty(PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE, /* value */ true); mActivity = setUpActivityWithComponent(); @@ -458,7 +458,7 @@ public class LetterboxUiControllerTest extends WindowTestsBase { boolean orientationRequest) { spyOn(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides()); doReturn(orientationRequest).when( - mLetterboxConfiguration).isUserAppAspectRatioSettingsEnabled(); + mAppCompatConfiguration).isUserAppAspectRatioSettingsEnabled(); mDisplayContent.setIgnoreOrientationRequest(true); doReturn(USER_MIN_ASPECT_RATIO_3_2) .when(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides()) @@ -471,7 +471,7 @@ public class LetterboxUiControllerTest extends WindowTestsBase { private void prepareActivityThatShouldApplyUserFullscreenOverride() { spyOn(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides()); - doReturn(true).when(mLetterboxConfiguration).isUserAppAspectRatioFullscreenEnabled(); + doReturn(true).when(mAppCompatConfiguration).isUserAppAspectRatioFullscreenEnabled(); mDisplayContent.setIgnoreOrientationRequest(true); doReturn(USER_MIN_ASPECT_RATIO_FULLSCREEN) .when(mActivity.mAppCompatController.getAppCompatAspectRatioOverrides()) @@ -531,7 +531,7 @@ public class LetterboxUiControllerTest extends WindowTestsBase { @Test @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS}) public void testShouldSendFakeFocus_overrideEnabled_returnsTrue() { - doReturn(true).when(mLetterboxConfiguration).isCompatFakeFocusEnabled(); + doReturn(true).when(mAppCompatConfiguration).isCompatFakeFocusEnabled(); mController = new LetterboxUiController(mWm, mActivity); @@ -541,7 +541,7 @@ public class LetterboxUiControllerTest extends WindowTestsBase { @Test @DisableCompatChanges({OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS}) public void testShouldSendFakeFocus_overrideDisabled_returnsFalse() { - doReturn(true).when(mLetterboxConfiguration).isCompatFakeFocusEnabled(); + doReturn(true).when(mAppCompatConfiguration).isCompatFakeFocusEnabled(); mController = new LetterboxUiController(mWm, mActivity); @@ -552,7 +552,7 @@ public class LetterboxUiControllerTest extends WindowTestsBase { @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS}) public void testIsCompatFakeFocusEnabled_propertyDisabledAndOverrideEnabled_fakeFocusDisabled() throws Exception { - doReturn(true).when(mLetterboxConfiguration).isCompatFakeFocusEnabled(); + doReturn(true).when(mAppCompatConfiguration).isCompatFakeFocusEnabled(); mockThatProperty(PROPERTY_COMPAT_ENABLE_FAKE_FOCUS, /* value */ false); mController = new LetterboxUiController(mWm, mActivity); @@ -564,7 +564,7 @@ public class LetterboxUiControllerTest extends WindowTestsBase { @DisableCompatChanges({OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS}) public void testIsCompatFakeFocusEnabled_propertyEnabled_noOverride_fakeFocusEnabled() throws Exception { - doReturn(true).when(mLetterboxConfiguration).isCompatFakeFocusEnabled(); + doReturn(true).when(mAppCompatConfiguration).isCompatFakeFocusEnabled(); mockThatProperty(PROPERTY_COMPAT_ENABLE_FAKE_FOCUS, /* value */ true); mController = new LetterboxUiController(mWm, mActivity); @@ -575,7 +575,7 @@ public class LetterboxUiControllerTest extends WindowTestsBase { @Test public void testIsCompatFakeFocusEnabled_propertyDisabled_fakeFocusDisabled() throws Exception { - doReturn(true).when(mLetterboxConfiguration).isCompatFakeFocusEnabled(); + doReturn(true).when(mAppCompatConfiguration).isCompatFakeFocusEnabled(); mockThatProperty(PROPERTY_COMPAT_ENABLE_FAKE_FOCUS, /* value */ false); mController = new LetterboxUiController(mWm, mActivity); @@ -586,7 +586,7 @@ public class LetterboxUiControllerTest extends WindowTestsBase { @Test public void testIsCompatFakeFocusEnabled_propertyEnabled_fakeFocusEnabled() throws Exception { - doReturn(true).when(mLetterboxConfiguration).isCompatFakeFocusEnabled(); + doReturn(true).when(mAppCompatConfiguration).isCompatFakeFocusEnabled(); mockThatProperty(PROPERTY_COMPAT_ENABLE_FAKE_FOCUS, /* value */ true); mController = new LetterboxUiController(mWm, mActivity); @@ -862,15 +862,15 @@ public class LetterboxUiControllerTest extends WindowTestsBase { @Test public void testgetFixedOrientationLetterboxAspectRatio_splitScreenAspectEnabled() { - doReturn(true).when(mActivity.mWmService.mLetterboxConfiguration) + doReturn(true).when(mActivity.mWmService.mAppCompatConfiguration) .isCameraCompatTreatmentEnabled(); - doReturn(true).when(mActivity.mWmService.mLetterboxConfiguration) + doReturn(true).when(mActivity.mWmService.mAppCompatConfiguration) .isCameraCompatTreatmentEnabledAtBuildTime(); - doReturn(true).when(mActivity.mWmService.mLetterboxConfiguration) + doReturn(true).when(mActivity.mWmService.mAppCompatConfiguration) .isCameraCompatSplitScreenAspectRatioEnabled(); - doReturn(false).when(mActivity.mWmService.mLetterboxConfiguration) + doReturn(false).when(mActivity.mWmService.mAppCompatConfiguration) .getIsDisplayAspectRatioEnabledForFixedOrientationLetterbox(); - doReturn(1.5f).when(mActivity.mWmService.mLetterboxConfiguration) + doReturn(1.5f).when(mActivity.mWmService.mAppCompatConfiguration) .getFixedOrientationLetterboxAspectRatio(); // Recreate DisplayContent with DisplayRotationCompatPolicy @@ -894,13 +894,13 @@ public class LetterboxUiControllerTest extends WindowTestsBase { @Test public void testIsVerticalThinLetterboxed() { // Vertical thin letterbox disabled - doReturn(-1).when(mActivity.mWmService.mLetterboxConfiguration) + doReturn(-1).when(mActivity.mWmService.mAppCompatConfiguration) .getThinLetterboxHeightPx(); assertFalse(mController.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.mLetterboxConfiguration) + doReturn(10).when(mActivity.mWmService.mAppCompatConfiguration) .getThinLetterboxHeightPx(); // Vertical thin letterbox disabled without Task @@ -925,13 +925,13 @@ public class LetterboxUiControllerTest extends WindowTestsBase { @Test public void testIsHorizontalThinLetterboxed() { // Horizontal thin letterbox disabled - doReturn(-1).when(mActivity.mWmService.mLetterboxConfiguration) + doReturn(-1).when(mActivity.mWmService.mAppCompatConfiguration) .getThinLetterboxWidthPx(); assertFalse(mController.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.mLetterboxConfiguration) + doReturn(10).when(mActivity.mWmService.mAppCompatConfiguration) .getThinLetterboxWidthPx(); // Vertical thin letterbox disabled without Task @@ -986,7 +986,7 @@ public class LetterboxUiControllerTest extends WindowTestsBase { @Test public void testIsLetterboxEducationEnabled() { mController.isLetterboxEducationEnabled(); - verify(mLetterboxConfiguration).getIsEducationEnabled(); + verify(mAppCompatConfiguration).getIsEducationEnabled(); } private void mockThatProperty(String propertyName, boolean value) throws Exception { diff --git a/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java index 0bf27d11493b..f93ffb83178f 100644 --- a/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java @@ -747,6 +747,8 @@ public class RemoteAnimationControllerTest extends WindowTestsBase { } } + @android.platform.test.annotations.RequiresFlagsDisabled( + com.android.window.flags.Flags.FLAG_DO_NOT_SKIP_IME_BY_TARGET_VISIBILITY) @SetupWindows(addWindows = W_INPUT_METHOD) @Test public void testLaunchRemoteAnimationWithoutImeBehind() { diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java index afe360430f02..8981f715cf4b 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java @@ -66,7 +66,7 @@ import static com.android.server.wm.ActivityRecord.State.RESUMED; import static com.android.server.wm.ActivityRecord.State.STOPPED; import static com.android.server.wm.AppCompatUtils.computeAspectRatio; import static com.android.server.wm.DisplayContent.IME_TARGET_LAYERING; -import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_POSITION_MULTIPLIER_CENTER; +import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_POSITION_MULTIPLIER_CENTER; import static com.android.server.wm.WindowContainer.POSITION_TOP; import static com.google.common.truth.Truth.assertThat; @@ -228,7 +228,7 @@ public class SizeCompatTests extends WindowTestsBase { boolean horizontalReachability) { setUpDisplaySizeWithApp(displayWidth, displayHeight); mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); - final LetterboxConfiguration config = mWm.mLetterboxConfiguration; + final AppCompatConfiguration config = mWm.mAppCompatConfiguration; config.setTranslucentLetterboxingOverrideEnabled(true); config.setLetterboxVerticalPositionMultiplier(0.5f); config.setIsVerticalReachabilityEnabled(true); @@ -353,8 +353,8 @@ public class SizeCompatTests extends WindowTestsBase { .build(); setUpApp(display); display.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); - mWm.mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(0.5f); - mWm.mLetterboxConfiguration.setIsVerticalReachabilityEnabled(true); + mWm.mAppCompatConfiguration.setLetterboxVerticalPositionMultiplier(0.5f); + mWm.mAppCompatConfiguration.setIsVerticalReachabilityEnabled(true); final ActivityRecord activity = getActivityBuilderOnSameTask() .setScreenOrientation(SCREEN_ORIENTATION_LANDSCAPE) @@ -1082,9 +1082,9 @@ public class SizeCompatTests extends WindowTestsBase { RESIZE_MODE_UNRESIZEABLE, ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); // Simulate the user selecting the fullscreen user aspect ratio override - spyOn(activity.mWmService.mLetterboxConfiguration); + spyOn(activity.mWmService.mAppCompatConfiguration); spyOn(activity.mAppCompatController.getAppCompatAspectRatioOverrides()); - doReturn(true).when(activity.mWmService.mLetterboxConfiguration) + doReturn(true).when(activity.mWmService.mAppCompatConfiguration) .isUserAppAspectRatioFullscreenEnabled(); doReturn(USER_MIN_ASPECT_RATIO_FULLSCREEN) .when(activity.mAppCompatController.getAppCompatAspectRatioOverrides()) @@ -1869,7 +1869,7 @@ public class SizeCompatTests extends WindowTestsBase { // Portrait fixed app with min aspect ratio higher that aspect ratio override for fixed // orientation letterbox. - mActivity.mWmService.mLetterboxConfiguration.setFixedOrientationLetterboxAspectRatio(1.1f); + mActivity.mWmService.mAppCompatConfiguration.setFixedOrientationLetterboxAspectRatio(1.1f); mActivity.info.setMinAspectRatio(3); prepareUnresizable(mActivity, /* maxAspect= */ 0, SCREEN_ORIENTATION_PORTRAIT); @@ -1901,7 +1901,7 @@ public class SizeCompatTests extends WindowTestsBase { // Portrait fixed app with max aspect ratio lower that aspect ratio override for fixed // orientation letterbox. - mActivity.mWmService.mLetterboxConfiguration.setFixedOrientationLetterboxAspectRatio(3); + mActivity.mWmService.mAppCompatConfiguration.setFixedOrientationLetterboxAspectRatio(3); prepareUnresizable(mActivity, /* maxAspect= */ 2, SCREEN_ORIENTATION_PORTRAIT); final Rect displayBounds = new Rect(mActivity.mDisplayContent.getBounds()); @@ -1931,7 +1931,7 @@ public class SizeCompatTests extends WindowTestsBase { mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); final float fixedOrientationLetterboxAspectRatio = 1.1f; - mActivity.mWmService.mLetterboxConfiguration.setFixedOrientationLetterboxAspectRatio( + mActivity.mWmService.mAppCompatConfiguration.setFixedOrientationLetterboxAspectRatio( fixedOrientationLetterboxAspectRatio); prepareLimitedBounds(mActivity, SCREEN_ORIENTATION_PORTRAIT, /* isUnresizable= */ false); @@ -1979,7 +1979,7 @@ public class SizeCompatTests extends WindowTestsBase { // Activity should be letterboxed with an aspect ratio of 1.01. final Rect afterBounds = mActivity.getBounds(); final float actualAspectRatio = 1f * afterBounds.height() / afterBounds.width(); - assertEquals(LetterboxConfiguration.DEFAULT_LETTERBOX_ASPECT_RATIO_FOR_MULTI_WINDOW, + assertEquals(AppCompatConfiguration.DEFAULT_LETTERBOX_ASPECT_RATIO_FOR_MULTI_WINDOW, actualAspectRatio, DELTA_ASPECT_RATIO_TOLERANCE); assertTrue(mActivity.areBoundsLetterboxed()); } @@ -2027,9 +2027,9 @@ public class SizeCompatTests extends WindowTestsBase { mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); final float fixedOrientationLetterboxAspectRatio = 1.1f; - mActivity.mWmService.mLetterboxConfiguration.setFixedOrientationLetterboxAspectRatio( + mActivity.mWmService.mAppCompatConfiguration.setFixedOrientationLetterboxAspectRatio( fixedOrientationLetterboxAspectRatio); - mActivity.mWmService.mLetterboxConfiguration.setDefaultMinAspectRatioForUnresizableApps( + mActivity.mWmService.mAppCompatConfiguration.setDefaultMinAspectRatioForUnresizableApps( 1.5f); prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT); @@ -2049,7 +2049,7 @@ public class SizeCompatTests extends WindowTestsBase { // Letterbox logic should use config_letterboxDefaultMinAspectRatioForUnresizableApps over // config_fixedOrientationLetterboxAspectRatio. assertEquals(displayBounds.height(), activityBounds.height()); - final float defaultAspectRatio = mActivity.mWmService.mLetterboxConfiguration + final float defaultAspectRatio = mActivity.mWmService.mAppCompatConfiguration .getDefaultMinAspectRatioForUnresizableApps(); assertEquals(displayBounds.height() / defaultAspectRatio, activityBounds.width(), 0.5); } @@ -2136,10 +2136,10 @@ public class SizeCompatTests extends WindowTestsBase { int screenHeight = 1400; setUpDisplaySizeWithApp(screenWidth, screenHeight); mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); - mActivity.mWmService.mLetterboxConfiguration + mActivity.mWmService.mAppCompatConfiguration .setIsSplitScreenAspectRatioForUnresizableAppsEnabled(true); - mActivity.mWmService.mLetterboxConfiguration.setFixedOrientationLetterboxAspectRatio(1.1f); + mActivity.mWmService.mAppCompatConfiguration.setFixedOrientationLetterboxAspectRatio(1.1f); prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT); @@ -2172,8 +2172,8 @@ public class SizeCompatTests extends WindowTestsBase { final int displayHeight = 1400; setUpDisplaySizeWithApp(displayWidth, displayHeight); mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); - spyOn(mActivity.mWmService.mLetterboxConfiguration); - doReturn(true).when(mActivity.mWmService.mLetterboxConfiguration) + spyOn(mActivity.mWmService.mAppCompatConfiguration); + doReturn(true).when(mActivity.mWmService.mAppCompatConfiguration) .isUserAppAspectRatioFullscreenEnabled(); // Set user aspect ratio override @@ -2197,8 +2197,8 @@ public class SizeCompatTests extends WindowTestsBase { final int displayHeight = 1600; setUpDisplaySizeWithApp(displayWidth, displayHeight); mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); - spyOn(mActivity.mWmService.mLetterboxConfiguration); - doReturn(true).when(mActivity.mWmService.mLetterboxConfiguration) + spyOn(mActivity.mWmService.mAppCompatConfiguration); + doReturn(true).when(mActivity.mWmService.mAppCompatConfiguration) .isUserAppAspectRatioFullscreenEnabled(); // Set user aspect ratio override @@ -2394,8 +2394,8 @@ public class SizeCompatTests extends WindowTestsBase { boolean enabled) { final ActivityRecord activity = getActivityBuilderOnSameTask().build(); activity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); - spyOn(activity.mWmService.mLetterboxConfiguration); - doReturn(enabled).when(activity.mWmService.mLetterboxConfiguration) + spyOn(activity.mWmService.mAppCompatConfiguration); + doReturn(enabled).when(activity.mWmService.mAppCompatConfiguration) .isUserAppAspectRatioSettingsEnabled(); // Set user aspect ratio override final IPackageManager pm = mAtm.getPackageManager(); @@ -2428,7 +2428,7 @@ public class SizeCompatTests extends WindowTestsBase { .build(); // Setup Letterbox Configuration activity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); - activity.mWmService.mLetterboxConfiguration.setFixedOrientationLetterboxAspectRatio(1.5f); + activity.mWmService.mAppCompatConfiguration.setFixedOrientationLetterboxAspectRatio(1.5f); // Non-resizable portrait activity prepareUnresizable(activity, ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); float expectedAspectRatio = 1f * displayWidth / getExpectedSplitSize(displayHeight); @@ -2449,7 +2449,7 @@ public class SizeCompatTests extends WindowTestsBase { .build(); // Setup Letterbox Configuration activity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); - activity.mWmService.mLetterboxConfiguration.setFixedOrientationLetterboxAspectRatio(1.5f); + activity.mWmService.mAppCompatConfiguration.setFixedOrientationLetterboxAspectRatio(1.5f); // Non-resizable portrait activity prepareUnresizable(activity, ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); float expectedAspectRatio = 1f * displayHeight / getExpectedSplitSize(displayWidth); @@ -2471,7 +2471,7 @@ public class SizeCompatTests extends WindowTestsBase { .build(); // Setup Letterbox Configuration activity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); - activity.mWmService.mLetterboxConfiguration.setFixedOrientationLetterboxAspectRatio(1.5f); + activity.mWmService.mAppCompatConfiguration.setFixedOrientationLetterboxAspectRatio(1.5f); // Non-resizable portrait activity prepareUnresizable(activity, ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); float expectedAspectRatio = 1f * displayWidth / getExpectedSplitSize(displayHeight); @@ -2493,7 +2493,7 @@ public class SizeCompatTests extends WindowTestsBase { .build(); // Setup Letterbox Configuration activity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); - activity.mWmService.mLetterboxConfiguration.setFixedOrientationLetterboxAspectRatio(1.5f); + activity.mWmService.mAppCompatConfiguration.setFixedOrientationLetterboxAspectRatio(1.5f); // Non-resizable portrait activity prepareUnresizable(activity, ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); float expectedAspectRatio = 1f * displayHeight / getExpectedSplitSize(displayWidth); @@ -2581,7 +2581,7 @@ public class SizeCompatTests extends WindowTestsBase { public void testOverrideMinAspectRatioExcludePortraitFullscreen() { setUpDisplaySizeWithApp(2600, 1600); mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); - mActivity.mWmService.mLetterboxConfiguration.setFixedOrientationLetterboxAspectRatio(1.33f); + mActivity.mWmService.mAppCompatConfiguration.setFixedOrientationLetterboxAspectRatio(1.33f); // Create a size compat activity on the same task. final ActivityRecord activity = getActivityBuilderOnSameTask().build(); @@ -2611,7 +2611,7 @@ public class SizeCompatTests extends WindowTestsBase { // In this test, the activity is not in fullscreen, so the override is not applied setUpDisplaySizeWithApp(2600, 1600); mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); - mActivity.mWmService.mLetterboxConfiguration.setFixedOrientationLetterboxAspectRatio(1.33f); + mActivity.mWmService.mAppCompatConfiguration.setFixedOrientationLetterboxAspectRatio(1.33f); // Create a size compat activity on the same task. final ActivityRecord activity = getActivityBuilderOnSameTask().build(); @@ -2677,10 +2677,10 @@ public class SizeCompatTests extends WindowTestsBase { int screenHeight = 1600; setUpDisplaySizeWithApp(screenWidth, screenHeight); mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); - mActivity.mWmService.mLetterboxConfiguration + mActivity.mWmService.mAppCompatConfiguration .setIsSplitScreenAspectRatioForUnresizableAppsEnabled(true); - mActivity.mWmService.mLetterboxConfiguration.setFixedOrientationLetterboxAspectRatio(1.1f); + mActivity.mWmService.mAppCompatConfiguration.setFixedOrientationLetterboxAspectRatio(1.1f); prepareUnresizable(mActivity, SCREEN_ORIENTATION_LANDSCAPE); @@ -2714,11 +2714,11 @@ public class SizeCompatTests extends WindowTestsBase { int displayHeight = 1600; setUpDisplaySizeWithApp(displayWidth, displayHeight); mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); - mWm.mLetterboxConfiguration.setFixedOrientationLetterboxAspectRatio(2f); + mWm.mAppCompatConfiguration.setFixedOrientationLetterboxAspectRatio(2f); // Enable display aspect ratio to take precedence before // fixedOrientationLetterboxAspectRatio - mWm.mLetterboxConfiguration + mWm.mAppCompatConfiguration .setIsDisplayAspectRatioEnabledForFixedOrientationLetterbox(true); // Set up resizable app in portrait @@ -2750,11 +2750,11 @@ public class SizeCompatTests extends WindowTestsBase { int displayHeight = 1400; setUpDisplaySizeWithApp(displayWidth, displayHeight); mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); - mWm.mLetterboxConfiguration.setFixedOrientationLetterboxAspectRatio(2f); + mWm.mAppCompatConfiguration.setFixedOrientationLetterboxAspectRatio(2f); // Enable display aspect ratio to take precedence before // fixedOrientationLetterboxAspectRatio - mWm.mLetterboxConfiguration + mWm.mAppCompatConfiguration .setIsDisplayAspectRatioEnabledForFixedOrientationLetterbox(true); // Set up resizable app in landscape @@ -2787,10 +2787,10 @@ public class SizeCompatTests extends WindowTestsBase { setUpDisplaySizeWithApp(displayWidth, displayHeight); mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); - mActivity.mWmService.mLetterboxConfiguration.setFixedOrientationLetterboxAspectRatio(1.1f); + mActivity.mWmService.mAppCompatConfiguration.setFixedOrientationLetterboxAspectRatio(1.1f); // Enable display aspect ratio to take precedence before // fixedOrientationLetterboxAspectRatio - mWm.mLetterboxConfiguration + mWm.mAppCompatConfiguration .setIsDisplayAspectRatioEnabledForFixedOrientationLetterbox(true); prepareUnresizable(mActivity, SCREEN_ORIENTATION_LANDSCAPE); @@ -2814,10 +2814,10 @@ public class SizeCompatTests extends WindowTestsBase { setUpDisplaySizeWithApp(displayWidth, displayHeight); mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); - mActivity.mWmService.mLetterboxConfiguration.setFixedOrientationLetterboxAspectRatio(1.1f); + mActivity.mWmService.mAppCompatConfiguration.setFixedOrientationLetterboxAspectRatio(1.1f); // Enable display aspect ratio to take precedence before // fixedOrientationLetterboxAspectRatio - mWm.mLetterboxConfiguration + mWm.mAppCompatConfiguration .setIsDisplayAspectRatioEnabledForFixedOrientationLetterbox(true); prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT); @@ -3134,14 +3134,14 @@ public class SizeCompatTests extends WindowTestsBase { RESIZE_MODE_UNRESIZEABLE, SCREEN_ORIENTATION_PORTRAIT); activity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); - spyOn(activity.mWmService.mLetterboxConfiguration); - doReturn(true).when(activity.mWmService.mLetterboxConfiguration) + spyOn(activity.mWmService.mAppCompatConfiguration); + doReturn(true).when(activity.mWmService.mAppCompatConfiguration) .isIgnoreOrientationRequestAllowed(); // Display should not be rotated. assertEquals(SCREEN_ORIENTATION_UNSPECIFIED, activity.mDisplayContent.getOrientation()); - doReturn(false).when(activity.mWmService.mLetterboxConfiguration) + doReturn(false).when(activity.mWmService.mAppCompatConfiguration) .isIgnoreOrientationRequestAllowed(); // Display should be rotated. @@ -3427,7 +3427,7 @@ public class SizeCompatTests extends WindowTestsBase { // Case when the reachability would be enabled otherwise setUpDisplaySizeWithApp(/* dw */ 1000, /* dh */ 2800); mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); - mWm.mLetterboxConfiguration.setIsVerticalReachabilityEnabled(true); + mWm.mAppCompatConfiguration.setIsVerticalReachabilityEnabled(true); prepareUnresizable(mActivity, SCREEN_ORIENTATION_LANDSCAPE); mActivity.getWindowConfiguration().setBounds(null); @@ -3442,7 +3442,7 @@ public class SizeCompatTests extends WindowTestsBase { mAtm.mDevEnableNonResizableMultiWindow = true; setUpDisplaySizeWithApp(2800, 1000); mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); - mWm.mLetterboxConfiguration.setIsHorizontalReachabilityEnabled(true); + mWm.mAppCompatConfiguration.setIsHorizontalReachabilityEnabled(true); setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ true); final TestSplitOrganizer organizer = new TestSplitOrganizer(mAtm, mActivity.getDisplayContent()); @@ -3465,7 +3465,7 @@ public class SizeCompatTests extends WindowTestsBase { mAtm.mDevEnableNonResizableMultiWindow = true; setUpDisplaySizeWithApp(1000, 2800); mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); - mWm.mLetterboxConfiguration.setIsVerticalReachabilityEnabled(true); + mWm.mAppCompatConfiguration.setIsVerticalReachabilityEnabled(true); setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ true); final TestSplitOrganizer organizer = new TestSplitOrganizer(mAtm, mActivity.getDisplayContent()); @@ -3487,7 +3487,7 @@ public class SizeCompatTests extends WindowTestsBase { public void testIsVerticalReachabilityEnabled_doesNotMatchParentWidth_false() { setUpDisplaySizeWithApp(1000, 2800); mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); - mWm.mLetterboxConfiguration.setIsVerticalReachabilityEnabled(true); + mWm.mAppCompatConfiguration.setIsVerticalReachabilityEnabled(true); setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ true); // Unresizable landscape-only activity. @@ -3509,7 +3509,7 @@ public class SizeCompatTests extends WindowTestsBase { public void testIsVerticalReachabilityEnabled_emptyBounds_true() { setUpDisplaySizeWithApp(/* dw */ 1000, /* dh */ 2800); mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); - mWm.mLetterboxConfiguration.setIsVerticalReachabilityEnabled(true); + mWm.mAppCompatConfiguration.setIsVerticalReachabilityEnabled(true); setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ true); prepareUnresizable(mActivity, SCREEN_ORIENTATION_LANDSCAPE); @@ -3526,7 +3526,7 @@ public class SizeCompatTests extends WindowTestsBase { public void testIsHorizontalReachabilityEnabled_emptyBounds_true() { setUpDisplaySizeWithApp(/* dw */ 2800, /* dh */ 1000); mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); - mWm.mLetterboxConfiguration.setIsHorizontalReachabilityEnabled(true); + mWm.mAppCompatConfiguration.setIsHorizontalReachabilityEnabled(true); setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ true); prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT); @@ -3543,7 +3543,7 @@ public class SizeCompatTests extends WindowTestsBase { public void testIsHorizontalReachabilityEnabled_portraitDisplayAndApp_true() { // Portrait display setUpDisplaySizeWithApp(1400, 1600); - mActivity.mWmService.mLetterboxConfiguration.setIsHorizontalReachabilityEnabled(true); + mActivity.mWmService.mAppCompatConfiguration.setIsHorizontalReachabilityEnabled(true); setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ true); // 16:9f unresizable portrait app @@ -3557,7 +3557,7 @@ public class SizeCompatTests extends WindowTestsBase { public void testIsVerticalReachabilityEnabled_landscapeDisplayAndApp_true() { // Landscape display setUpDisplaySizeWithApp(1600, 1500); - mActivity.mWmService.mLetterboxConfiguration.setIsVerticalReachabilityEnabled(true); + mActivity.mWmService.mAppCompatConfiguration.setIsVerticalReachabilityEnabled(true); setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ true); // 16:9f unresizable landscape app @@ -3571,7 +3571,7 @@ public class SizeCompatTests extends WindowTestsBase { public void testIsHorizontalReachabilityEnabled_doesNotMatchParentHeight_false() { setUpDisplaySizeWithApp(2800, 1000); mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); - mWm.mLetterboxConfiguration.setIsHorizontalReachabilityEnabled(true); + mWm.mAppCompatConfiguration.setIsHorizontalReachabilityEnabled(true); setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ true); // Unresizable portrait-only activity. @@ -3593,7 +3593,7 @@ public class SizeCompatTests extends WindowTestsBase { public void testIsHorizontalReachabilityEnabled_inSizeCompatMode_matchesParentHeight_true() { setUpDisplaySizeWithApp(1800, 2200); mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); - mWm.mLetterboxConfiguration.setIsHorizontalReachabilityEnabled(true); + mWm.mAppCompatConfiguration.setIsHorizontalReachabilityEnabled(true); setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ true); // Unresizable portrait-only activity. @@ -3615,7 +3615,7 @@ public class SizeCompatTests extends WindowTestsBase { public void testIsVerticalReachabilityEnabled_inSizeCompatMode_matchesParentWidth_true() { setUpDisplaySizeWithApp(2200, 1800); mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); - mWm.mLetterboxConfiguration.setIsVerticalReachabilityEnabled(true); + mWm.mAppCompatConfiguration.setIsVerticalReachabilityEnabled(true); setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ true); // Unresizable landscape-only activity. @@ -3698,7 +3698,7 @@ public class SizeCompatTests extends WindowTestsBase { @Test public void testLetterboxDetailsForStatusBar_letterboxNotOverlappingStatusBar() { // Align to center so that we don't overlap with the status bar - mAtm.mWindowManager.mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(0.5f); + mAtm.mWindowManager.mAppCompatConfiguration.setLetterboxVerticalPositionMultiplier(0.5f); final DisplayContent display = new TestDisplayContent.Builder(mAtm, 1000, 2800) .setNotch(100) .build(); @@ -3751,7 +3751,7 @@ public class SizeCompatTests extends WindowTestsBase { navSource.setFlags(FLAG_INSETS_ROUNDED_CORNER, FLAG_INSETS_ROUNDED_CORNER); navSource.setFrame(new Rect(0, screenHeight - taskbarHeight, screenWidth, screenHeight)); - mActivity.mWmService.mLetterboxConfiguration.setLetterboxActivityCornersRadius(15); + mActivity.mWmService.mAppCompatConfiguration.setLetterboxActivityCornersRadius(15); final WindowState w1 = addWindowToActivity(mActivity); w1.mAboveInsetsState.addSource(navSource); @@ -3793,7 +3793,7 @@ public class SizeCompatTests extends WindowTestsBase { setUpDisplaySizeWithApp(screenWidth, screenHeight); mActivity.mDisplayContent.setIgnoreOrientationRequest(true); - mActivity.mWmService.mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(1.0f); + mActivity.mWmService.mAppCompatConfiguration.setLetterboxVerticalPositionMultiplier(1.0f); final InsetsSource navSource = new InsetsSource( InsetsSource.createId(null, 0, navigationBars()), navigationBars()); @@ -3801,7 +3801,7 @@ public class SizeCompatTests extends WindowTestsBase { // Immersive activity has transient navbar navSource.setVisible(!immersive); navSource.setFrame(new Rect(0, screenHeight - taskbarHeight, screenWidth, screenHeight)); - mActivity.mWmService.mLetterboxConfiguration.setLetterboxActivityCornersRadius(15); + mActivity.mWmService.mAppCompatConfiguration.setLetterboxActivityCornersRadius(15); final WindowState w1 = addWindowToActivity(mActivity); w1.mAboveInsetsState.addSource(navSource); @@ -3909,7 +3909,7 @@ public class SizeCompatTests extends WindowTestsBase { spyOn(policy); doReturn(decorInfo).when(policy).getDecorInsetsInfo(ROTATION_90, display.mBaseDisplayHeight, display.mBaseDisplayWidth); - mWm.mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(0.5f); + mWm.mAppCompatConfiguration.setLetterboxVerticalPositionMultiplier(0.5f); setUpApp(display); prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT); @@ -3974,7 +3974,7 @@ public class SizeCompatTests extends WindowTestsBase { setUpDisplaySizeWithApp(2800, 1400); mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); - mActivity.mWmService.mLetterboxConfiguration.setLetterboxHorizontalPositionMultiplier( + mActivity.mWmService.mAppCompatConfiguration.setLetterboxHorizontalPositionMultiplier( letterboxHorizontalPositionMultiplier); prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT); assertEquals(fixedOrientationLetterbox, mActivity.getBounds()); @@ -4171,7 +4171,7 @@ public class SizeCompatTests extends WindowTestsBase { public void testApplyAspectRatio_containingRatioAlmostEqualToMaxRatio_boundsUnchanged() { setUpDisplaySizeWithApp(1981, 2576); mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); - mWm.mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(0.5f); + mWm.mAppCompatConfiguration.setLetterboxVerticalPositionMultiplier(0.5f); final Rect originalBounds = new Rect(mActivity.getBounds()); prepareUnresizable(mActivity, 1.3f, SCREEN_ORIENTATION_UNSPECIFIED); @@ -4206,7 +4206,7 @@ public class SizeCompatTests extends WindowTestsBase { spyOn(policy); doReturn(decorInfo).when(policy).getDecorInsetsInfo(ROTATION_90, display.mBaseDisplayHeight, display.mBaseDisplayWidth); - mWm.mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(0.5f); + mWm.mAppCompatConfiguration.setLetterboxVerticalPositionMultiplier(0.5f); setUpApp(display); prepareUnresizable(mActivity, SCREEN_ORIENTATION_LANDSCAPE); @@ -4269,7 +4269,7 @@ public class SizeCompatTests extends WindowTestsBase { // Set up a display in portrait with a fixed-orientation LANDSCAPE app setUpDisplaySizeWithApp(1400, 2800); mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); - mActivity.mWmService.mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier( + mActivity.mWmService.mAppCompatConfiguration.setLetterboxVerticalPositionMultiplier( 1.0f /*letterboxVerticalPositionMultiplier*/); prepareUnresizable(mActivity, SCREEN_ORIENTATION_LANDSCAPE); @@ -4294,7 +4294,7 @@ public class SizeCompatTests extends WindowTestsBase { // Set up a display in portrait with a fixed-orientation LANDSCAPE app. setUpDisplaySizeWithApp(1000, 2000); mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); - mActivity.mWmService.mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier( + mActivity.mWmService.mAppCompatConfiguration.setLetterboxVerticalPositionMultiplier( 1.0f /*letterboxVerticalPositionMultiplier*/); prepareUnresizable(mActivity, SCREEN_ORIENTATION_LANDSCAPE); @@ -4322,9 +4322,9 @@ public class SizeCompatTests extends WindowTestsBase { public void testGetFixedOrientationLetterboxAspectRatio_tabletop_centered() { // Set up a display in portrait with a fixed-orientation LANDSCAPE app setUpDisplaySizeWithApp(1400, 2800); - mWm.mLetterboxConfiguration.setLetterboxHorizontalPositionMultiplier( + mWm.mAppCompatConfiguration.setLetterboxHorizontalPositionMultiplier( LETTERBOX_POSITION_MULTIPLIER_CENTER); - mActivity.mWmService.mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier( + mActivity.mWmService.mAppCompatConfiguration.setLetterboxVerticalPositionMultiplier( 1.0f /*letterboxVerticalPositionMultiplier*/); prepareUnresizable(mActivity, SCREEN_ORIENTATION_LANDSCAPE); @@ -4360,8 +4360,8 @@ public class SizeCompatTests extends WindowTestsBase { // Set up a display in landscape with a fixed-orientation PORTRAIT app setUpDisplaySizeWithApp(2800, 1400); mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); - mWm.mLetterboxConfiguration.setIsAutomaticReachabilityInBookModeEnabled(true); - mWm.mLetterboxConfiguration.setLetterboxHorizontalPositionMultiplier( + mWm.mAppCompatConfiguration.setIsAutomaticReachabilityInBookModeEnabled(true); + mWm.mAppCompatConfiguration.setLetterboxHorizontalPositionMultiplier( 1.0f /*letterboxHorizontalPositionMultiplier*/); prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT); @@ -4386,8 +4386,8 @@ public class SizeCompatTests extends WindowTestsBase { // Set up a display in landscape with a fixed-orientation PORTRAIT app setUpDisplaySizeWithApp(2800, 1400); mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); - mWm.mLetterboxConfiguration.setIsAutomaticReachabilityInBookModeEnabled(false); - mWm.mLetterboxConfiguration.setLetterboxHorizontalPositionMultiplier(0.5f); + mWm.mAppCompatConfiguration.setIsAutomaticReachabilityInBookModeEnabled(false); + mWm.mAppCompatConfiguration.setLetterboxHorizontalPositionMultiplier(0.5f); prepareUnresizable(mActivity, 1.75f, SCREEN_ORIENTATION_PORTRAIT); Rect letterboxNoFold = new Rect(1000, 0, 1800, 1400); @@ -4469,7 +4469,7 @@ public class SizeCompatTests extends WindowTestsBase { setUpDisplaySizeWithApp(1400, 2800); mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); - mActivity.mWmService.mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier( + mActivity.mWmService.mAppCompatConfiguration.setLetterboxVerticalPositionMultiplier( letterboxVerticalPositionMultiplier); prepareUnresizable(mActivity, SCREEN_ORIENTATION_LANDSCAPE); @@ -4585,7 +4585,7 @@ public class SizeCompatTests extends WindowTestsBase { public void testIsEligibleForLetterboxEducation_educationNotEnabled_returnsFalse() { setUpDisplaySizeWithApp(2500, 1000); mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); - mActivity.mWmService.mLetterboxConfiguration.setIsEducationEnabled(false); + mActivity.mWmService.mAppCompatConfiguration.setIsEducationEnabled(false); prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT); @@ -4596,7 +4596,7 @@ public class SizeCompatTests extends WindowTestsBase { public void testIsEligibleForLetterboxEducation_notEligibleForFixedOrientation_returnsFalse() { setUpDisplaySizeWithApp(1000, 2500); mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); - mActivity.mWmService.mLetterboxConfiguration.setIsEducationEnabled(true); + mActivity.mWmService.mAppCompatConfiguration.setIsEducationEnabled(true); prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT); @@ -4609,7 +4609,7 @@ public class SizeCompatTests extends WindowTestsBase { mAtm.mDevEnableNonResizableMultiWindow = true; setUpDisplaySizeWithApp(1000, 1200); mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); - mActivity.mWmService.mLetterboxConfiguration.setIsEducationEnabled(true); + mActivity.mWmService.mAppCompatConfiguration.setIsEducationEnabled(true); final TestSplitOrganizer organizer = new TestSplitOrganizer(mAtm, mActivity.getDisplayContent()); @@ -4630,7 +4630,7 @@ public class SizeCompatTests extends WindowTestsBase { public void testIsEligibleForLetterboxEducation_fixedOrientationLandscape_returnsFalse() { setUpDisplaySizeWithApp(1000, 2500); mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); - mActivity.mWmService.mLetterboxConfiguration.setIsEducationEnabled(true); + mActivity.mWmService.mAppCompatConfiguration.setIsEducationEnabled(true); prepareUnresizable(mActivity, SCREEN_ORIENTATION_LANDSCAPE); @@ -4643,7 +4643,7 @@ public class SizeCompatTests extends WindowTestsBase { public void testIsEligibleForLetterboxEducation_hasStartingWindow_returnsFalseUntilRemoved() { setUpDisplaySizeWithApp(2500, 1000); mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); - mActivity.mWmService.mLetterboxConfiguration.setIsEducationEnabled(true); + mActivity.mWmService.mAppCompatConfiguration.setIsEducationEnabled(true); prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT); mActivity.mStartingData = mock(StartingData.class); @@ -4666,7 +4666,7 @@ public class SizeCompatTests extends WindowTestsBase { public void testIsEligibleForLetterboxEducation_hasStartingWindowAndEducationNotEnabled() { setUpDisplaySizeWithApp(2500, 1000); mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); - mActivity.mWmService.mLetterboxConfiguration.setIsEducationEnabled(false); + mActivity.mWmService.mAppCompatConfiguration.setIsEducationEnabled(false); prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT); mActivity.mStartingData = mock(StartingData.class); @@ -4689,7 +4689,7 @@ public class SizeCompatTests extends WindowTestsBase { public void testIsEligibleForLetterboxEducation_letterboxedForFixedOrientation_returnsTrue() { setUpDisplaySizeWithApp(2500, 1000); mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); - mActivity.mWmService.mLetterboxConfiguration.setIsEducationEnabled(true); + mActivity.mWmService.mAppCompatConfiguration.setIsEducationEnabled(true); prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT); @@ -4702,7 +4702,7 @@ public class SizeCompatTests extends WindowTestsBase { public void testIsEligibleForLetterboxEducation_sizeCompatAndEligibleForFixedOrientation() { setUpDisplaySizeWithApp(1000, 2500); mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); - mActivity.mWmService.mLetterboxConfiguration.setIsEducationEnabled(true); + mActivity.mWmService.mAppCompatConfiguration.setIsEducationEnabled(true); prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT); @@ -4879,7 +4879,7 @@ public class SizeCompatTests extends WindowTestsBase { setUpDisplaySizeWithApp(2800, 1400); mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); - mActivity.mWmService.mLetterboxConfiguration.setLetterboxHorizontalPositionMultiplier( + mActivity.mWmService.mAppCompatConfiguration.setLetterboxHorizontalPositionMultiplier( letterboxHorizontalPositionMultiplier); prepareUnresizable(mActivity, SCREEN_ORIENTATION_LANDSCAPE); assertFitted(); @@ -4896,7 +4896,7 @@ public class SizeCompatTests extends WindowTestsBase { setUpDisplaySizeWithApp(1400, 2800); mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); - mActivity.mWmService.mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier( + mActivity.mWmService.mAppCompatConfiguration.setLetterboxVerticalPositionMultiplier( letterboxVerticalPositionMultiplier); prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT); diff --git a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java index a816aa9b7598..d5d284783978 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java +++ b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java @@ -203,6 +203,7 @@ public class SystemServicesTestRule implements TestRule { .mockStatic(LockGuard.class, mockStubOnly) .mockStatic(Watchdog.class, mockStubOnly) .spyStatic(DesktopModeHelper.class) + .spyStatic(DesktopModeBoundsCalculator.class) .strictness(Strictness.LENIENT) .startMocking(); diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java index 76690ec1691f..0bf850afb30b 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java @@ -611,7 +611,7 @@ public class TaskTests extends WindowTestsBase { .setWindowingMode(WINDOWING_MODE_FULLSCREEN).setDisplay(display).build(); final Task task = rootTask.getBottomMostTask(); final ActivityRecord root = task.getTopNonFinishingActivity(); - spyOn(mWm.mLetterboxConfiguration); + spyOn(mWm.mAppCompatConfiguration); spyOn(root); spyOn(root.mAppCompatController.getAppCompatAspectRatioOverrides()); @@ -655,7 +655,7 @@ public class TaskTests extends WindowTestsBase { .setWindowingMode(WINDOWING_MODE_FULLSCREEN).setDisplay(display).build(); final Task task = rootTask.getBottomMostTask(); final ActivityRecord root = task.getTopNonFinishingActivity(); - spyOn(mWm.mLetterboxConfiguration); + spyOn(mWm.mAppCompatConfiguration); spyOn(root); doReturn(false).when(root).fillsParent(); 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 f07b402c31b6..cbf17c408115 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TransparentPolicyTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TransparentPolicyTest.java @@ -258,7 +258,7 @@ public class TransparentPolicyTest extends WindowTestsBase { robot.transparentActivity((ta) -> { ta.applyOnActivity((a) -> { a.applyToTopActivity((topActivity) -> { - topActivity.mWmService.mLetterboxConfiguration + topActivity.mWmService.mAppCompatConfiguration .setLetterboxHorizontalPositionMultiplier(1.0f); }); a.configureUnresizableTopActivity(SCREEN_ORIENTATION_PORTRAIT); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTraversalTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTraversalTests.java index 593e983f3d23..22def515a98e 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTraversalTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTraversalTests.java @@ -23,6 +23,7 @@ import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; +import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.eq; import android.platform.test.annotations.Presubmit; @@ -60,4 +61,24 @@ public class WindowContainerTraversalTests extends WindowTestsBase { verify(c).accept(eq(mDockedDividerWindow)); verify(c).accept(eq(mImeWindow)); } + + @android.platform.test.annotations.RequiresFlagsEnabled( + com.android.window.flags.Flags.FLAG_DO_NOT_SKIP_IME_BY_TARGET_VISIBILITY) + @SetupWindows(addWindows = { W_ACTIVITY, W_INPUT_METHOD }) + @Test + public void testTraverseImeRegardlessOfImeTarget() { + mDisplayContent.setImeLayeringTarget(mAppWindow); + mDisplayContent.setImeInputTarget(mAppWindow); + mAppWindow.mHasSurface = false; + mAppWindow.mActivityRecord.setVisibleRequested(false); + mAppWindow.mActivityRecord.setVisible(false); + + final boolean[] foundIme = { false }; + mDisplayContent.forAllWindows(w -> { + if (w == mImeWindow) { + foundIme[0] = true; + } + }, true /* traverseTopToBottom */); + assertTrue("IME must be found", foundIme[0]); + } } diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java index 89abe2ff0866..39640fbc9422 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java @@ -47,10 +47,10 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.never; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; -import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND; -import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING; -import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_SOLID_COLOR; -import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_WALLPAPER; +import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND; +import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING; +import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_SOLID_COLOR; +import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_WALLPAPER; import static com.google.common.truth.Truth.assertThat; @@ -1390,8 +1390,8 @@ public class WindowManagerServiceTests extends WindowTestsBase { } private boolean setupLetterboxConfigurationWithBackgroundType( - @LetterboxConfiguration.LetterboxBackgroundType int letterboxBackgroundType) { - mWm.mLetterboxConfiguration.setLetterboxBackgroundTypeOverride(letterboxBackgroundType); + @AppCompatConfiguration.LetterboxBackgroundType int letterboxBackgroundType) { + mWm.mAppCompatConfiguration.setLetterboxBackgroundTypeOverride(letterboxBackgroundType); return mWm.isLetterboxBackgroundMultiColored(); } } diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java index b512aa852ddb..ea2abf7ddcb8 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java @@ -141,8 +141,8 @@ import java.util.HashMap; import java.util.List; /** Common base class for window manager unit test classes. */ -class WindowTestsBase extends SystemServiceTestsBase { - final Context mContext = getInstrumentation().getTargetContext(); +public class WindowTestsBase extends SystemServiceTestsBase { + protected final Context mContext = getInstrumentation().getTargetContext(); // Default package name static final String DEFAULT_COMPONENT_PACKAGE_NAME = "com.foo"; @@ -262,34 +262,34 @@ class WindowTestsBase extends SystemServiceTestsBase { // Ensure letterbox aspect ratio is not overridden on any device target. // {@link com.android.internal.R.dimen.config_fixedOrientationLetterboxAspectRatio}, is set // on some device form factors. - mAtm.mWindowManager.mLetterboxConfiguration.setFixedOrientationLetterboxAspectRatio(0); + mAtm.mWindowManager.mAppCompatConfiguration.setFixedOrientationLetterboxAspectRatio(0); // Ensure letterbox horizontal position multiplier is not overridden on any device target. // {@link com.android.internal.R.dimen.config_letterboxHorizontalPositionMultiplier}, // may be set on some device form factors. - mAtm.mWindowManager.mLetterboxConfiguration.setLetterboxHorizontalPositionMultiplier(0.5f); + mAtm.mWindowManager.mAppCompatConfiguration.setLetterboxHorizontalPositionMultiplier(0.5f); // Ensure letterbox vertical position multiplier is not overridden on any device target. // {@link com.android.internal.R.dimen.config_letterboxHorizontalPositionMultiplier}, // may be set on some device form factors. - mAtm.mWindowManager.mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(0.0f); + mAtm.mWindowManager.mAppCompatConfiguration.setLetterboxVerticalPositionMultiplier(0.0f); // Ensure letterbox horizontal reachability treatment isn't overridden on any device target. // {@link com.android.internal.R.bool.config_letterboxIsHorizontalReachabilityEnabled}, // may be set on some device form factors. - mAtm.mWindowManager.mLetterboxConfiguration.setIsHorizontalReachabilityEnabled(false); + mAtm.mWindowManager.mAppCompatConfiguration.setIsHorizontalReachabilityEnabled(false); // Ensure letterbox vertical reachability treatment isn't overridden on any device target. // {@link com.android.internal.R.bool.config_letterboxIsVerticalReachabilityEnabled}, // may be set on some device form factors. - mAtm.mWindowManager.mLetterboxConfiguration.setIsVerticalReachabilityEnabled(false); + mAtm.mWindowManager.mAppCompatConfiguration.setIsVerticalReachabilityEnabled(false); // Ensure aspect ratio for unresizable apps isn't overridden on any device target. // {@link com.android.internal.R.bool // .config_letterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled}, may be set on some // device form factors. - mAtm.mWindowManager.mLetterboxConfiguration + mAtm.mWindowManager.mAppCompatConfiguration .setIsSplitScreenAspectRatioForUnresizableAppsEnabled(false); // Ensure aspect ratio for al apps isn't overridden on any device target. // {@link com.android.internal.R.bool // .config_letterboxIsDisplayAspectRatioForFixedOrientationLetterboxEnabled}, may be set on // some device form factors. - mAtm.mWindowManager.mLetterboxConfiguration + mAtm.mWindowManager.mAppCompatConfiguration .setIsDisplayAspectRatioEnabledForFixedOrientationLetterbox(false); // Setup WallpaperController crop utils with a simple center-align strategy @@ -331,7 +331,7 @@ class WindowTestsBase extends SystemServiceTestsBase { private void checkDeviceSpecificOverridesNotApplied() { // Check global overrides if (!sGlobalOverridesChecked) { - assertEquals(0, mWm.mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio(), + assertEquals(0, mWm.mAppCompatConfiguration.getFixedOrientationLetterboxAspectRatio(), 0 /* delta */); sGlobalOverridesChecked = true; } diff --git a/services/tests/wmtests/src/com/android/server/wm/utils/DesktopModeFlagsUtilTest.java b/services/tests/wmtests/src/com/android/server/wm/utils/DesktopModeFlagsUtilTest.java new file mode 100644 index 000000000000..e5f2f89ccead --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/utils/DesktopModeFlagsUtilTest.java @@ -0,0 +1,459 @@ +/* + * 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.utils; + +import static com.android.server.wm.utils.DesktopModeFlagsUtil.DESKTOP_WINDOWING_MODE; +import static com.android.server.wm.utils.DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_OFF; +import static com.android.server.wm.utils.DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_ON; +import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE; +import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY; +import static com.android.window.flags.Flags.FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.ContentResolver; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.annotations.Presubmit; +import android.platform.test.flag.junit.SetFlagsRule; +import android.provider.Settings; + +import androidx.test.filters.SmallTest; + +import com.android.server.wm.WindowTestRunner; +import com.android.server.wm.WindowTestsBase; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.lang.reflect.Field; + +/** + * Test class for [DesktopModeFlagsUtil] + * + * Build/Install/Run: + * atest WmTests:DesktopModeFlagsUtilTest + */ +@SmallTest +@Presubmit +@RunWith(WindowTestRunner.class) +public class DesktopModeFlagsUtilTest extends WindowTestsBase { + + @Rule + public SetFlagsRule setFlagsRule = new SetFlagsRule(); + + @Before + public void setUp() throws Exception { + resetCache(); + } + + private static final String SYSTEM_PROPERTY_OVERRIDE_KEY = + "sys.wmshell.desktopmode.dev_toggle_override"; + + @Test + @DisableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + public void isEnabled_devOptionFlagDisabled_overrideOff_featureFlagOn_returnsTrue() { + setOverride(OVERRIDE_OFF.getSetting()); + // In absence of dev options, follow flag + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue(); + } + + + @Test + @DisableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) + public void isEnabled_devOptionFlagDisabled_overrideOn_featureFlagOff_returnsFalse() { + setOverride(OVERRIDE_ON.getSetting()); + + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse(); + } + + @Test + @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) + public void isEnabled_overrideUnset_featureFlagOn_returnsTrue() { + setOverride(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting()); + + // For overridableFlag, for unset overrides, follow flag + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue(); + } + + @Test + @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) + @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + public void isEnabled_overrideUnset_featureFlagOff_returnsFalse() { + setOverride(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting()); + + // For overridableFlag, for unset overrides, follow flag + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse(); + } + + @Test + @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) + public void isEnabled_noOverride_featureFlagOn_returnsTrue() { + setOverride(null); + + // For overridableFlag, in absence of overrides, follow flag + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue(); + } + + @Test + @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) + @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + public void isEnabled_noOverride_featureFlagOff_returnsFalse() { + setOverride(null); + + // For overridableFlag, in absence of overrides, follow flag + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse(); + } + + @Test + @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) + public void isEnabled_unrecognizableOverride_featureFlagOn_returnsTrue() { + setOverride(-2); + + // For overridableFlag, for unrecognized overrides, follow flag + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue(); + } + + @Test + @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) + @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + public void isEnabled_unrecognizableOverride_featureFlagOff_returnsFalse() { + setOverride(-2); + + // For overridableFlag, for unrecognizable overrides, follow flag + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse(); + } + + @Test + @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) + public void isEnabled_overrideOff_featureFlagOn_returnsFalse() { + setOverride(OVERRIDE_OFF.getSetting()); + + // For overridableFlag, follow override if they exist + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse(); + } + + @Test + @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) + @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + public void isEnabled_overrideOn_featureFlagOff_returnsTrue() { + setOverride(OVERRIDE_ON.getSetting()); + + // For overridableFlag, follow override if they exist + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue(); + } + + @Test + @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) + public void isEnabled_overrideOffThenOn_featureFlagOn_returnsFalseAndFalse() { + setOverride(OVERRIDE_OFF.getSetting()); + + // For overridableFlag, follow override if they exist + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse(); + + setOverride(OVERRIDE_ON.getSetting()); + + // Keep overrides constant through the process + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse(); + } + + @Test + @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) + @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + public void isEnabled_overrideOnThenOff_featureFlagOff_returnsTrueAndTrue() { + setOverride(OVERRIDE_ON.getSetting()); + + // For overridableFlag, follow override if they exist + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue(); + + setOverride(OVERRIDE_OFF.getSetting()); + + // Keep overrides constant through the process + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue(); + } + + @Test + @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) + @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + public void isEnabled_noProperty_overrideOn_featureFlagOff_returnsTrueAndPropertyOn() { + System.clearProperty(SYSTEM_PROPERTY_OVERRIDE_KEY); + setOverride(OVERRIDE_ON.getSetting()); + + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue(); + // Store System Property if not present + assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY)) + .isEqualTo(String.valueOf(OVERRIDE_ON.getSetting())); + } + + @Test + @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) + public void isEnabled_noProperty_overrideUnset_featureFlagOn_returnsTrueAndPropertyUnset() { + System.clearProperty(SYSTEM_PROPERTY_OVERRIDE_KEY); + setOverride(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting()); + + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue(); + // Store System Property if not present + assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY)) + .isEqualTo(String.valueOf( + DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting())); + } + + @Test + @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) + @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + public void isEnabled_noProperty_overrideUnset_featureFlagOff_returnsFalseAndPropertyUnset() { + System.clearProperty(SYSTEM_PROPERTY_OVERRIDE_KEY); + setOverride(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting()); + + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse(); + // Store System Property if not present + assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY)) + .isEqualTo(String.valueOf( + DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting())); + } + + @Test + @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) + public void isEnabled_propertyNotInt_overrideOff_featureFlagOn_returnsFalseAndPropertyOff() { + System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, "abc"); + setOverride(OVERRIDE_OFF.getSetting()); + + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse(); + // Store System Property if currently invalid + assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY)) + .isEqualTo(String.valueOf(OVERRIDE_OFF.getSetting())); + } + + @Test + @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) + public void isEnabled_propertyInvalid_overrideOff_featureFlagOn_returnsFalseAndPropertyOff() { + System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, "-2"); + setOverride(OVERRIDE_OFF.getSetting()); + + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse(); + // Store System Property if currently invalid + assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY)) + .isEqualTo(String.valueOf(OVERRIDE_OFF.getSetting())); + } + + @Test + @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) + public void isEnabled_propertyOff_overrideOn_featureFlagOn_returnsFalseAndnoPropertyUpdate() { + System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, String.valueOf( + OVERRIDE_OFF.getSetting())); + setOverride(OVERRIDE_ON.getSetting()); + + // Have a consistent override until reboot + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse(); + assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY)) + .isEqualTo(String.valueOf(OVERRIDE_OFF.getSetting())); + } + + @Test + @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) + @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + public void isEnabled_propertyOn_overrideOff_featureFlagOff_returnsTrueAndnoPropertyUpdate() { + System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, String.valueOf(OVERRIDE_ON.getSetting())); + setOverride(OVERRIDE_OFF.getSetting()); + + // Have a consistent override until reboot + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue(); + assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY)) + .isEqualTo(String.valueOf(OVERRIDE_ON.getSetting())); + } + + @Test + @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) + public void isEnabled_propertyUnset_overrideOff_featureFlagOn_returnsTrueAndnoPropertyUpdate() { + System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, + String.valueOf(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting())); + setOverride(OVERRIDE_OFF.getSetting()); + + // Have a consistent override until reboot + assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue(); + assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY)) + .isEqualTo(String.valueOf( + DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting())); + } + + @Test + @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY}) + public void isEnabled_dwFlagOn_overrideUnset_featureFlagOn_returnsTrue() { + setOverride(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting()); + + // For unset overrides, follow flag + assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isTrue(); + } + + @Test + @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) + @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + public void isEnabled_dwFlagOn_overrideUnset_featureFlagOff_returnsFalse() { + setOverride(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting()); + // For unset overrides, follow flag + assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isFalse(); + } + + @Test + @EnableFlags({ + FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, + FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY + }) + public void isEnabled_dwFlagOn_overrideOn_featureFlagOn_returnsTrue() { + setOverride(OVERRIDE_ON.getSetting()); + + // When toggle override matches its default state (dw flag), don't override flags + assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isTrue(); + } + + @Test + @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) + @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + public void isEnabled_dwFlagOn_overrideOn_featureFlagOff_returnsFalse() { + setOverride(OVERRIDE_ON.getSetting()); + + // When toggle override matches its default state (dw flag), don't override flags + assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isFalse(); + } + + @Test + @EnableFlags({ + FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, + FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY + }) + public void isEnabled_dwFlagOn_overrideOff_featureFlagOn_returnsFalse() { + setOverride(OVERRIDE_OFF.getSetting()); + + // Follow override if they exist, and is not equal to default toggle state (dw flag) + assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isFalse(); + } + + @Test + @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE}) + @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + public void isEnabled_dwFlagOn_overrideOff_featureFlagOff_returnsFalse() { + setOverride(OVERRIDE_OFF.getSetting()); + + // Follow override if they exist, and is not equal to default toggle state (dw flag) + assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isFalse(); + } + + @Test + @EnableFlags({ + FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, + FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY + }) + @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + public void isEnabled_dwFlagOff_overrideUnset_featureFlagOn_returnsTrue() { + setOverride(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting()); + + // For unset overrides, follow flag + assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isTrue(); + } + + @Test + @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) + @DisableFlags({ + FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY + }) + public void isEnabled_dwFlagOff_overrideUnset_featureFlagOff_returnsFalse() { + setOverride(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting()); + + // For unset overrides, follow flag + assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isFalse(); + } + + @Test + @EnableFlags({ + FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, + FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY + }) + @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + public void isEnabled_dwFlagOff_overrideOn_featureFlagOn_returnsTrue() { + setOverride(OVERRIDE_ON.getSetting()); + + // Follow override if they exist, and is not equal to default toggle state (dw flag) + assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isTrue(); + } + + @Test + @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) + @DisableFlags({ + FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY + }) + public void isEnabled_dwFlagOff_overrideOn_featureFlagOff_returnTrue() { + setOverride(OVERRIDE_ON.getSetting()); + + // Follow override if they exist, and is not equal to default toggle state (dw flag) + assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isTrue(); + } + + @Test + @EnableFlags({ + FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, + FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY + }) + @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + public void isEnabled_dwFlagOff_overrideOff_featureFlagOn_returnsTrue() { + setOverride(OVERRIDE_OFF.getSetting()); + + // When toggle override matches its default state (dw flag), don't override flags + assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isTrue(); + } + + @Test + @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) + @DisableFlags({ + FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY + }) + public void isEnabled_dwFlagOff_overrideOff_featureFlagOff_returnsFalse() { + setOverride(OVERRIDE_OFF.getSetting()); + + // When toggle override matches its default state (dw flag), don't override flags + assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isFalse(); + } + + private void setOverride(Integer setting) { + ContentResolver contentResolver = mContext.getContentResolver(); + String key = Settings.Global.DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES; + + if (setting == null) { + Settings.Global.putString(contentResolver, key, null); + } else { + Settings.Global.putInt(contentResolver, key, setting); + } + } + + private void resetCache() throws Exception { + Field cachedToggleOverride = DesktopModeFlagsUtil.class.getDeclaredField( + "sCachedToggleOverride"); + cachedToggleOverride.setAccessible(true); + cachedToggleOverride.set(null, null); + + // Clear override cache stored in System property + System.clearProperty(SYSTEM_PROPERTY_OVERRIDE_KEY); + } +} diff --git a/services/usb/java/com/android/server/usb/UsbDeviceManager.java b/services/usb/java/com/android/server/usb/UsbDeviceManager.java index 175a09db54e3..14044135eca7 100644 --- a/services/usb/java/com/android/server/usb/UsbDeviceManager.java +++ b/services/usb/java/com/android/server/usb/UsbDeviceManager.java @@ -16,6 +16,8 @@ package com.android.server.usb; +import com.android.internal.annotations.Keep; + import static android.hardware.usb.UsbPortStatus.DATA_ROLE_DEVICE; import static android.hardware.usb.UsbPortStatus.DATA_ROLE_HOST; import static android.hardware.usb.UsbPortStatus.MODE_AUDIO_ACCESSORY; @@ -82,6 +84,7 @@ import android.util.Pair; import android.util.Slog; import android.text.TextUtils; +import com.android.internal.R; import com.android.internal.annotations.GuardedBy; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; @@ -147,6 +150,8 @@ public class UsbDeviceManager implements ActivityTaskManagerInternal.ScreenObser "DEVPATH=/devices/virtual/android_usb/android0"; private static final String ACCESSORY_START_MATCH = "DEVPATH=/devices/virtual/misc/usb_accessory"; + private static final String UDC_SUBSYS_MATCH = + "SUBSYSTEM=udc"; private static final String FUNCTIONS_PATH = "/sys/class/android_usb/android0/functions"; private static final String STATE_PATH = @@ -226,6 +231,9 @@ public class UsbDeviceManager implements ActivityTaskManagerInternal.ScreenObser private static UsbGadgetHal mUsbGadgetHal; + private final boolean mEnableUdcSysfsUsbStateUpdate; + private String mUdcName = ""; + /** * Counter for tracking UsbOperation operations. */ @@ -260,12 +268,9 @@ public class UsbDeviceManager implements ActivityTaskManagerInternal.ScreenObser if (DEBUG) Slog.d(TAG, "sEventLogger == null"); } - String state = event.get("USB_STATE"); String accessory = event.get("ACCESSORY"); - if (state != null) { - mHandler.updateState(state); - } else if ("GETPROTOCOL".equals(accessory)) { + if ("GETPROTOCOL".equals(accessory)) { if (DEBUG) Slog.d(TAG, "got accessory get protocol"); mHandler.setAccessoryUEventTime(SystemClock.elapsedRealtime()); resetAccessoryHandshakeTimeoutHandler(); @@ -279,6 +284,24 @@ public class UsbDeviceManager implements ActivityTaskManagerInternal.ScreenObser mHandler.setStartAccessoryTrue(); startAccessoryMode(); } + + if (mEnableUdcSysfsUsbStateUpdate) { + if (!mUdcName.isEmpty() + && "udc".equals(event.get("SUBSYSTEM")) + && event.get("DEVPATH").contains(mUdcName)) { + String action = event.get("ACTION"); + if ("add".equals(action)) { + nativeStartGadgetMonitor(mUdcName); + } else if ("remove".equals(action)) { + nativeStopGadgetMonitor(); + } + } + } else { + String state = event.get("USB_STATE"); + if (state != null) { + mHandler.updateState(state); + } + } } } @@ -406,9 +429,28 @@ public class UsbDeviceManager implements ActivityTaskManagerInternal.ScreenObser // Watch for USB configuration changes mUEventObserver = new UsbUEventObserver(); - mUEventObserver.startObserving(USB_STATE_MATCH); mUEventObserver.startObserving(ACCESSORY_START_MATCH); + mEnableUdcSysfsUsbStateUpdate = + android.hardware.usb.flags.Flags.enableUdcSysfsUsbStateUpdate() + && context.getResources().getBoolean(R.bool.config_enableUdcSysfsUsbStateUpdate); + + if (mEnableUdcSysfsUsbStateUpdate) { + mUEventObserver.startObserving(UDC_SUBSYS_MATCH); + new Thread("GetUsbControllerSysprop") { + public void run() { + String udcName; + // blocking wait until usb controller sysprop is available + udcName = nativeWaitAndGetProperty(USB_CONTROLLER_NAME_PROPERTY); + nativeStartGadgetMonitor(udcName); + mUdcName = udcName; + Slog.v(TAG, "USB controller name " + udcName); + } + }.start(); + } else { + mUEventObserver.startObserving(USB_STATE_MATCH); + } + sEventLogger = new EventLogger(DUMPSYS_LOG_BUFFER, "UsbDeviceManager activity"); } @@ -2609,11 +2651,27 @@ public class UsbDeviceManager implements ActivityTaskManagerInternal.ScreenObser dump.end(token); } + /** + * Update usb state (Called by native code). + */ + @Keep + private void updateGadgetState(String state) { + Slog.d(TAG, "Usb state update " + state); + + mHandler.updateState(state); + } + private native String[] nativeGetAccessoryStrings(); private native ParcelFileDescriptor nativeOpenAccessory(); + private native String nativeWaitAndGetProperty(String propName); + private native FileDescriptor nativeOpenControl(String usbFunction); private native boolean nativeIsStartRequested(); + + private native boolean nativeStartGadgetMonitor(String udcName); + + private native void nativeStopGadgetMonitor(); } diff --git a/telephony/OWNERS b/telephony/OWNERS index 7607c64150d8..92af034217a9 100644 --- a/telephony/OWNERS +++ b/telephony/OWNERS @@ -15,4 +15,4 @@ per-file CarrierConfigManager.java=set noparent per-file CarrierConfigManager.java=amruthr@google.com,tgunn@google.com,rgreenwalt@google.com,satk@google.com #Domain Selection is jointly owned, add additional owners for domain selection specific files -per-file TransportSelectorCallback.java,WwanSelectorCallback.java,DomainSelectionService.java,DomainSelectionService.aidl,DomainSelector.java,EmergencyRegResult.java,EmergencyRegResult.aidl,IDomainSelectionServiceController.aidl,IDomainSelector.aidl,ITransportSelectorCallback.aidl,ITransportSelectorResultCallback.aidl,IWwanSelectorCallback.aidl,IWwanSelectorResultCallback.aidl=hwangoo@google.com,forestchoi@google.com,avinashmp@google.com,mkoon@google.com,seheele@google.com,radhikaagrawal@google.com,jdyou@google.com +per-file TransportSelectorCallback.java,WwanSelectorCallback.java,DomainSelectionService.java,DomainSelectionService.aidl,DomainSelector.java,EmergencyRegResult.java,EmergencyRegResult.aidl,IDomainSelectionServiceController.aidl,IDomainSelector.aidl,ITransportSelectorCallback.aidl,ITransportSelectorResultCallback.aidl,IWwanSelectorCallback.aidl,IWwanSelectorResultCallback.aidl=hwangoo@google.com,jaesikkong@google.com,avinashmp@google.com,mkoon@google.com,seheele@google.com,radhikaagrawal@google.com,jdyou@google.com diff --git a/telephony/java/android/telephony/satellite/stub/ProvisionSubscriberId.aidl b/telephony/java/android/telephony/satellite/stub/ProvisionSubscriberId.aidl index 2dc8ffba6e96..460de8c8113d 100644 --- a/telephony/java/android/telephony/satellite/stub/ProvisionSubscriberId.aidl +++ b/telephony/java/android/telephony/satellite/stub/ProvisionSubscriberId.aidl @@ -25,4 +25,7 @@ parcelable ProvisionSubscriberId { /** carrier id */ int mCarrierId; + + /** apn */ + String mNiddApn; } diff --git a/tests/BatteryStatsPerfTest/src/com/android/internal/os/BatteryUsageStatsPerfTest.java b/tests/BatteryStatsPerfTest/src/com/android/internal/os/BatteryUsageStatsPerfTest.java index 08430f2f2744..4143f595f9a0 100644 --- a/tests/BatteryStatsPerfTest/src/com/android/internal/os/BatteryUsageStatsPerfTest.java +++ b/tests/BatteryStatsPerfTest/src/com/android/internal/os/BatteryUsageStatsPerfTest.java @@ -159,7 +159,7 @@ public class BatteryUsageStatsPerfTest { private static BatteryUsageStats buildBatteryUsageStats() { final BatteryUsageStats.Builder builder = - new BatteryUsageStats.Builder(new String[]{"FOO"}, true, false, 0) + new BatteryUsageStats.Builder(new String[]{"FOO"}, true, false, false, false, 0) .setBatteryCapacity(4000) .setDischargePercentage(20) .setDischargedPowerRange(1000, 2000) diff --git a/tests/Internal/src/com/android/internal/protolog/ProtologDataSourceTest.java b/tests/Internal/src/com/android/internal/protolog/ProtologDataSourceTest.java index be9fb1b309f6..9a062e3b2f80 100644 --- a/tests/Internal/src/com/android/internal/protolog/ProtologDataSourceTest.java +++ b/tests/Internal/src/com/android/internal/protolog/ProtologDataSourceTest.java @@ -67,7 +67,7 @@ public class ProtologDataSourceTest { @Test public void allEnabledTraceMode() { - final ProtoLogDataSource ds = new ProtoLogDataSource((c) -> {}, () -> {}, (c) -> {}); + final ProtoLogDataSource ds = new ProtoLogDataSource((idx, c) -> {}, () -> {}, (idx, c) -> {}); final ProtoLogDataSource.TlsState tlsState = createTlsState( DataSourceConfigOuterClass.DataSourceConfig.newBuilder().setProtologConfig( @@ -154,7 +154,7 @@ public class ProtologDataSourceTest { private ProtoLogDataSource.TlsState createTlsState( DataSourceConfigOuterClass.DataSourceConfig config) { final ProtoLogDataSource ds = - Mockito.spy(new ProtoLogDataSource((c) -> {}, () -> {}, (c) -> {})); + Mockito.spy(new ProtoLogDataSource((idx, c) -> {}, () -> {}, (idx, c) -> {})); ProtoInputStream configStream = new ProtoInputStream(config.toByteArray()); final ProtoLogDataSource.Instance dsInstance = Mockito.spy( diff --git a/tools/aapt2/cmd/Util.cpp b/tools/aapt2/cmd/Util.cpp index e839fc1ceb0f..7739171b347f 100644 --- a/tools/aapt2/cmd/Util.cpp +++ b/tools/aapt2/cmd/Util.cpp @@ -137,22 +137,25 @@ bool ParseFeatureFlagsParameter(StringPiece arg, android::IDiagnostics* diag, diag->Error(android::DiagMessage() << "No name given for one or more flags in: " << arg); return false; } + std::vector<std::string> name_parts = util::Split(flag_name, ':'); if (name_parts.size() > 2) { diag->Error(android::DiagMessage() << "Invalid feature flag and optional value '" << flag_and_value - << "'. Must be in the format 'flag_name[:ro][=true|false]"); + << "'. Must be in the format 'flag_name[:READ_ONLY|READ_WRITE][=true|false]"); return false; } flag_name = name_parts[0]; bool read_only = false; if (name_parts.size() == 2) { - if (name_parts[1] == "ro") { + if (name_parts[1] == "ro" || name_parts[1] == "READ_ONLY") { read_only = true; + } else if (name_parts[1] == "READ_WRITE") { + read_only = false; } else { diag->Error(android::DiagMessage() << "Invalid feature flag and optional value '" << flag_and_value - << "'. Must be in the format 'flag_name[:ro][=true|false]"); + << "'. Must be in the format 'flag_name[:READ_ONLY|READ_WRITE][=true|false]"); return false; } } diff --git a/tools/aapt2/cmd/Util_test.cpp b/tools/aapt2/cmd/Util_test.cpp index 35bc63714e58..78183409ad8f 100644 --- a/tools/aapt2/cmd/Util_test.cpp +++ b/tools/aapt2/cmd/Util_test.cpp @@ -383,7 +383,7 @@ TEST(UtilTest, ParseFeatureFlagsParameter_InvalidValue) { TEST(UtilTest, ParseFeatureFlagsParameter_DuplicateFlag) { auto diagnostics = test::ContextBuilder().Build()->GetDiagnostics(); FeatureFlagValues feature_flag_values; - ASSERT_TRUE(ParseFeatureFlagsParameter("foo=true,bar=true,foo:ro=false", diagnostics, + ASSERT_TRUE(ParseFeatureFlagsParameter("foo=true,bar:READ_WRITE=true,foo:ro=false", diagnostics, &feature_flag_values)); EXPECT_THAT( feature_flag_values, @@ -394,11 +394,11 @@ TEST(UtilTest, ParseFeatureFlagsParameter_DuplicateFlag) { TEST(UtilTest, ParseFeatureFlagsParameter_Valid) { auto diagnostics = test::ContextBuilder().Build()->GetDiagnostics(); FeatureFlagValues feature_flag_values; - ASSERT_TRUE(ParseFeatureFlagsParameter("foo= true, bar:ro =FALSE,baz=, quux", diagnostics, - &feature_flag_values)); + ASSERT_TRUE(ParseFeatureFlagsParameter("foo:READ_ONLY= true, bar:ro =FALSE,baz:READ_WRITE=, quux", + diagnostics, &feature_flag_values)); EXPECT_THAT( feature_flag_values, - UnorderedElementsAre(Pair("foo", FeatureFlagProperties{false, std::optional<bool>(true)}), + UnorderedElementsAre(Pair("foo", FeatureFlagProperties{true, std::optional<bool>(true)}), Pair("bar", FeatureFlagProperties{true, std::optional<bool>(false)}), Pair("baz", FeatureFlagProperties{false, std::nullopt}), Pair("quux", FeatureFlagProperties{false, std::nullopt}))); diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AppInfoFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AppInfoFactory.java index 277a508ced57..5ecf5cf0b723 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AppInfoFactory.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AppInfoFactory.java @@ -115,6 +115,9 @@ public class AppInfoFactory implements AslMarshallableFactory<AppInfo> { /** Creates a {@link AppInfo} from the human-readable DOM element. */ public AppInfo createFromHrElement(Element appInfoEle, long version) throws MalformedXmlException { + if (appInfoEle == null) { + return null; + } XmlUtils.throwIfExtraneousAttributes( appInfoEle, XmlUtils.getMostRecentVersion(mRecognizedHrAttrs, version)); XmlUtils.throwIfExtraneousChildrenHr( @@ -184,6 +187,9 @@ public class AppInfoFactory implements AslMarshallableFactory<AppInfo> { /** Creates an {@link AslMarshallableFactory} from on-device DOM elements */ public AppInfo createFromOdElement(Element appInfoEle, long version) throws MalformedXmlException { + if (appInfoEle == null) { + return null; + } XmlUtils.throwIfExtraneousChildrenOd( appInfoEle, XmlUtils.getMostRecentVersion(mRecognizedOdEleNames, version)); var requiredOdEles = XmlUtils.getMostRecentVersion(mRequiredOdEles, version); diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/AllTests.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/AllTests.java index 14e65e5e5b2b..e3aa50a4cee2 100644 --- a/tools/app_metadata_bundles/src/test/java/com/android/asllib/AllTests.java +++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/AllTests.java @@ -20,8 +20,11 @@ import com.android.asllib.marshallable.AndroidSafetyLabelTest; import com.android.asllib.marshallable.AppInfoTest; import com.android.asllib.marshallable.DataLabelsTest; import com.android.asllib.marshallable.DataTypeEqualityTest; +import com.android.asllib.marshallable.DeveloperInfoTest; import com.android.asllib.marshallable.SafetyLabelsTest; +import com.android.asllib.marshallable.SecurityLabelsTest; import com.android.asllib.marshallable.SystemAppSafetyLabelTest; +import com.android.asllib.marshallable.ThirdPartyVerificationTest; import com.android.asllib.marshallable.TransparencyInfoTest; import org.junit.runner.RunWith; @@ -36,6 +39,9 @@ import org.junit.runners.Suite; DataTypeEqualityTest.class, SafetyLabelsTest.class, SystemAppSafetyLabelTest.class, - TransparencyInfoTest.class + TransparencyInfoTest.class, + DeveloperInfoTest.class, + SecurityLabelsTest.class, + ThirdPartyVerificationTest.class }) public class AllTests {} diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/AndroidSafetyLabelTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/AndroidSafetyLabelTest.java index 283ccbc44791..6470c060af87 100644 --- a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/AndroidSafetyLabelTest.java +++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/AndroidSafetyLabelTest.java @@ -28,6 +28,7 @@ import org.junit.runners.JUnit4; import org.w3c.dom.Element; import java.nio.file.Paths; +import java.util.List; @RunWith(JUnit4.class) public class AndroidSafetyLabelTest { @@ -37,12 +38,16 @@ public class AndroidSafetyLabelTest { "com/android/asllib/androidsafetylabel/od"; private static final String MISSING_VERSION_FILE_NAME = "missing-version.xml"; - private static final String VALID_EMPTY_FILE_NAME = "valid-empty.xml"; + private static final String VALID_V2_FILE_NAME = "valid-empty.xml"; + private static final String VALID_V1_FILE_NAME = "valid-v1.xml"; private static final String WITH_SAFETY_LABELS_FILE_NAME = "with-safety-labels.xml"; private static final String WITH_SYSTEM_APP_SAFETY_LABEL_FILE_NAME = "with-system-app-safety-label.xml"; private static final String WITH_TRANSPARENCY_INFO_FILE_NAME = "with-transparency-info.xml"; + public static final List<String> REQUIRED_FIELD_NAMES_OD_V2 = + List.of("system_app_safety_label", "transparency_info"); + @Before public void setUp() throws Exception { System.out.println("set up."); @@ -56,12 +61,12 @@ public class AndroidSafetyLabelTest { odToHrExpectException(MISSING_VERSION_FILE_NAME); } - /** Test for android safety label valid empty. */ + /** Test for android safety label valid v2. */ @Test - public void testAndroidSafetyLabelValidEmptyFile() throws Exception { - System.out.println("starting testAndroidSafetyLabelValidEmptyFile."); - testHrToOdAndroidSafetyLabel(VALID_EMPTY_FILE_NAME); - testOdToHrAndroidSafetyLabel(VALID_EMPTY_FILE_NAME); + public void testAndroidSafetyLabelValidV2File() throws Exception { + System.out.println("starting testAndroidSafetyLabelValidV2File."); + testHrToOdAndroidSafetyLabel(VALID_V2_FILE_NAME); + testOdToHrAndroidSafetyLabel(VALID_V2_FILE_NAME); } /** Test for android safety label with safety labels. */ @@ -72,6 +77,34 @@ public class AndroidSafetyLabelTest { testOdToHrAndroidSafetyLabel(WITH_SAFETY_LABELS_FILE_NAME); } + /** Tests missing required fields fails, V2. */ + @Test + public void testMissingRequiredFieldsOdV2() throws Exception { + for (String reqField : REQUIRED_FIELD_NAMES_OD_V2) { + System.out.println("testing missing required field od v2: " + reqField); + var ele = + TestUtils.getElementFromResource( + Paths.get(ANDROID_SAFETY_LABEL_OD_PATH, VALID_V2_FILE_NAME)); + TestUtils.removeOdChildEleWithName(ele, reqField); + assertThrows( + MalformedXmlException.class, + () -> new AndroidSafetyLabelFactory().createFromOdElement(ele)); + } + } + + /** Tests missing optional fields succeeds, V1. */ + @Test + public void testMissingOptionalFieldsOdV1() throws Exception { + for (String reqField : REQUIRED_FIELD_NAMES_OD_V2) { + System.out.println("testing missing optional field od v1: " + reqField); + var ele = + TestUtils.getElementFromResource( + Paths.get(ANDROID_SAFETY_LABEL_OD_PATH, VALID_V1_FILE_NAME)); + TestUtils.removeOdChildEleWithName(ele, reqField); + var unused = new AndroidSafetyLabelFactory().createFromOdElement(ele); + } + } + private void hrToOdExpectException(String fileName) { assertThrows( MalformedXmlException.class, diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DataLabelsTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DataLabelsTest.java index b557fea9572b..cc58a61760f4 100644 --- a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DataLabelsTest.java +++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DataLabelsTest.java @@ -35,8 +35,6 @@ import javax.xml.parsers.ParserConfigurationException; @RunWith(JUnit4.class) public class DataLabelsTest { - private static final long DEFAULT_VERSION = 2L; - private static final String DATA_LABELS_HR_PATH = "com/android/asllib/datalabels/hr"; private static final String DATA_LABELS_OD_PATH = "com/android/asllib/datalabels/od"; diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DeveloperInfoTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DeveloperInfoTest.java new file mode 100644 index 000000000000..a4472b1b78e5 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DeveloperInfoTest.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.asllib.marshallable; + +import static org.junit.Assert.assertThrows; + +import com.android.asllib.testutils.TestUtils; +import com.android.asllib.util.MalformedXmlException; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.w3c.dom.Element; + +import java.nio.file.Paths; +import java.util.List; + +@RunWith(JUnit4.class) +public class DeveloperInfoTest { + private static final String DEVELOPER_INFO_HR_PATH = "com/android/asllib/developerinfo/hr"; + private static final String DEVELOPER_INFO_OD_PATH = "com/android/asllib/developerinfo/od"; + public static final List<String> REQUIRED_FIELD_NAMES = + List.of("address", "countryRegion", "email", "name", "relationship"); + public static final List<String> REQUIRED_FIELD_NAMES_OD = + List.of("address", "country_region", "email", "name", "relationship"); + public static final List<String> OPTIONAL_FIELD_NAMES = List.of("website", "registryId"); + public static final List<String> OPTIONAL_FIELD_NAMES_OD = + List.of("website", "app_developer_registry_id"); + + private static final String ALL_FIELDS_VALID_FILE_NAME = "all-fields-valid.xml"; + + /** Logic for setting up tests (empty if not yet needed). */ + public static void main(String[] params) throws Exception {} + + @Before + public void setUp() throws Exception { + System.out.println("set up."); + } + + /** Test for all fields valid. */ + @Test + public void testAllFieldsValid() throws Exception { + System.out.println("starting testAllFieldsValid."); + testHrToOdDeveloperInfo(ALL_FIELDS_VALID_FILE_NAME); + testOdToHrDeveloperInfo(ALL_FIELDS_VALID_FILE_NAME); + } + + /** Tests missing required fields fails. */ + @Test + public void testMissingRequiredFields() throws Exception { + System.out.println("Starting testMissingRequiredFields"); + for (String reqField : REQUIRED_FIELD_NAMES) { + System.out.println("testing missing required field: " + reqField); + var developerInfoEle = + TestUtils.getElementFromResource( + Paths.get(DEVELOPER_INFO_HR_PATH, ALL_FIELDS_VALID_FILE_NAME)); + developerInfoEle.removeAttribute(reqField); + + assertThrows( + MalformedXmlException.class, + () -> new DeveloperInfoFactory().createFromHrElement(developerInfoEle)); + } + + for (String reqField : REQUIRED_FIELD_NAMES_OD) { + System.out.println("testing missing required field od: " + reqField); + var developerInfoEle = + TestUtils.getElementFromResource( + Paths.get(DEVELOPER_INFO_OD_PATH, ALL_FIELDS_VALID_FILE_NAME)); + TestUtils.removeOdChildEleWithName(developerInfoEle, reqField); + + assertThrows( + MalformedXmlException.class, + () -> new DeveloperInfoFactory().createFromOdElement(developerInfoEle)); + } + } + + /** Tests missing optional fields passes. */ + @Test + public void testMissingOptionalFields() throws Exception { + for (String optField : OPTIONAL_FIELD_NAMES) { + var developerInfoEle = + TestUtils.getElementFromResource( + Paths.get(DEVELOPER_INFO_HR_PATH, ALL_FIELDS_VALID_FILE_NAME)); + developerInfoEle.removeAttribute(optField); + DeveloperInfo developerInfo = + new DeveloperInfoFactory().createFromHrElement(developerInfoEle); + developerInfo.toOdDomElement(TestUtils.document()); + } + + for (String optField : OPTIONAL_FIELD_NAMES_OD) { + var developerInfoEle = + TestUtils.getElementFromResource( + Paths.get(DEVELOPER_INFO_OD_PATH, ALL_FIELDS_VALID_FILE_NAME)); + TestUtils.removeOdChildEleWithName(developerInfoEle, optField); + DeveloperInfo developerInfo = + new DeveloperInfoFactory().createFromOdElement(developerInfoEle); + developerInfo.toHrDomElement(TestUtils.document()); + } + } + + private void testHrToOdDeveloperInfo(String fileName) throws Exception { + var doc = TestUtils.document(); + DeveloperInfo developerInfo = + new DeveloperInfoFactory() + .createFromHrElement( + TestUtils.getElementFromResource( + Paths.get(DEVELOPER_INFO_HR_PATH, fileName))); + Element developerInfoEle = developerInfo.toOdDomElement(doc); + doc.appendChild(developerInfoEle); + TestUtils.testFormatToFormat(doc, Paths.get(DEVELOPER_INFO_OD_PATH, fileName)); + } + + private void testOdToHrDeveloperInfo(String fileName) throws Exception { + var doc = TestUtils.document(); + DeveloperInfo developerInfo = + new DeveloperInfoFactory() + .createFromOdElement( + TestUtils.getElementFromResource( + Paths.get(DEVELOPER_INFO_OD_PATH, fileName))); + Element developerInfoEle = developerInfo.toHrDomElement(doc); + doc.appendChild(developerInfoEle); + TestUtils.testFormatToFormat(doc, Paths.get(DEVELOPER_INFO_HR_PATH, fileName)); + } +} diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SafetyLabelsTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SafetyLabelsTest.java index 7cd510f0ddfc..fc8ff00794ad 100644 --- a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SafetyLabelsTest.java +++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SafetyLabelsTest.java @@ -26,13 +26,9 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.w3c.dom.Element; -import org.xml.sax.SAXException; -import java.io.IOException; import java.nio.file.Paths; -import javax.xml.parsers.ParserConfigurationException; - @RunWith(JUnit4.class) public class SafetyLabelsTest { private static final long DEFAULT_VERSION = 2L; @@ -42,6 +38,8 @@ public class SafetyLabelsTest { private static final String VALID_EMPTY_FILE_NAME = "valid-empty.xml"; private static final String WITH_DATA_LABELS_FILE_NAME = "with-data-labels.xml"; + private static final String VALID_V1_FILE_NAME = "valid-v1.xml"; + private static final String UNRECOGNIZED_FIELD_V2_FILE_NAME = "unrecognized-field-v2.xml"; @Before public void setUp() throws Exception { @@ -52,61 +50,59 @@ public class SafetyLabelsTest { @Test public void testSafetyLabelsValidEmptyFile() throws Exception { System.out.println("starting testSafetyLabelsValidEmptyFile."); - testHrToOdSafetyLabels(VALID_EMPTY_FILE_NAME); - testOdToHrSafetyLabels(VALID_EMPTY_FILE_NAME); + testHrToOdSafetyLabels(VALID_EMPTY_FILE_NAME, DEFAULT_VERSION); + testOdToHrSafetyLabels(VALID_EMPTY_FILE_NAME, DEFAULT_VERSION); } /** Test for safety labels with data labels. */ @Test public void testSafetyLabelsWithDataLabels() throws Exception { System.out.println("starting testSafetyLabelsWithDataLabels."); - testHrToOdSafetyLabels(WITH_DATA_LABELS_FILE_NAME); - testOdToHrSafetyLabels(WITH_DATA_LABELS_FILE_NAME); + testHrToOdSafetyLabels(WITH_DATA_LABELS_FILE_NAME, DEFAULT_VERSION); + testOdToHrSafetyLabels(WITH_DATA_LABELS_FILE_NAME, DEFAULT_VERSION); } - private void hrToOdExpectException(String fileName) - throws ParserConfigurationException, IOException, SAXException { - var safetyLabelsEle = - TestUtils.getElementFromResource(Paths.get(SAFETY_LABELS_HR_PATH, fileName)); - assertThrows( - MalformedXmlException.class, - () -> - new SafetyLabelsFactory() - .createFromHrElement(safetyLabelsEle, DEFAULT_VERSION)); + /** Tests valid fields v1. */ + @Test + public void testValidFieldsV1() throws Exception { + var ele = + TestUtils.getElementFromResource( + Paths.get(SAFETY_LABELS_OD_PATH, VALID_V1_FILE_NAME)); + var unused = new SafetyLabelsFactory().createFromOdElement(ele, 1L); } - private void odToHrExpectException(String fileName) - throws ParserConfigurationException, IOException, SAXException { - var safetyLabelsEle = - TestUtils.getElementFromResource(Paths.get(SAFETY_LABELS_OD_PATH, fileName)); + /** Tests unrecognized field v2. */ + @Test + public void testUnrecognizedFieldV2() throws Exception { + var ele = + TestUtils.getElementFromResource( + Paths.get(SAFETY_LABELS_OD_PATH, VALID_V1_FILE_NAME)); assertThrows( MalformedXmlException.class, - () -> - new SafetyLabelsFactory() - .createFromOdElement(safetyLabelsEle, DEFAULT_VERSION)); + () -> new SafetyLabelsFactory().createFromOdElement(ele, 2L)); } - private void testHrToOdSafetyLabels(String fileName) throws Exception { + private void testHrToOdSafetyLabels(String fileName, long version) throws Exception { var doc = TestUtils.document(); SafetyLabels safetyLabels = new SafetyLabelsFactory() .createFromHrElement( TestUtils.getElementFromResource( Paths.get(SAFETY_LABELS_HR_PATH, fileName)), - DEFAULT_VERSION); + version); Element appInfoEle = safetyLabels.toOdDomElement(doc); doc.appendChild(appInfoEle); TestUtils.testFormatToFormat(doc, Paths.get(SAFETY_LABELS_OD_PATH, fileName)); } - private void testOdToHrSafetyLabels(String fileName) throws Exception { + private void testOdToHrSafetyLabels(String fileName, long version) throws Exception { var doc = TestUtils.document(); SafetyLabels safetyLabels = new SafetyLabelsFactory() .createFromOdElement( TestUtils.getElementFromResource( Paths.get(SAFETY_LABELS_OD_PATH, fileName)), - DEFAULT_VERSION); + version); Element appInfoEle = safetyLabels.toHrDomElement(doc); doc.appendChild(appInfoEle); TestUtils.testFormatToFormat(doc, Paths.get(SAFETY_LABELS_HR_PATH, fileName)); diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SecurityLabelsTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SecurityLabelsTest.java new file mode 100644 index 000000000000..9d197a2cf7f5 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SecurityLabelsTest.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.asllib.marshallable; + +import com.android.asllib.testutils.TestUtils; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.w3c.dom.Element; + +import java.nio.file.Paths; +import java.util.List; + +@RunWith(JUnit4.class) +public class SecurityLabelsTest { + private static final String SECURITY_LABELS_HR_PATH = "com/android/asllib/securitylabels/hr"; + private static final String SECURITY_LABELS_OD_PATH = "com/android/asllib/securitylabels/od"; + + public static final List<String> OPTIONAL_FIELD_NAMES = + List.of("isDataDeletable", "isDataEncrypted"); + public static final List<String> OPTIONAL_FIELD_NAMES_OD = + List.of("is_data_deletable", "is_data_encrypted"); + + private static final String ALL_FIELDS_VALID_FILE_NAME = "all-fields-valid.xml"; + + /** Logic for setting up tests (empty if not yet needed). */ + public static void main(String[] params) throws Exception {} + + @Before + public void setUp() throws Exception { + System.out.println("set up."); + } + + /** Test for all fields valid. */ + @Test + public void testAllFieldsValid() throws Exception { + System.out.println("starting testAllFieldsValid."); + testHrToOdSecurityLabels(ALL_FIELDS_VALID_FILE_NAME); + testOdToHrSecurityLabels(ALL_FIELDS_VALID_FILE_NAME); + } + + /** Tests missing optional fields passes. */ + @Test + public void testMissingOptionalFields() throws Exception { + for (String optField : OPTIONAL_FIELD_NAMES) { + var ele = + TestUtils.getElementFromResource( + Paths.get(SECURITY_LABELS_HR_PATH, ALL_FIELDS_VALID_FILE_NAME)); + ele.removeAttribute(optField); + SecurityLabels securityLabels = new SecurityLabelsFactory().createFromHrElement(ele); + securityLabels.toOdDomElement(TestUtils.document()); + } + for (String optField : OPTIONAL_FIELD_NAMES_OD) { + var ele = + TestUtils.getElementFromResource( + Paths.get(SECURITY_LABELS_OD_PATH, ALL_FIELDS_VALID_FILE_NAME)); + TestUtils.removeOdChildEleWithName(ele, optField); + SecurityLabels securityLabels = new SecurityLabelsFactory().createFromOdElement(ele); + securityLabels.toHrDomElement(TestUtils.document()); + } + } + + private void testHrToOdSecurityLabels(String fileName) throws Exception { + var doc = TestUtils.document(); + SecurityLabels securityLabels = + new SecurityLabelsFactory() + .createFromHrElement( + TestUtils.getElementFromResource( + Paths.get(SECURITY_LABELS_HR_PATH, fileName))); + Element ele = securityLabels.toOdDomElement(doc); + doc.appendChild(ele); + TestUtils.testFormatToFormat(doc, Paths.get(SECURITY_LABELS_OD_PATH, fileName)); + } + + private void testOdToHrSecurityLabels(String fileName) throws Exception { + var doc = TestUtils.document(); + SecurityLabels securityLabels = + new SecurityLabelsFactory() + .createFromOdElement( + TestUtils.getElementFromResource( + Paths.get(SECURITY_LABELS_OD_PATH, fileName))); + Element ele = securityLabels.toHrDomElement(doc); + doc.appendChild(ele); + TestUtils.testFormatToFormat(doc, Paths.get(SECURITY_LABELS_HR_PATH, fileName)); + } +} diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SystemAppSafetyLabelTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SystemAppSafetyLabelTest.java index 9dcc6529969e..04bcd783a1dd 100644 --- a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SystemAppSafetyLabelTest.java +++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SystemAppSafetyLabelTest.java @@ -43,6 +43,7 @@ public class SystemAppSafetyLabelTest { "com/android/asllib/systemappsafetylabel/od"; private static final String VALID_FILE_NAME = "valid.xml"; + private static final String VALID_V1_FILE_NAME = "valid-v1.xml"; private static final String MISSING_BOOL_FILE_NAME = "missing-bool.xml"; /** Logic for setting up tests (empty if not yet needed). */ @@ -57,59 +58,81 @@ public class SystemAppSafetyLabelTest { @Test public void testValid() throws Exception { System.out.println("starting testValid."); - testHrToOdSystemAppSafetyLabel(VALID_FILE_NAME); - testOdToHrSystemAppSafetyLabel(VALID_FILE_NAME); + testHrToOdSystemAppSafetyLabel(VALID_FILE_NAME, DEFAULT_VERSION); + testOdToHrSystemAppSafetyLabel(VALID_FILE_NAME, DEFAULT_VERSION); + } + + /** Test for valid v1. */ + @Test + public void testValidV1() throws Exception { + System.out.println("starting testValidV1."); + var doc = TestUtils.document(); + var unused = + new SystemAppSafetyLabelFactory() + .createFromOdElement( + TestUtils.getElementFromResource( + Paths.get( + SYSTEM_APP_SAFETY_LABEL_OD_PATH, + VALID_V1_FILE_NAME)), + 1L); + } + + /** Test for testV1InvalidAsV2. */ + @Test + public void testV1InvalidAsV2() throws Exception { + System.out.println("starting testV1InvalidAsV2."); + odToHrExpectException(VALID_V1_FILE_NAME, 2L); } /** Tests missing bool. */ @Test public void testMissingBool() throws Exception { System.out.println("starting testMissingBool."); - hrToOdExpectException(MISSING_BOOL_FILE_NAME); - odToHrExpectException(MISSING_BOOL_FILE_NAME); + hrToOdExpectException(MISSING_BOOL_FILE_NAME, DEFAULT_VERSION); + odToHrExpectException(MISSING_BOOL_FILE_NAME, DEFAULT_VERSION); } - private void hrToOdExpectException(String fileName) + private void hrToOdExpectException(String fileName, long version) throws ParserConfigurationException, IOException, SAXException { var ele = TestUtils.getElementFromResource( Paths.get(SYSTEM_APP_SAFETY_LABEL_HR_PATH, fileName)); assertThrows( MalformedXmlException.class, - () -> new SystemAppSafetyLabelFactory().createFromHrElement(ele, DEFAULT_VERSION)); + () -> new SystemAppSafetyLabelFactory().createFromHrElement(ele, version)); } - private void odToHrExpectException(String fileName) + private void odToHrExpectException(String fileName, long version) throws ParserConfigurationException, IOException, SAXException { var ele = TestUtils.getElementFromResource( Paths.get(SYSTEM_APP_SAFETY_LABEL_OD_PATH, fileName)); assertThrows( MalformedXmlException.class, - () -> new SystemAppSafetyLabelFactory().createFromOdElement(ele, DEFAULT_VERSION)); + () -> new SystemAppSafetyLabelFactory().createFromOdElement(ele, version)); } - private void testHrToOdSystemAppSafetyLabel(String fileName) throws Exception { + private void testHrToOdSystemAppSafetyLabel(String fileName, long version) throws Exception { var doc = TestUtils.document(); SystemAppSafetyLabel systemAppSafetyLabel = new SystemAppSafetyLabelFactory() .createFromHrElement( TestUtils.getElementFromResource( Paths.get(SYSTEM_APP_SAFETY_LABEL_HR_PATH, fileName)), - DEFAULT_VERSION); + version); Element resultingEle = systemAppSafetyLabel.toOdDomElement(doc); doc.appendChild(resultingEle); TestUtils.testFormatToFormat(doc, Paths.get(SYSTEM_APP_SAFETY_LABEL_OD_PATH, fileName)); } - private void testOdToHrSystemAppSafetyLabel(String fileName) throws Exception { + private void testOdToHrSystemAppSafetyLabel(String fileName, long version) throws Exception { var doc = TestUtils.document(); SystemAppSafetyLabel systemAppSafetyLabel = new SystemAppSafetyLabelFactory() .createFromOdElement( TestUtils.getElementFromResource( Paths.get(SYSTEM_APP_SAFETY_LABEL_OD_PATH, fileName)), - DEFAULT_VERSION); + version); Element resultingEle = systemAppSafetyLabel.toHrDomElement(doc); doc.appendChild(resultingEle); TestUtils.testFormatToFormat(doc, Paths.get(SYSTEM_APP_SAFETY_LABEL_HR_PATH, fileName)); diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/ThirdPartyVerificationTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/ThirdPartyVerificationTest.java new file mode 100644 index 000000000000..ebb2e93af920 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/ThirdPartyVerificationTest.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.asllib.marshallable; + +import static org.junit.Assert.assertThrows; + +import com.android.asllib.testutils.TestUtils; +import com.android.asllib.util.MalformedXmlException; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.w3c.dom.Element; + +import java.nio.file.Paths; + +@RunWith(JUnit4.class) +public class ThirdPartyVerificationTest { + private static final String THIRD_PARTY_VERIFICATION_HR_PATH = + "com/android/asllib/thirdpartyverification/hr"; + private static final String THIRD_PARTY_VERIFICATION_OD_PATH = + "com/android/asllib/thirdpartyverification/od"; + + private static final String VALID_FILE_NAME = "valid.xml"; + private static final String MISSING_URL_FILE_NAME = "missing-url.xml"; + + /** Logic for setting up tests (empty if not yet needed). */ + public static void main(String[] params) throws Exception {} + + @Before + public void setUp() throws Exception { + System.out.println("set up."); + } + + /** Test for valid. */ + @Test + public void testValid() throws Exception { + System.out.println("starting testValid."); + testHrToOdThirdPartyVerification(VALID_FILE_NAME); + testOdToHrThirdPartyVerification(VALID_FILE_NAME); + } + + /** Tests missing url. */ + @Test + public void testMissingUrl() throws Exception { + System.out.println("starting testMissingUrl."); + hrToOdExpectException(MISSING_URL_FILE_NAME); + odToHrExpectException(MISSING_URL_FILE_NAME); + } + + private void hrToOdExpectException(String fileName) { + assertThrows( + MalformedXmlException.class, + () -> { + new ThirdPartyVerificationFactory() + .createFromHrElement( + TestUtils.getElementFromResource( + Paths.get(THIRD_PARTY_VERIFICATION_HR_PATH, fileName))); + }); + } + + private void odToHrExpectException(String fileName) { + assertThrows( + MalformedXmlException.class, + () -> { + new ThirdPartyVerificationFactory() + .createFromOdElement( + TestUtils.getElementFromResource( + Paths.get(THIRD_PARTY_VERIFICATION_OD_PATH, fileName))); + }); + } + + private void testHrToOdThirdPartyVerification(String fileName) throws Exception { + var doc = TestUtils.document(); + ThirdPartyVerification thirdPartyVerification = + new ThirdPartyVerificationFactory() + .createFromHrElement( + TestUtils.getElementFromResource( + Paths.get(THIRD_PARTY_VERIFICATION_HR_PATH, fileName))); + Element ele = thirdPartyVerification.toOdDomElement(doc); + doc.appendChild(ele); + TestUtils.testFormatToFormat(doc, Paths.get(THIRD_PARTY_VERIFICATION_OD_PATH, fileName)); + } + + private void testOdToHrThirdPartyVerification(String fileName) throws Exception { + var doc = TestUtils.document(); + ThirdPartyVerification thirdPartyVerification = + new ThirdPartyVerificationFactory() + .createFromOdElement( + TestUtils.getElementFromResource( + Paths.get(THIRD_PARTY_VERIFICATION_OD_PATH, fileName))); + Element ele = thirdPartyVerification.toHrDomElement(doc); + doc.appendChild(ele); + TestUtils.testFormatToFormat(doc, Paths.get(THIRD_PARTY_VERIFICATION_HR_PATH, fileName)); + } +} diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/TransparencyInfoTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/TransparencyInfoTest.java index 6547fb952944..b27d6ddb6243 100644 --- a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/TransparencyInfoTest.java +++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/TransparencyInfoTest.java @@ -16,16 +16,23 @@ package com.android.asllib.marshallable; +import static org.junit.Assert.assertThrows; + import com.android.asllib.testutils.TestUtils; +import com.android.asllib.util.MalformedXmlException; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.w3c.dom.Element; +import org.xml.sax.SAXException; +import java.io.IOException; import java.nio.file.Paths; +import javax.xml.parsers.ParserConfigurationException; + @RunWith(JUnit4.class) public class TransparencyInfoTest { private static final long DEFAULT_VERSION = 2L; @@ -35,6 +42,10 @@ public class TransparencyInfoTest { private static final String TRANSPARENCY_INFO_OD_PATH = "com/android/asllib/transparencyinfo/od"; private static final String WITH_APP_INFO_FILE_NAME = "with-app-info.xml"; + private static final String VALID_EMPTY_V1_FILE_NAME = "valid-empty-v1.xml"; + private static final String VALID_DEV_INFO_V1_FILE_NAME = "valid-dev-info-v1.xml"; + private static final String WITH_APP_INFO_AND_DEV_INFO_FILE_NAME = + "with-app-info-v2-and-dev-info-v1.xml"; @Before public void setUp() throws Exception { @@ -45,33 +56,78 @@ public class TransparencyInfoTest { @Test public void testTransparencyInfoWithAppInfo() throws Exception { System.out.println("starting testTransparencyInfoWithAppInfo."); - testHrToOdTransparencyInfo(WITH_APP_INFO_FILE_NAME); - testOdToHrTransparencyInfo(WITH_APP_INFO_FILE_NAME); + testHrToOdTransparencyInfo(WITH_APP_INFO_FILE_NAME, DEFAULT_VERSION); + testOdToHrTransparencyInfo(WITH_APP_INFO_FILE_NAME, DEFAULT_VERSION); + } + + /** Test for testMissingAppInfoFailsInV2. */ + @Test + public void testMissingAppInfoFailsInV2() throws Exception { + System.out.println("starting testMissingAppInfoFailsInV2."); + odToHrExpectException(VALID_EMPTY_V1_FILE_NAME, 2L); + } + + /** Test for testMissingAppInfoPassesInV1. */ + @Test + public void testMissingAppInfoPassesInV1() throws Exception { + System.out.println("starting testMissingAppInfoPassesInV1."); + testParseOdTransparencyInfo(VALID_EMPTY_V1_FILE_NAME, 1L); + } + + /** Test for testDeveloperInfoExistencePassesInV1. */ + @Test + public void testDeveloperInfoExistencePassesInV1() throws Exception { + System.out.println("starting testDeveloperInfoExistencePassesInV1."); + testParseOdTransparencyInfo(VALID_DEV_INFO_V1_FILE_NAME, 1L); } - private void testHrToOdTransparencyInfo(String fileName) throws Exception { + /** Test for testDeveloperInfoExistenceFailsInV2. */ + @Test + public void testDeveloperInfoExistenceFailsInV2() throws Exception { + System.out.println("starting testDeveloperInfoExistenceFailsInV2."); + odToHrExpectException(WITH_APP_INFO_AND_DEV_INFO_FILE_NAME, 2L); + } + + private void testHrToOdTransparencyInfo(String fileName, long version) throws Exception { var doc = TestUtils.document(); TransparencyInfo transparencyInfo = new TransparencyInfoFactory() .createFromHrElement( TestUtils.getElementFromResource( Paths.get(TRANSPARENCY_INFO_HR_PATH, fileName)), - DEFAULT_VERSION); + version); Element resultingEle = transparencyInfo.toOdDomElement(doc); doc.appendChild(resultingEle); TestUtils.testFormatToFormat(doc, Paths.get(TRANSPARENCY_INFO_OD_PATH, fileName)); } - private void testOdToHrTransparencyInfo(String fileName) throws Exception { + private void testParseOdTransparencyInfo(String fileName, long version) throws Exception { + var unused = + new TransparencyInfoFactory() + .createFromOdElement( + TestUtils.getElementFromResource( + Paths.get(TRANSPARENCY_INFO_OD_PATH, fileName)), + version); + } + + private void testOdToHrTransparencyInfo(String fileName, long version) throws Exception { var doc = TestUtils.document(); TransparencyInfo transparencyInfo = new TransparencyInfoFactory() .createFromOdElement( TestUtils.getElementFromResource( Paths.get(TRANSPARENCY_INFO_OD_PATH, fileName)), - DEFAULT_VERSION); + version); Element resultingEle = transparencyInfo.toHrDomElement(doc); doc.appendChild(resultingEle); TestUtils.testFormatToFormat(doc, Paths.get(TRANSPARENCY_INFO_HR_PATH, fileName)); } + + private void odToHrExpectException(String fileName, long version) + throws ParserConfigurationException, IOException, SAXException { + var ele = TestUtils.getElementFromResource(Paths.get(TRANSPARENCY_INFO_OD_PATH, fileName)); + assertThrows( + MalformedXmlException.class, + () -> new TransparencyInfoFactory().createFromOdElement(ele, version)); + } } diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/valid-v1.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/valid-v1.xml new file mode 100644 index 000000000000..7e984e333ceb --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/valid-v1.xml @@ -0,0 +1,8 @@ +<bundle> + <long name="version" value="1"/> + <pbundle_as_map name="system_app_safety_label"> + <string name="url" value="www.example.com"/> + </pbundle_as_map> + <pbundle_as_map name="transparency_info"> + </pbundle_as_map> +</bundle>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/appinfo/od/unrecognized-v1.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/appinfo/od/unrecognized-v1.xml index 810078e777fb..01fd7180c3a6 100644 --- a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/appinfo/od/unrecognized-v1.xml +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/appinfo/od/unrecognized-v1.xml @@ -21,5 +21,5 @@ <string name="category" value="Food and drink"/> <string name="email" value="max@maxloh.com"/> <string name="website" value="www.example.com"/> - <string name="unrecognized" value="www.example.com"/> + <boolean name="aps_compliant" value="false"/> </pbundle_as_map> diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/safetylabels/od/valid-v1.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/safetylabels/od/valid-v1.xml new file mode 100644 index 000000000000..1384a2f6dd52 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/safetylabels/od/valid-v1.xml @@ -0,0 +1,9 @@ +<pbundle_as_map name="safety_labels"> + <pbundle_as_map name="security_labels"> + <boolean name="is_data_deletable" value="true" /> + <boolean name="is_data_encrypted" value="false" /> + </pbundle_as_map> + <pbundle_as_map name="third_party_verification"> + <string name="url" value="www.example.com"/> + </pbundle_as_map> +</pbundle_as_map>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/systemappsafetylabel/od/valid-v1.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/systemappsafetylabel/od/valid-v1.xml new file mode 100644 index 000000000000..f96535b4b49b --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/systemappsafetylabel/od/valid-v1.xml @@ -0,0 +1,3 @@ +<pbundle_as_map name="system_app_safety_label"> + <string name="url" value="www.example.com"/> +</pbundle_as_map>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/valid-dev-info-v1.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/valid-dev-info-v1.xml new file mode 100644 index 000000000000..d7a4e1a959b7 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/valid-dev-info-v1.xml @@ -0,0 +1,12 @@ + +<pbundle_as_map name="transparency_info"> + <pbundle_as_map name="developer_info"> + <string name="name" value="max"/> + <string name="email" value="max@example.com"/> + <string name="address" value="111 blah lane"/> + <string name="country_region" value="US"/> + <long name="relationship" value="5"/> + <string name="website" value="example.com"/> + <string name="app_developer_registry_id" value="registry_id"/> + </pbundle_as_map> +</pbundle_as_map>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/valid-empty.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/valid-empty-v1.xml index af574cf92b3a..af574cf92b3a 100644 --- a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/valid-empty.xml +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/valid-empty-v1.xml diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/with-developer-info.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/with-app-info-v2-and-dev-info-v1.xml index b5e64b925ca5..b5e64b925ca5 100644 --- a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/with-developer-info.xml +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/with-app-info-v2-and-dev-info-v1.xml |