Merge "Fix bug with half-black screen when folding or waking with a SHOW_WHEN_LOCKED app in splitscreen" into main
diff --git a/apct-tests/perftests/packagemanager/src/android/os/PackageManagerPerfTest.java b/apct-tests/perftests/packagemanager/src/android/os/PackageManagerPerfTest.java
index 4bcc8c4..f302033 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.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 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 dc5e341..93904a7 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 @@
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 @@
}
/**
- * 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 6a04f0d..89a0c18 100644
--- a/api/Android.bp
+++ b/api/Android.bp
@@ -29,12 +29,14 @@
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/api.go b/api/api.go
index d4db49e..f0d1f42 100644
--- a/api/api.go
+++ b/api/api.go
@@ -54,11 +54,11 @@
// 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 @@
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 (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 0000000..47d1670
--- /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 edd002e..c09bee5 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 0f23721..d610f4c 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -26569,7 +26569,7 @@
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);
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 44c4ab4..e0c3230 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -618,6 +618,7 @@
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 f3b330a..5223c87 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/Activity.java b/core/java/android/app/Activity.java
index 85b6e51b..cbabb02 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -3702,7 +3702,9 @@
* @see View#findViewById(int)
* @see Activity#requireViewById(int)
*/
- /* TODO(b/347672184): Re-add @Nullable */
+ // Strictly speaking this should be marked as @Nullable but the nullability of the return value
+ // is deliberately left unspecified as idiomatically correct code can make assumptions either
+ // way based on local context, e.g. layout specification.
public <T extends View> T findViewById(@IdRes int id) {
return getWindow().findViewById(id);
}
diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java
index c6a1546..65acd49 100644
--- a/core/java/android/app/ActivityOptions.java
+++ b/core/java/android/app/ActivityOptions.java
@@ -104,7 +104,9 @@
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 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 0e8e2e3..b3fc058 100644
--- a/core/java/android/app/ComponentOptions.java
+++ b/core/java/android/app/ComponentOptions.java
@@ -18,9 +18,11 @@
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 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 @@
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 @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 @@
}
/**
- * Set PendingIntent activity can be launched from background if caller has BAL permission.
- * @hide
- */
- public void setPendingIntentBackgroundActivityLaunchAllowedByPermission(boolean allowed) {
- mPendingIntentBalAllowedByPermission = allowed;
- }
-
- /**
* Get PendingIntent activity is allowed to be started in the background if the caller
* has BAL permission.
* @hide
+ * @deprecated check for #MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS
*/
+ @Deprecated
public boolean isPendingIntentBackgroundActivityLaunchAllowedByPermission() {
- return mPendingIntentBalAllowedByPermission;
+ return mPendingIntentBalAllowed == MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS;
+ }
+
+ /**
+ * Set PendingIntent activity can be launched from background if caller has BAL permission.
+ * @hide
+ * @deprecated use #MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS
+ */
+ @Deprecated
+ public void setPendingIntentBackgroundActivityLaunchAllowedByPermission(boolean allowed) {
+ if (allowed) {
+ setPendingIntentBackgroundActivityStartMode(
+ MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS);
+ } else {
+ if (getPendingIntentBackgroundActivityStartMode()
+ == MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS) {
+ setPendingIntentBackgroundActivityStartMode(MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
+ }
+ }
}
/** @hide */
@@ -162,10 +174,6 @@
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/Dialog.java b/core/java/android/app/Dialog.java
index 57d9a71..5cb17fa 100644
--- a/core/java/android/app/Dialog.java
+++ b/core/java/android/app/Dialog.java
@@ -565,7 +565,9 @@
* @see View#findViewById(int)
* @see Dialog#requireViewById(int)
*/
- /* TODO(b/347672184): Re-add @Nullable */
+ // Strictly speaking this should be marked as @Nullable but the nullability of the return value
+ // is deliberately left unspecified as idiomatically correct code can make assumptions either
+ // way based on local context, e.g. layout specification.
public <T extends View> T findViewById(@IdRes int id) {
return mWindow.findViewById(id);
}
diff --git a/core/java/android/app/IApplicationThread.aidl b/core/java/android/app/IApplicationThread.aidl
index a64261a..9f3829e 100644
--- a/core/java/android/app/IApplicationThread.aidl
+++ b/core/java/android/app/IApplicationThread.aidl
@@ -65,6 +65,7 @@
*
* {@hide}
*/
+@JavaDelegator
oneway interface IApplicationThread {
void scheduleReceiver(in Intent intent, in ActivityInfo info,
in CompatibilityInfo compatInfo,
diff --git a/core/java/android/app/OWNERS b/core/java/android/app/OWNERS
index 0fad979..1200b4b 100644
--- a/core/java/android/app/OWNERS
+++ b/core/java/android/app/OWNERS
@@ -118,6 +118,8 @@
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/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index fb0ce0d..a7070b9 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -9202,6 +9202,14 @@
/**
* @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 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 @@
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 d183713..381f996 100644
--- a/core/java/android/app/admin/IDevicePolicyManager.aidl
+++ b/core/java/android/app/admin/IDevicePolicyManager.aidl
@@ -160,7 +160,8 @@
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 8227112..c789af3 100644
--- a/core/java/android/app/admin/flags/flags.aconfig
+++ b/core/java/android/app/admin/flags/flags.aconfig
@@ -393,3 +393,13 @@
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 97404dc..111e6a8 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -5277,12 +5277,28 @@
* 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 c2c7b81..5a39702 100644
--- a/core/java/android/content/pm/multiuser.aconfig
+++ b/core/java/android/content/pm/multiuser.aconfig
@@ -327,3 +327,13 @@
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 0f944cf..51024ba 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 @@
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/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java
index 855c309..da7997d 100644
--- a/core/java/android/inputmethodservice/InputMethodService.java
+++ b/core/java/android/inputmethodservice/InputMethodService.java
@@ -4341,7 +4341,8 @@
}
/**
- * Called when the IME switch button was clicked from the client. This will show the input
+ * Called when the IME switch button was clicked from the client. Depending on the number of
+ * enabled IME subtypes, this will either switch to the next IME/subtype, or show the input
* method picker dialog.
*
* @hide
diff --git a/core/java/android/inputmethodservice/navigationbar/NavigationBarView.java b/core/java/android/inputmethodservice/navigationbar/NavigationBarView.java
index a3beaf4..209f323 100644
--- a/core/java/android/inputmethodservice/navigationbar/NavigationBarView.java
+++ b/core/java/android/inputmethodservice/navigationbar/NavigationBarView.java
@@ -216,7 +216,11 @@
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/IBinder.java b/core/java/android/os/IBinder.java
index c9f207c..50242ba 100644
--- a/core/java/android/os/IBinder.java
+++ b/core/java/android/os/IBinder.java
@@ -29,7 +29,7 @@
* 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 @@
* 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 @@
* 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 @@
* 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 @@
* 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 @@
* 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 @@
* 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 @@
* 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 @@
* 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 @@
/**
* 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 @@
/**
* 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 @@
* 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 @@
/**
* 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 @@
/**
* 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 @@
* 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/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java
index 6b0d301..e4fc1cd 100644
--- a/core/java/android/service/dreams/DreamService.java
+++ b/core/java/android/service/dreams/DreamService.java
@@ -742,7 +742,9 @@
* @see View#findViewById(int)
* @see DreamService#requireViewById(int)
*/
- /* TODO(b/347672184): Re-add @Nullable */
+ // Strictly speaking this should be marked as @Nullable but the nullability of the return value
+ // is deliberately left unspecified as idiomatically correct code can make assumptions either
+ // way based on local context, e.g. layout specification.
public <T extends View> T findViewById(@IdRes int id) {
return getWindow().findViewById(id);
}
diff --git a/core/java/android/view/Choreographer.java b/core/java/android/view/Choreographer.java
index f0e673b..7e24749 100644
--- a/core/java/android/view/Choreographer.java
+++ b/core/java/android/view/Choreographer.java
@@ -41,6 +41,7 @@
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 @@
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 @@
* @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 @@
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 @@
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 1d950dc..6343313 100644
--- a/core/java/android/view/ImeInsetsSourceConsumer.java
+++ b/core/java/android/view/ImeInsetsSourceConsumer.java
@@ -119,9 +119,11 @@
@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 @@
@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 df2af73..f166b89 100644
--- a/core/java/android/view/InsetsController.java
+++ b/core/java/android/view/InsetsController.java
@@ -765,7 +765,7 @@
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 c73cbc6..477e35b 100644
--- a/core/java/android/view/InsetsSourceConsumer.java
+++ b/core/java/android/view/InsetsSourceConsumer.java
@@ -43,6 +43,7 @@
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 @@
@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 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 2edbcc2..a23e383 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_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 @@
* 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 @@
// 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) {
@@ -27543,7 +27560,9 @@
* @return a view with given ID if found, or {@code null} otherwise
* @see View#requireViewById(int)
*/
- /* TODO(b/347672184): Re-add @Nullable */
+ // Strictly speaking this should be marked as @Nullable but the nullability of the return value
+ // is deliberately left unspecified as idiomatically correct code can make assumptions either
+ // way based on local context, e.g. layout specification.
public final <T extends View> T findViewById(@IdRes int id) {
if (id == NO_ID) {
return null;
diff --git a/core/java/android/view/Window.java b/core/java/android/view/Window.java
index 7cb70bd..0582afe 100644
--- a/core/java/android/view/Window.java
+++ b/core/java/android/view/Window.java
@@ -1722,7 +1722,9 @@
* @see View#findViewById(int)
* @see Window#requireViewById(int)
*/
- /* TODO(b/347672184): Re-add @Nullable */
+ // Strictly speaking this should be marked as @Nullable but the nullability of the return value
+ // is deliberately left unspecified as idiomatically correct code can make assumptions either
+ // way based on local context, e.g. layout specification.
public <T extends View> T findViewById(@IdRes int id) {
return getDecorView().findViewById(id);
}
diff --git a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
index d0bc57b..44c1acc 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 {
+ 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."
diff --git a/core/java/android/view/inputmethod/InputMethodInfo.java b/core/java/android/view/inputmethod/InputMethodInfo.java
index 098f655..0e66f7a 100644
--- a/core/java/android/view/inputmethod/InputMethodInfo.java
+++ b/core/java/android/view/inputmethod/InputMethodInfo.java
@@ -891,12 +891,13 @@
@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 dbbfff0..fed8eea 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -1352,12 +1352,16 @@
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 @@
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 @@
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,
@@ -4354,7 +4363,8 @@
}
/**
- * Called when the IME switch button was clicked from the system. This will show the input
+ * Called when the IME switch button was clicked from the system. Depending on the number of
+ * enabled IME subtypes, this will either switch to the next IME/subtype, or show the input
* method picker dialog.
*
* @param displayId The ID of the display where the input method picker dialog should be shown.
diff --git a/core/java/android/webkit/WebViewProviderInfo.java b/core/java/android/webkit/WebViewProviderInfo.java
index 6629fdc4..16727c3 100644
--- a/core/java/android/webkit/WebViewProviderInfo.java
+++ b/core/java/android/webkit/WebViewProviderInfo.java
@@ -23,6 +23,9 @@
import android.os.Parcelable;
import android.util.Base64;
+import java.util.Arrays;
+import java.util.Objects;
+
/**
* @hide
*/
@@ -80,6 +83,35 @@
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/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig
index 3f1c06a..91ac4ff 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 {
+ 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 {
+ 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 5397e91..c451cc8 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -19,6 +19,16 @@
}
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/protolog/IProtoLogClient.aidl b/core/java/com/android/internal/protolog/IProtoLogClient.aidl
new file mode 100644
index 0000000..969ed99
--- /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 0000000..cc349ea
--- /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 652cba7..8f7d703 100644
--- a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
+++ b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
@@ -88,7 +88,7 @@
/**
* 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 @@
);
@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 @@
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 PerfettoProtoLogImpl(
- @Nullable ViewerConfigInputStreamProvider viewerConfigInputStreamProvider,
+ @NonNull ViewerConfigInputStreamProvider viewerConfigInputStreamProvider,
Runnable cacheUpdater
) {
this(viewerConfigInputStreamProvider,
@@ -203,13 +203,23 @@
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 @@
* @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 @@
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);
diff --git a/core/java/com/android/internal/protolog/ProtoLogImpl.java b/core/java/com/android/internal/protolog/ProtoLogImpl.java
index 3082295..77ca7ce 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.LogLevel;
import com.android.internal.protolog.common.ProtoLogToolInjected;
+import java.io.File;
import java.util.TreeMap;
/**
@@ -105,7 +106,15 @@
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 bb6c8b7..38ca0d8 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.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 334f548..14bc8e4 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 0000000..d65dddd
--- /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 c07fd38..7c62615 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 @@
jfieldID mMixerBehavior;
} gAudioMixerAttributesField;
+static struct {
+ jclass clazz;
+ jmethodID run;
+} gRunnableClassInfo;
+
+static JavaVM* gVm;
+
static Mutex gLock;
enum AudioError {
@@ -3362,6 +3372,55 @@
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 @@
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 @@
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 d32486c..240be3f 100644
--- a/core/jni/android_view_MotionEvent.cpp
+++ b/core/jni/android_view_MotionEvent.cpp
@@ -415,7 +415,7 @@
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 30c926c..7e2a5ac 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 @@
* (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 @@
}
}
- 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 @@
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 @@
#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;
- }
- 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);
+// 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");
}
- keyboardId++;
}
-
- 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);
}
-} // namespace android
+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;");
-using namespace android;
+ // 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);
+
+ if (register_jni_procs(gRegJNIMap, classesToRegister, env) < 0) {
+ return JNI_ERR;
+ }
+
+ 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 @@
ALOGE("Aborting because: %s", abort_message);
}
+// ------------------ Host implementation of AndroidRuntime ------------------
+
+/*static*/ JavaVM* AndroidRuntime::mJavaVM;
+
+/*static*/ int AndroidRuntime::registerNativeMethods(JNIEnv* env, const char* className,
+ const JNINativeMethod* gMethods,
+ int numMethods) {
+ return jniRegisterNativeMethods(env, className, gMethods, numMethods);
+}
+
+/*static*/ JNIEnv* AndroidRuntime::getJNIEnv() {
+ JNIEnv* env;
+ JavaVM* vm = AndroidRuntime::getJavaVM();
+ if (vm->GetEnv((void**)&env, JNI_VERSION_1_6) != JNI_OK) {
+ return nullptr;
+ }
+ return env;
+}
+
+/*static*/ JavaVM* AndroidRuntime::getJavaVM() {
+ return mJavaVM;
+}
+
+/*static*/ int AndroidRuntime::startReg(JNIEnv* env) {
+ if (register_android_core_classes(env) < 0) {
+ return JNI_ERR;
+ }
+ if (register_android_graphics_classes(env) < 0) {
+ return JNI_ERR;
+ }
+ return 0;
+}
+
+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");
+}
+
+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();
+}
+
+AndroidRuntime::AndroidRuntime(char* argBlockStart, const size_t argBlockLength)
+ : mExitWithoutCleanup(false), mArgBlockStart(argBlockStart), mArgBlockLength(argBlockLength) {
+ init_android_graphics();
+}
+
+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*) {
- 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);
+ Vector<String8> args;
+ HostRuntime runtime;
- init_android_graphics();
-
- // 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;");
-
- // 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);
- }
- // Get the names of classes that need to register their native methods
- vector<string> classesToRegister = parseCsv(systemProperties["core_native_classes"]);
-
- 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();
- }
-
- if (register_jni_procs(gRegJNIMap, classesToRegister, 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 (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);
- }
-
- // 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");
- }
+ runtime.onVmCreated(env);
+ runtime.start("HostRuntime", args, false);
return JNI_VERSION_1_6;
}
-
-JNIEXPORT void JNI_OnUnload(JavaVM* vm, void*) {
- JNIEnv* env = nullptr;
- vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6);
- env->DeleteGlobalRef(bridge);
- env->DeleteGlobalRef(layoutLog);
-}
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 0000000..04f4a25
--- /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 0000000..396d81e
--- /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 0000000..eb7a246
--- /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 0000000..5a4d6b1
--- /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 0000000..16a97c4
--- /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 0975eda..7cc9e13 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 dc3d935..236e7c5 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 46b15416..ec865f6 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 c50b961..09688f2 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 382ff04..f5c6738 100644
--- a/core/res/res/values/themes_device_defaults.xml
+++ b/core/res/res/values/themes_device_defaults.xml
@@ -6179,4 +6179,10 @@
<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/coretests/src/android/animation/OWNERS b/core/tests/coretests/src/android/animation/OWNERS
new file mode 100644
index 0000000..1eefb3a
--- /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/view/ImeInsetsSourceConsumerTest.java b/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java
index 47b28899..248db65 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.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.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 @@
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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/errorprone/java/com/google/errorprone/bugpatterns/android/RequiresPermissionChecker.java b/errorprone/java/com/google/errorprone/bugpatterns/android/RequiresPermissionChecker.java
index 7c7cb18..9887c27 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.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.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 @@
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 @@
}
}
- 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 @@
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;
+ 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(last);
- } else {
- final Name name = resolveName(tree.getMethodSelect());
- if (Objects.equal(argName, name)
- && INTENT_SET_ACTION.matches(tree, state)) {
- last = parseIntentAction(tree);
+ @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 e53372d..05fde7c 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 @@
" 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 efc4fb1..9d622ff 100644
--- a/errorprone/tests/res/android/content/Context.java
+++ b/errorprone/tests/res/android/content/Context.java
@@ -36,4 +36,15 @@
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 288396e..7ccea78 100644
--- a/errorprone/tests/res/android/content/Intent.java
+++ b/errorprone/tests/res/android/content/Intent.java
@@ -24,4 +24,8 @@
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 e6cb3a0..5135e9e 100644
--- a/libs/WindowManager/Shell/Android.bp
+++ b/libs/WindowManager/Shell/Android.bp
@@ -239,6 +239,9 @@
"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 52ae93f..bbbc23e 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 235b9bf..fc3dc14 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 @@
}
}
+ /** 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/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
index bb239ad..a9fdea3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
@@ -114,6 +114,7 @@
@Nullable
private BackNavigationInfo mBackNavigationInfo;
+ private boolean mReceivedNullNavigationInfo = false;
private final IActivityTaskManager mActivityTaskManager;
private final Context mContext;
private final ContentResolver mContentResolver;
@@ -430,7 +431,7 @@
mThresholdCrossed = true;
// There was no focus window when calling startBackNavigation, still pilfer pointers so
// the next focus window won't receive motion events.
- if (mBackNavigationInfo == null) {
+ if (mBackNavigationInfo == null && mReceivedNullNavigationInfo) {
tryPilferPointers();
return;
}
@@ -553,6 +554,7 @@
ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Received backNavigationInfo:%s", backNavigationInfo);
if (backNavigationInfo == null) {
ProtoLog.e(WM_SHELL_BACK_PREVIEW, "Received BackNavigationInfo is null.");
+ mReceivedNullNavigationInfo = true;
cancelLatencyTracking();
tryPilferPointers();
return;
@@ -909,6 +911,7 @@
mPointersPilfered = false;
mShellBackAnimationRegistry.resetDefaultCrossActivity();
cancelLatencyTracking();
+ mReceivedNullNavigationInfo = false;
if (mBackNavigationInfo != null) {
mPreviousNavigationType = mBackNavigationInfo.getType();
mBackNavigationInfo.onBackNavigationFinished(triggerBack);
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 f7a5c27..d4d9d00 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 @@
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 c79d9c4..5e2141a 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 @@
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 48e1376..80f6a63 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 @@
@ShellMainThread ShellExecutor mainExecutor,
@ShellMainThread Handler mainHandler,
@ShellMainThread Choreographer mainChoreographer,
+ @ShellBackgroundThread ShellExecutor bgExecutor,
ShellInit shellInit,
IWindowManager windowManager,
ShellCommandHandler shellCommandHandler,
@@ -229,6 +230,7 @@
mainExecutor,
mainHandler,
mainChoreographer,
+ bgExecutor,
shellInit,
shellCommandHandler,
windowManager,
@@ -246,6 +248,7 @@
context,
mainHandler,
mainExecutor,
+ bgExecutor,
mainChoreographer,
windowManager,
shellInit,
@@ -574,8 +577,9 @@
@Provides
static EnterDesktopTaskTransitionHandler provideEnterDesktopModeTaskTransitionHandler(
Transitions transitions,
- Optional<DesktopTasksLimiter> desktopTasksLimiter) {
- return new EnterDesktopTaskTransitionHandler(transitions);
+ Optional<DesktopTasksLimiter> desktopTasksLimiter,
+ InteractionJankMonitor interactionJankMonitor) {
+ return new EnterDesktopTaskTransitionHandler(transitions, interactionJankMonitor);
}
@WMSingleton
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
index 1bf1259..da212e7 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 @@
idealSize
}
} else {
- maximumSizeMaintainingAspectRatio(taskInfo, idealSize, appAspectRatio)
+ maximizeSizeGivenAspectRatio(taskInfo, idealSize, appAspectRatio)
}
}
ORIENTATION_PORTRAIT -> {
@@ -85,13 +85,13 @@
} 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 @@
* 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 9a1a8a2..a91edaa 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
@@ -671,7 +671,7 @@
} 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 +818,8 @@
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 +982,7 @@
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 +990,17 @@
" 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 +1079,6 @@
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 +1088,6 @@
} 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()) {
@@ -1420,7 +1421,6 @@
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/desktopmode/EnterDesktopTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java
index e5b624f..04506c1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java
@@ -18,6 +18,7 @@
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_MODE_APP_HANDLE_MENU;
import static com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.getEnterTransitionType;
import static com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.isEnterDesktopModeTransition;
@@ -39,6 +40,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import com.android.internal.jank.InteractionJankMonitor;
import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource;
import com.android.wm.shell.transition.Transitions;
import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener;
@@ -60,18 +62,21 @@
public static final int FREEFORM_ANIMATION_DURATION = 336;
private final List<IBinder> mPendingTransitionTokens = new ArrayList<>();
+ private final InteractionJankMonitor mInteractionJankMonitor;
private OnTaskResizeAnimationListener mOnTaskResizeAnimationListener;
public EnterDesktopTaskTransitionHandler(
- Transitions transitions) {
- this(transitions, SurfaceControl.Transaction::new);
+ Transitions transitions, InteractionJankMonitor interactionJankMonitor) {
+ this(transitions, interactionJankMonitor, SurfaceControl.Transaction::new);
}
public EnterDesktopTaskTransitionHandler(
Transitions transitions,
+ InteractionJankMonitor interactionJankMonitor,
Supplier<SurfaceControl.Transaction> supplier) {
mTransitions = transitions;
+ mInteractionJankMonitor = interactionJankMonitor;
mTransactionSupplier = supplier;
}
@@ -175,6 +180,7 @@
mOnTaskResizeAnimationListener.onAnimationEnd(taskInfo.taskId);
mTransitions.getMainExecutor().execute(
() -> finishCallback.onTransitionFinished(null));
+ mInteractionJankMonitor.end(CUJ_DESKTOP_MODE_ENTER_MODE_APP_HANDLE_MENU);
}
});
animator.start();
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 95fe8b6..7e03624 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 @@
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 9539a45..d001b2c 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 @@
@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/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index dd7228a..a4f32c4 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;
@@ -1950,8 +1950,8 @@
}
// 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.
@@ -3783,8 +3783,9 @@
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/sysui/ShellInit.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java
index dd4595a..287e779 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 ShellInit(ShellExecutor mainExecutor) {
mMainExecutor = mainExecutor;
+ ProtoLog.registerGroups(ShellProtoLogGroup.values());
}
/**
@@ -76,7 +77,6 @@
*/
@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 b9cb6d3..5c230c0 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.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 @@
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 CaptionWindowDecorViewModel(
Context context,
Handler mainHandler,
+ @ShellBackgroundThread ShellExecutor bgExecutor,
ShellExecutor shellExecutor,
Choreographer mainChoreographer,
IWindowManager windowManager,
@@ -120,6 +123,7 @@
mContext = context;
mMainExecutor = shellExecutor;
mMainHandler = mainHandler;
+ mBgExecutor = bgExecutor;
mWindowManager = windowManager;
mMainChoreographer = mainChoreographer;
mTaskOrganizer = taskOrganizer;
@@ -289,6 +293,7 @@
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 7e1b973..cf42a49 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.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.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 @@
*/
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 @@
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 @@
relayoutParams.mOccludingCaptionElements.add(controlsElement);
}
+ @SuppressLint("MissingPermission")
void relayout(RunningTaskInfo taskInfo,
SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
boolean applyStartTransactionOnDraw, boolean setTaskCropAndPosition) {
@@ -235,7 +242,7 @@
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 d12b906..eaeedaa 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_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;
@@ -31,6 +33,7 @@
import static android.view.MotionEvent.ACTION_UP;
import static android.view.WindowInsets.Type.statusBars;
+import static com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_MODE_APP_HANDLE_MENU;
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
import static com.android.wm.shell.compatui.AppCompatUtils.isTopActivityExemptFromDesktopWindowing;
@@ -42,11 +45,8 @@
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;
@@ -97,6 +97,7 @@
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;
@@ -131,6 +132,7 @@
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;
@@ -182,6 +184,7 @@
ShellExecutor shellExecutor,
Handler mainHandler,
Choreographer mainChoreographer,
+ @ShellBackgroundThread ShellExecutor bgExecutor,
ShellInit shellInit,
ShellCommandHandler shellCommandHandler,
IWindowManager windowManager,
@@ -200,6 +203,7 @@
shellExecutor,
mainHandler,
mainChoreographer,
+ bgExecutor,
shellInit,
shellCommandHandler,
windowManager,
@@ -224,6 +228,7 @@
ShellExecutor shellExecutor,
Handler mainHandler,
Choreographer mainChoreographer,
+ @ShellBackgroundThread ShellExecutor bgExecutor,
ShellInit shellInit,
ShellCommandHandler shellCommandHandler,
IWindowManager windowManager,
@@ -244,6 +249,7 @@
mMainExecutor = shellExecutor;
mMainHandler = mainHandler;
mMainChoreographer = mainChoreographer;
+ mBgExecutor = bgExecutor;
mActivityTaskManager = mContext.getSystemService(ActivityTaskManager.class);
mTaskOrganizer = taskOrganizer;
mShellController = shellController;
@@ -423,19 +429,12 @@
}
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 {
@@ -499,6 +498,8 @@
final WindowContainerTransaction wct = new WindowContainerTransaction();
// App sometimes draws before the insets from WindowDecoration#relayout have
// been added, so they must be added here
+ mInteractionJankMonitor.begin(decoration.mTaskSurface, mContext,
+ CUJ_DESKTOP_MODE_ENTER_MODE_APP_HANDLE_MENU);
mWindowDecorByTaskId.get(mTaskId).addCaptionInset(wct);
mDesktopTasksController.moveToDesktop(mTaskId, wct,
DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON);
@@ -1092,6 +1093,7 @@
taskInfo,
taskSurface,
mMainHandler,
+ mBgExecutor,
mMainChoreographer,
mSyncQueue,
mRootTaskDisplayAreaOrganizer);
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 5d662b2..7988983 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
@@ -29,6 +29,7 @@
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 +69,9 @@
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,6 +98,7 @@
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;
@@ -151,11 +155,12 @@
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,
+ handler, bgExecutor, choreographer, syncQueue, rootTaskDisplayAreaOrganizer,
SurfaceControl.Builder::new, SurfaceControl.Transaction::new,
WindowContainerTransaction::new, SurfaceControl::new,
new SurfaceControlViewHostFactory() {}, DefaultMaximizeMenuFactory.INSTANCE);
@@ -168,6 +173,7 @@
ActivityManager.RunningTaskInfo taskInfo,
SurfaceControl taskSurface,
Handler handler,
+ @ShellBackgroundThread ShellExecutor bgExecutor,
Choreographer choreographer,
SyncTransactionQueue syncQueue,
RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
@@ -182,6 +188,7 @@
windowContainerTransactionSupplier, surfaceControlSupplier,
surfaceControlViewHostFactory);
mHandler = handler;
+ mBgExecutor = bgExecutor;
mChoreographer = choreographer;
mSyncQueue = syncQueue;
mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer;
@@ -327,6 +334,7 @@
mHandler.post(mCurrentViewHostRunnable);
}
+ @SuppressLint("MissingPermission")
private void updateRelayoutParamsAndSurfaces(ActivityManager.RunningTaskInfo taskInfo,
SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop) {
@@ -337,7 +345,7 @@
}
if (isHandleMenuActive()) {
- mHandleMenu.relayout(startT);
+ mHandleMenu.relayout(startT, mResult.mCaptionX);
}
updateRelayoutParams(mRelayoutParams, mContext, taskInfo, applyStartTransactionOnDraw,
@@ -353,7 +361,7 @@
// 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) {
@@ -863,7 +871,9 @@
splitScreenController,
DesktopModeStatus.canEnterDesktopMode(mContext),
browserLinkAvailable(),
- mResult.mCaptionHeight
+ mResult.mCaptionWidth,
+ mResult.mCaptionHeight,
+ mResult.mCaptionX
);
mWindowDecorViewHolder.onHandleMenuOpened();
mHandleMenu.show();
@@ -1147,6 +1157,7 @@
ActivityManager.RunningTaskInfo taskInfo,
SurfaceControl taskSurface,
Handler handler,
+ @ShellBackgroundThread ShellExecutor bgExecutor,
Choreographer choreographer,
SyncTransactionQueue syncQueue,
RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) {
@@ -1157,6 +1168,7 @@
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 bce233f..e174e83 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.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.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 @@
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 @@
// 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 @@
* 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 @@
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)
}
- val browserButton = handleMenu.findViewById<Button>(R.id.open_in_browser_button)
- browserButton.setOnClickListener(onClickListener)
+
+ pill.requireViewById<Button>(R.id.open_in_browser_button).let { browserButton ->
+ browserButton.setOnClickListener(onClickListener)
+ browserButton.setTextColor(style.textColor)
+ browserButton.compoundDrawableTintList = ColorStateList.valueOf(style.textColor)
+ }
}
/**
* 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 @@
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 @@
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 @@
/**
* 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 @@
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/additionalviewcontainer/AdditionalSystemViewContainer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt
index 4897f76..03d2421 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
@@ -38,6 +38,7 @@
height: Int
) : AdditionalViewContainer() {
override val view: View
+ val windowManager: WindowManager? = context.getSystemService(WindowManager::class.java)
init {
view = LayoutInflater.from(context).inflate(layoutId, null)
@@ -51,12 +52,11 @@
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 +64,6 @@
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 7ade987..6f8e001 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 @@
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 @@
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/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/EnterDesktopWithAppHandleMenu.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/EnterDesktopWithAppHandleMenu.kt
new file mode 100644
index 0000000..47a215a
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/EnterDesktopWithAppHandleMenu.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.service.desktopmode.scenarios
+
+import android.app.Instrumentation
+import android.tools.traces.parsers.WindowManagerStateHelper
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import com.android.launcher3.tapl.LauncherInstrumentation
+import com.android.server.wm.flicker.helpers.DesktopModeAppHelper
+import com.android.server.wm.flicker.helpers.SimpleAppHelper
+import com.android.window.flags.Flags
+import org.junit.After
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Test
+
+/** Base test class for enter desktop with app handle menu CUJ. */
+@Ignore("Base Test Class")
+abstract class EnterDesktopWithAppHandleMenu {
+
+ private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+ private val tapl = LauncherInstrumentation()
+ private val wmHelper = WindowManagerStateHelper(instrumentation)
+ private val device = UiDevice.getInstance(instrumentation)
+ private val simpleAppHelper = SimpleAppHelper(instrumentation)
+ private val testApp = DesktopModeAppHelper(simpleAppHelper)
+
+ @Before
+ fun setup() {
+ Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet)
+ }
+
+ @Test
+ open fun enterDesktopWithAppHandleMenu() {
+ simpleAppHelper.launchViaIntent(wmHelper)
+ testApp.enterDesktopModeFromAppHandleMenu(wmHelper, device)
+ }
+
+ @After
+ fun teardown() {
+ testApp.exit(wmHelper)
+ }
+}
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 db962e7..2406bde 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 @@
@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 da88686..8421365 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 @@
}
@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 @@
}
@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 @@
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 @@
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 @@
@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 @@
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 @@
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 @@
@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 @@
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 @@
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 @@
@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 @@
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 @@
@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 @@
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 @@
@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 @@
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 @@
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 @@
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 @@
@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 @@
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 @@
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 @@
@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 @@
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 @@
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 @@
@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 @@
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 @@
@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 @@
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 @@
@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 @@
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/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
index 315332f..eaef704 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
@@ -348,8 +348,7 @@
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
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 b1803e9..ae00c3e 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
@@ -62,6 +62,7 @@
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
@@ -136,6 +137,7 @@
@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 +157,7 @@
mockShellExecutor,
mockMainHandler,
mockMainChoreographer,
+ bgExecutor,
shellInit,
mockShellCommandHandler,
mockWindowManager,
@@ -208,6 +211,7 @@
task,
taskSurface,
mockMainHandler,
+ bgExecutor,
mockMainChoreographer,
mockSyncQueue,
mockRootTaskDisplayAreaOrganizer
@@ -232,6 +236,7 @@
task,
taskSurface,
mockMainHandler,
+ bgExecutor,
mockMainChoreographer,
mockSyncQueue,
mockRootTaskDisplayAreaOrganizer
@@ -247,6 +252,7 @@
task,
taskSurface,
mockMainHandler,
+ bgExecutor,
mockMainChoreographer,
mockSyncQueue,
mockRootTaskDisplayAreaOrganizer
@@ -344,7 +350,7 @@
onTaskChanging(task)
verify(mockDesktopModeWindowDecorFactory, never())
- .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any())
+ .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any(), any())
}
@Test
@@ -365,7 +371,7 @@
onTaskOpening(task)
verify(mockDesktopModeWindowDecorFactory)
- .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any())
+ .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any(), any())
} finally {
mockitoSession.finishMocking()
}
@@ -382,7 +388,7 @@
onTaskOpening(task)
verify(mockDesktopModeWindowDecorFactory, never())
- .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any())
+ .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any(), any())
}
@Test
@@ -399,7 +405,7 @@
onTaskOpening(task)
verify(mockDesktopModeWindowDecorFactory, never())
- .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any())
+ .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any(), any())
}
@Test
@@ -496,7 +502,7 @@
onTaskOpening(task)
verify(mockDesktopModeWindowDecorFactory, never())
- .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any())
+ .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any(), any())
} finally {
mockitoSession.finishMocking()
}
@@ -520,7 +526,7 @@
onTaskOpening(task)
verify(mockDesktopModeWindowDecorFactory)
- .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any())
+ .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any(), any())
} finally {
mockitoSession.finishMocking()
}
@@ -543,7 +549,7 @@
onTaskOpening(task)
verify(mockDesktopModeWindowDecorFactory)
- .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any())
+ .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any(), any())
} finally {
mockitoSession.finishMocking()
}
@@ -682,7 +688,7 @@
val decoration = mock(DesktopModeWindowDecoration::class.java)
whenever(
mockDesktopModeWindowDecorFactory.create(
- any(), any(), any(), eq(task), 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 d860609..2c19fdc 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,7 +82,9 @@
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.windowdecor.WindowDecoration.RelayoutParams;
@@ -165,6 +167,7 @@
private SurfaceControl.Transaction mMockTransaction;
private StaticMockitoSession mMockitoSession;
private TestableContext mTestableContext;
+ private ShellExecutor mBgExecutor = new TestShellExecutor();
/** Set up run before test class. */
@BeforeClass
@@ -657,7 +660,8 @@
MaximizeMenuFactory maximizeMenuFactory) {
final DesktopModeWindowDecoration windowDecor = new DesktopModeWindowDecoration(mContext,
mMockDisplayController, mMockShellTaskOrganizer, taskInfo, mMockSurfaceControl,
- mMockHandler, mMockChoreographer, mMockSyncQueue, mMockRootTaskDisplayAreaOrganizer,
+ 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 adda9a6..e548f8f 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 @@
@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 @@
@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 @@
@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 @@
@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 @@
}
}
- 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 @@
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/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java
index d148afd..a255f73 100644
--- a/media/java/android/media/AudioSystem.java
+++ b/media/java/android/media/AudioSystem.java
@@ -1636,7 +1636,8 @@
/** @hide */ public static final int FORCE_ENCODED_SURROUND_NEVER = 13;
/** @hide */ public static final int FORCE_ENCODED_SURROUND_ALWAYS = 14;
/** @hide */ public static final int FORCE_ENCODED_SURROUND_MANUAL = 15;
- /** @hide */ public static final int NUM_FORCE_CONFIG = 16;
+ /** @hide */ public static final int FORCE_BT_BLE = 16;
+ /** @hide */ public static final int NUM_FORCE_CONFIG = 17;
/** @hide */ public static final int FORCE_DEFAULT = FORCE_NONE;
/** @hide */
@@ -1658,6 +1659,7 @@
case FORCE_ENCODED_SURROUND_NEVER: return "FORCE_ENCODED_SURROUND_NEVER";
case FORCE_ENCODED_SURROUND_ALWAYS: return "FORCE_ENCODED_SURROUND_ALWAYS";
case FORCE_ENCODED_SURROUND_MANUAL: return "FORCE_ENCODED_SURROUND_MANUAL";
+ case FORCE_BT_BLE: return "FORCE_BT_BLE";
default: return "unknown config (" + config + ")" ;
}
}
@@ -2649,4 +2651,11 @@
* @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 999f40e5..1c5049e 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.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 @@
/**
* @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 @@
* <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 @@
* {@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 88770d4..186b69b 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 @@
"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 @@
}
_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 5dd4d29..8de8fbb 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 @@
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 @@
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 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 31b9ccb..e2ab316 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 @@
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/src/com/android/settingslib/spa/framework/common/EntrySliceData.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/EntrySliceData.kt
deleted file mode 100644
index fc551a8..0000000
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/EntrySliceData.kt
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settingslib.spa.framework.common
-
-import androidx.lifecycle.LiveData
-import androidx.slice.Slice
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.launch
-
-open class EntrySliceData : LiveData<Slice?>() {
- private val asyncRunnerScope = CoroutineScope(Dispatchers.IO)
- private var asyncRunnerJob: Job? = null
- private var asyncActionJob: Job? = null
- private var isActive = false
-
- open suspend fun asyncRunner() {}
-
- open suspend fun asyncAction() {}
-
- override fun onActive() {
- asyncRunnerJob?.cancel()
- asyncRunnerJob = asyncRunnerScope.launch { asyncRunner() }
- isActive = true
- }
-
- override fun onInactive() {
- asyncRunnerJob?.cancel()
- asyncRunnerJob = null
- asyncActionJob?.cancel()
- asyncActionJob = null
- isActive = false
- }
-
- fun isActive(): Boolean {
- return isActive
- }
-
- fun doAction() {
- asyncActionJob?.cancel()
- asyncActionJob = asyncRunnerScope.launch { asyncAction() }
- }
-}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt
index 90581b9..3f53091 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt
@@ -16,7 +16,6 @@
package com.android.settingslib.spa.framework.common
-import android.net.Uri
import android.os.Bundle
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
@@ -42,7 +41,6 @@
typealias UiLayerRenderer = @Composable (arguments: Bundle?) -> Unit
typealias StatusDataGetter = (arguments: Bundle?) -> EntryStatusData?
typealias SearchDataGetter = (arguments: Bundle?) -> EntrySearchData?
-typealias SliceDataGetter = (sliceUri: Uri, arguments: Bundle?) -> EntrySliceData?
/**
* Defines data of a Settings entry.
@@ -80,9 +78,6 @@
// If so, for instance, we'll reindex its status for search.
val hasMutableStatus: Boolean = false,
- // Indicate whether the entry has SliceProvider support.
- val hasSliceSupport: Boolean = false,
-
/**
* ========================================
* Defines entry APIs to get data here.
@@ -102,12 +97,6 @@
private val searchDataImpl: SearchDataGetter = { null },
/**
- * API to get Slice data of this entry. The Slice data is implemented as a LiveData,
- * and is associated with the Slice's lifecycle (pin / unpin) by the framework.
- */
- private val sliceDataImpl: SliceDataGetter = { _: Uri, _: Bundle? -> null },
-
- /**
* API to Render UI of this entry directly. For now, we use it in the internal injection, to
* support the case that the injection page owner wants to maintain both data and UI of the
* injected entry. In the long term, we may deprecate the @Composable Page() API in SPP, and
@@ -137,10 +126,6 @@
return searchDataImpl(fullArgument(runtimeArguments))
}
- fun getSliceData(sliceUri: Uri, runtimeArguments: Bundle? = null): EntrySliceData? {
- return sliceDataImpl(sliceUri, fullArgument(runtimeArguments))
- }
-
@Composable
fun UiLayout(runtimeArguments: Bundle? = null) {
val arguments = remember { fullArgument(runtimeArguments) }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntryBuilder.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntryBuilder.kt
index 0d489e8..085c3c6 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntryBuilder.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntryBuilder.kt
@@ -16,7 +16,6 @@
package com.android.settingslib.spa.framework.common
-import android.net.Uri
import android.os.Bundle
import androidx.compose.runtime.remember
import com.android.settingslib.spa.framework.util.genEntryId
@@ -36,13 +35,11 @@
private var isAllowSearch: Boolean = false
private var isSearchDataDynamic: Boolean = false
private var hasMutableStatus: Boolean = false
- private var hasSliceSupport: Boolean = false
// Functions
private var uiLayoutFn: UiLayerRenderer = { }
private var statusDataFn: StatusDataGetter = { null }
private var searchDataFn: SearchDataGetter = { null }
- private var sliceDataFn: SliceDataGetter = { _: Uri, _: Bundle? -> null }
fun build(): SettingsEntry {
val page = fromPage ?: owner
@@ -62,12 +59,10 @@
isAllowSearch = isEnabled && isAllowSearch,
isSearchDataDynamic = isSearchDataDynamic,
hasMutableStatus = hasMutableStatus,
- hasSliceSupport = isEnabled && hasSliceSupport,
// functions
statusDataImpl = statusDataFn,
searchDataImpl = searchDataFn,
- sliceDataImpl = sliceDataFn,
uiLayoutImpl = uiLayoutFn,
)
}
@@ -123,12 +118,6 @@
return this
}
- fun setSliceDataFn(fn: SliceDataGetter): SettingsEntryBuilder {
- this.sliceDataFn = fn
- this.hasSliceSupport = true
- return this
- }
-
fun setUiLayoutFn(fn: UiLayerRenderer): SettingsEntryBuilder {
this.uiLayoutFn = fn
return this
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaEnvironment.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaEnvironment.kt
index 6e5132b..11ae9e9 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaEnvironment.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaEnvironment.kt
@@ -74,7 +74,7 @@
// Set your SpaLogger implementation, for any SPA events logging.
open val logger: SpaLogger = object : SpaLogger {}
- // Specify class name of browse activity and slice broadcast receiver, which is used to
+ // Specify class name of browse activity, which is used to
// generate the necessary intents.
open val browseActivityClass: Class<out Activity>? = null
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SliceUtil.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SliceUtil.kt
deleted file mode 100644
index ec89c7c..0000000
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SliceUtil.kt
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settingslib.spa.slice
-
-import android.net.Uri
-import android.os.Bundle
-import com.android.settingslib.spa.framework.util.KEY_DESTINATION
-import com.android.settingslib.spa.framework.util.KEY_HIGHLIGHT_ENTRY
-
-// Defines SliceUri, which contains special query parameters:
-// -- KEY_DESTINATION: The route that this slice is navigated to.
-// -- KEY_HIGHLIGHT_ENTRY: The entry id of this slice
-// Other parameters can considered as runtime parameters.
-// Use {entryId, runtimeParams} as the unique Id of this Slice.
-typealias SliceUri = Uri
-
-fun SliceUri.getEntryId(): String? {
- return getQueryParameter(KEY_HIGHLIGHT_ENTRY)
-}
-
-fun Uri.Builder.appendSpaParams(
- destination: String? = null,
- entryId: String? = null,
- runtimeArguments: Bundle? = null
-): Uri.Builder {
- if (destination != null) appendQueryParameter(KEY_DESTINATION, destination)
- if (entryId != null) appendQueryParameter(KEY_HIGHLIGHT_ENTRY, entryId)
- if (runtimeArguments != null) {
- for (key in runtimeArguments.keySet()) {
- appendQueryParameter(key, runtimeArguments.getString(key, ""))
- }
- }
- return this
-}
-
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsEntryTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsEntryTest.kt
index ce34979..d0a040c 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsEntryTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsEntryTest.kt
@@ -17,7 +17,6 @@
package com.android.settingslib.spa.framework.common
import android.content.Context
-import android.net.Uri
import androidx.compose.runtime.Composable
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.core.os.bundleOf
@@ -25,8 +24,6 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settingslib.spa.framework.util.genEntryId
import com.android.settingslib.spa.framework.util.genPageId
-import com.android.settingslib.spa.slice.appendSpaParams
-import com.android.settingslib.spa.slice.getEntryId
import com.android.settingslib.spa.tests.testutils.SpaEnvironmentForTest
import com.android.settingslib.spa.tests.testutils.createSettingsPage
import com.google.common.truth.Truth.assertThat
@@ -76,7 +73,6 @@
assertThat(entry.isAllowSearch).isFalse()
assertThat(entry.isSearchDataDynamic).isFalse()
assertThat(entry.hasMutableStatus).isFalse()
- assertThat(entry.hasSliceSupport).isFalse()
}
@Test
@@ -137,7 +133,6 @@
.setIsSearchDataDynamic(false)
.setHasMutableStatus(true)
.setSearchDataFn { null }
- .setSliceDataFn { _, _ -> null }
val entry = entryBuilder.build()
assertThat(entry.id).isEqualTo(genEntryId("myEntry", owner))
assertThat(entry.label).isEqualTo("myEntryDisplay")
@@ -146,7 +141,6 @@
assertThat(entry.isAllowSearch).isTrue()
assertThat(entry.isSearchDataDynamic).isFalse()
assertThat(entry.hasMutableStatus).isTrue()
- assertThat(entry.hasSliceSupport).isTrue()
// Test disabled Spp
val ownerDisabled = createSettingsPage("SppDisabled")
@@ -156,7 +150,6 @@
.setIsSearchDataDynamic(false)
.setHasMutableStatus(true)
.setSearchDataFn { null }
- .setSliceDataFn { _, _ -> null }
val entryDisabled = entryBuilderDisabled.build()
assertThat(entryDisabled.id).isEqualTo(genEntryId("myEntry", ownerDisabled))
assertThat(entryDisabled.label).isEqualTo("myEntryDisplay")
@@ -165,7 +158,6 @@
assertThat(entryDisabled.isAllowSearch).isFalse()
assertThat(entryDisabled.isSearchDataDynamic).isFalse()
assertThat(entryDisabled.hasMutableStatus).isTrue()
- assertThat(entryDisabled.hasSliceSupport).isFalse()
// Clear search data fn
val entry2 = entryBuilder.clearSearchDataFn().build()
@@ -181,7 +173,6 @@
assertThat(entry3.isAllowSearch).isFalse()
assertThat(entry3.isSearchDataDynamic).isFalse()
assertThat(entry3.hasMutableStatus).isTrue()
- assertThat(entry3.hasSliceSupport).isFalse()
}
@Test
@@ -200,7 +191,6 @@
assertThat(entry.isAllowSearch).isTrue()
assertThat(entry.isSearchDataDynamic).isFalse()
assertThat(entry.hasMutableStatus).isFalse()
- assertThat(entry.hasSliceSupport).isFalse()
val searchData = entry.getSearchData(rtArguments)
val statusData = entry.getStatusData(rtArguments)
assertThat(searchData?.title).isEqualTo("myTitle")
@@ -208,25 +198,4 @@
assertThat(statusData?.isDisabled).isTrue()
assertThat(statusData?.isSwitchOff).isTrue()
}
-
- @Test
- fun testSetSliceDataFn() {
- SpaEnvironmentFactory.reset(spaEnvironment)
- val owner = createSettingsPage("SppHome")
- val entryId = genEntryId("myEntry", owner)
- val emptySliceData = EntrySliceData()
-
- val entryBuilder = SettingsEntryBuilder.create(owner, "myEntry").setSliceDataFn { uri, _ ->
- return@setSliceDataFn if (uri.getEntryId() == entryId) emptySliceData else null
- }
- val entry = entryBuilder.build()
- assertThat(entry.id).isEqualTo(entryId)
- assertThat(entry.hasSliceSupport).isTrue()
- assertThat(entry.getSliceData(Uri.EMPTY)).isNull()
- assertThat(
- entry.getSliceData(
- Uri.Builder().scheme("content").appendSpaParams(entryId = entryId).build()
- )
- ).isEqualTo(emptySliceData)
- }
}
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/slice/SliceUtilTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/slice/SliceUtilTest.kt
deleted file mode 100644
index b489afd..0000000
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/slice/SliceUtilTest.kt
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settingslib.spa.slice
-
-import android.net.Uri
-import androidx.core.os.bundleOf
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@RunWith(AndroidJUnit4::class)
-class SliceUtilTest {
- @Test
- fun sliceUriTest() {
- assertThat(Uri.EMPTY.getEntryId()).isNull()
-
- // valid slice uri
- val dest = "myRoute"
- val entryId = "myEntry"
- val sliceUriWithoutParams = Uri.Builder().appendSpaParams(dest, entryId).build()
- assertThat(sliceUriWithoutParams.getEntryId()).isEqualTo(entryId)
-
- val sliceUriWithParams =
- Uri.Builder().appendSpaParams(dest, entryId, bundleOf("p1" to "v1")).build()
- assertThat(sliceUriWithParams.getEntryId()).isEqualTo(entryId)
- }
-}
\ No newline at end of file
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/tests/testutils/SpaEnvironmentForTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/tests/testutils/SpaEnvironmentForTest.kt
index 4f8fd79..9fcf886 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/tests/testutils/SpaEnvironmentForTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/tests/testutils/SpaEnvironmentForTest.kt
@@ -17,9 +17,7 @@
package com.android.settingslib.spa.tests.testutils
import android.app.Activity
-import android.content.BroadcastReceiver
import android.content.Context
-import android.content.Intent
import android.os.Bundle
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -27,7 +25,6 @@
import androidx.navigation.navArgument
import com.android.settingslib.spa.framework.BrowseActivity
import com.android.settingslib.spa.framework.common.EntrySearchData
-import com.android.settingslib.spa.framework.common.EntrySliceData
import com.android.settingslib.spa.framework.common.EntryStatusData
import com.android.settingslib.spa.framework.common.LogCategory
import com.android.settingslib.spa.framework.common.LogEvent
@@ -75,9 +72,6 @@
}
class BlankActivity : BrowseActivity()
-class BlankSliceBroadcastReceiver : BroadcastReceiver() {
- override fun onReceive(p0: Context?, p1: Intent?) {}
-}
object SppHome : SettingsPageProvider {
override val name = "SppHome"
@@ -149,15 +143,7 @@
override fun buildEntry(arguments: Bundle?): List<SettingsEntry> {
val owner = this.createSettingsPage()
return listOf(
- SettingsEntryBuilder.create(owner, "Layer2Entry1")
- .setSliceDataFn { _, _ ->
- return@setSliceDataFn object : EntrySliceData() {
- init {
- postValue(null)
- }
- }
- }
- .build(),
+ SettingsEntryBuilder.create(owner, "Layer2Entry1").build(),
SettingsEntryBuilder.create(owner, "Layer2Entry2").build(),
)
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatteryUtils.java b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatteryUtils.java
index ca3af53..8571360 100644
--- a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatteryUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatteryUtils.java
@@ -83,6 +83,12 @@
return userManager.isManagedProfile() && !userManager.isSystemUser();
}
+ /** Returns true if current user is a private profile user. */
+ public static boolean isPrivateProfile(Context context) {
+ final UserManager userManager = context.getSystemService(UserManager.class);
+ return userManager.isPrivateProfile();
+ }
+
private static Boolean sChargingStringV2Enabled = null;
/** Returns {@code true} if the charging string v2 is enabled. */
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 4028b73..714f951 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 @@
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 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 c92204f..5f7fa27 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 @@
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 @@
}
/**
- * 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 7e60421..90e5189 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 @@
+ 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 2ccf295..0f80edf 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 @@
@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 @@
@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 @@
}
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 7886e85..49b974f 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.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 @@
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 @@
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 @@
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 b2fcb5f..0ff7f84 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 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 @@
/** 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 @@
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 7b994d5..2f7cdd6 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 @@
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/robotests/src/com/android/settingslib/fuelgauge/BatteryUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/fuelgauge/BatteryUtilsTest.java
index 6424352..465798c 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/fuelgauge/BatteryUtilsTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/fuelgauge/BatteryUtilsTest.java
@@ -23,11 +23,11 @@
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import static org.robolectric.Shadows.shadowOf;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.content.Context;
-import android.content.Intent;
import android.content.IntentFilter;
import android.os.UserManager;
import android.provider.Settings;
@@ -41,7 +41,6 @@
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
-import org.robolectric.Shadows;
import org.robolectric.shadows.ShadowAccessibilityManager;
import java.util.Arrays;
@@ -59,12 +58,15 @@
private AccessibilityServiceInfo mAccessibilityServiceInfo1;
@Mock
private AccessibilityServiceInfo mAccessibilityServiceInfo2;
+ @Mock
+ private UserManager mUserManager;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mContext = spy(ApplicationProvider.getApplicationContext());
doReturn(mContext).when(mContext).getApplicationContext();
+ when(mContext.getSystemService(UserManager.class)).thenReturn(mUserManager);
mAccessibilityManager = spy(mContext.getSystemService(AccessibilityManager.class));
mShadowAccessibilityManager = shadowOf(mAccessibilityManager);
doReturn(mAccessibilityManager).when(mContext)
@@ -108,13 +110,23 @@
@Test
public void isWorkProfile_workProfileMode_returnTrue() {
- final UserManager userManager = mContext.getSystemService(UserManager.class);
- Shadows.shadowOf(userManager).setManagedProfile(true);
- Shadows.shadowOf(userManager).setIsSystemUser(false);
+ doReturn(true).when(mUserManager).isManagedProfile();
assertThat(BatteryUtils.isWorkProfile(mContext)).isTrue();
}
+ @Test
+ public void isPrivateProfile_defaultValue_returnFalse() {
+ assertThat(BatteryUtils.isPrivateProfile(mContext)).isFalse();
+ }
+
+ @Test
+ public void isPrivateProfile_privateProfileMode_returnTrue() {
+ doReturn(true).when(mUserManager).isPrivateProfile();
+
+ assertThat(BatteryUtils.isPrivateProfile(mContext)).isTrue();
+ }
+
private void setTtsPackageName(String defaultTtsPackageName) {
Settings.Secure.putString(mContext.getContentResolver(),
Settings.Secure.TTS_DEFAULT_SYNTH, defaultTtsPackageName);
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index a17076b..4e01a71 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -539,6 +539,7 @@
"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 71f5511..7032c73 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -870,6 +870,13 @@
}
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"
@@ -1186,4 +1193,34 @@
namespace: "systemui"
description: "Enables MSDL feedback in SysUI surfaces."
bug: "352600066"
-}
\ No newline at end of file
+}
+
+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"
+ bug: "351426938"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+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 92f03d7..35db9e0 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 @@
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 768e653..1c02d3f 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 @@
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 @@
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 @@
)
}
+ 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 c34fb38..c993855 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 @@
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 d629eec..f8bd633 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.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 0398133..a22bc34 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 @@
/** [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 @@
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 @@
// 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/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
index 776e166..c4970c5 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
@@ -145,6 +145,7 @@
}
// Note: boundsInWindow doesn't scroll off the screen
stackScrollView.setHeadsUpTop(boundsInWindow.top)
+ stackScrollView.setHeadsUpBottom(boundsInWindow.bottom)
}
)
}
@@ -471,7 +472,11 @@
)
}
if (shouldIncludeHeadsUpSpace) {
- HeadsUpNotificationSpace(stackScrollView = stackScrollView, viewModel = viewModel)
+ HeadsUpNotificationSpace(
+ stackScrollView = stackScrollView,
+ viewModel = viewModel,
+ modifier = Modifier.padding(top = topPadding)
+ )
}
}
}
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 2d5d259..3cf8e70 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 @@
if (isCustomizerShowing) {
Modifier.fillMaxHeight().align(Alignment.TopCenter)
} else {
- Modifier.verticalNestedScrollToScene()
- .verticalScroll(
+ Modifier.verticalScroll(
scrollState,
enabled = isScrollable,
)
@@ -408,7 +407,7 @@
HeadsUpNotificationSpace(
stackScrollView = notificationStackScrollView,
viewModel = notificationsPlaceholderViewModel,
- modifier = Modifier.align(Alignment.BottomCenter),
+ modifier = Modifier.align(Alignment.BottomCenter).navigationBarsPadding(),
isPeekFromBottom = true,
)
NotificationScrollingStack(
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 df47cba..7d46c75 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.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 @@
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 805351e..ece8b40 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 @@
.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 20b1303..78ba7de 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 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 @@
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 615d393..2b78b5a 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.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 @@
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 @@
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 ddff2f7..945043d 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 @@
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 @@
* 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 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 scrollBehaviorOwner: ScrollBehaviorOwner? = null
- private var nestedScrollNode: DelegatableNode =
- nestedScrollModifierNode(
- connection = priorityNestedScrollConnection,
- dispatcher = 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 @@
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 0c467b1..82275a9 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 @@
* @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 @@
* @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 aeb6262..b8010f2 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.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 @@
draggableHandler: DraggableHandlerImpl,
swipeDetector: SwipeDetector,
) : DelegatingNode(), PointerInputModifierNode {
- private val delegate =
+ private val multiPointerDraggableNode =
delegate(
MultiPointerDraggableNode(
orientation = draggableHandler.orientation,
@@ -74,21 +78,41 @@
// 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 @@
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 7988e0e..c91151e 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 @@
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 @@
) {
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 0000000..311a580
--- /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 5cf4f16..7fd9ce2 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 @@
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/bouncer/domain/interactor/PrimaryBouncerInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorTest.kt
index 546a6b7..d5e1fae 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorTest.kt
@@ -160,6 +160,12 @@
}
@Test
+ fun testShowReturnsFalseWhenDelegateIsNotSet() {
+ whenever(bouncerView.delegate).thenReturn(null)
+ assertThat(underTest.show(true)).isEqualTo(false)
+ }
+
+ @Test
fun testShow_isResumed() {
whenever(repository.primaryBouncerShow.value).thenReturn(true)
whenever(keyguardSecurityModel.getSecurityMode(anyInt()))
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 a8bdc7c..1f5e30c 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 @@
}
@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 5cdbe9c..9539c04 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 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 @@
)
}
+ @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 7b79d28..9a92f76 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 @@
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/view/viewmodel/CommunalEditModeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
index b138fb3..f8906ad 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.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 @@
}
@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 73ef775..88ba041 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 @@
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 @@
}
)
}
+
+ @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 0000000..ee51e37
--- /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 96b4b43..2b2c121 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 @@
}
@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 @@
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 @@
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/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModelTest.kt
index be0d899..9e69601 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 @@
@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 @@
@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 63d06a4..41c5b73 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 @@
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 @@
}
@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 @@
}
@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 @@
}
@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 278c90a..fd2e335 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 @@
)
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 @@
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 9b9e584..d5c9102 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 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 @@
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 0000000..3baf2f4
--- /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 fd1b213..540a85a 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 @@
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 @@
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 @@
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 0000000..8810ade
--- /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 0000000..6ddc074
--- /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/domain/interactor/HeadsUpNotificationInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt
index 8b4265f..14134cc 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.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 5e87f46..61873ad 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 @@
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 @@
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 f8e6337..f96cf10 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.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 71cd95f..6f09931 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 @@
)
)
runCurrent()
- assertThat(alpha).isEqualTo(0f)
+ // Resets to 1f after communal scene is hidden
+ assertThat(alpha).isEqualTo(1f)
}
@Test
@@ -1151,7 +1152,7 @@
)
)
runCurrent()
- assertThat(alpha).isEqualTo(0f)
+ assertThat(alpha).isEqualTo(1f)
}
@Test
@@ -1208,7 +1209,8 @@
)
)
runCurrent()
- assertThat(alpha).isEqualTo(0f)
+ // Resets to 1f after communal scene is hidden
+ assertThat(alpha).isEqualTo(1f)
}
@Test
@@ -1263,7 +1265,7 @@
)
)
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 b643968..c3c5a48 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 @@
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 10a2f64..d82b9db 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 @@
// 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 @@
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 495ab61..8f9da3b 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 @@
}
@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 d0ddbff..5dadc4c 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.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.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 @@
}
@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 0000000..fdfc7f1
--- /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/layout/rich_ongoing_timer_notification.xml b/packages/SystemUI/res/layout/rich_ongoing_timer_notification.xml
index f2bfbe5c9..3a679e3 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 763930d..07a40c8 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 2bd97d9..68c83c7 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 845ca5e..3019fe7 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 @@
* 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 10d1891..0f61233 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.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 @@
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 f1fb45c..baca959 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 @@
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 4035e95..efa55e9 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 @@
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 5c53234..e634726 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 @@
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 @@
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 @@
private ColorProfile getCurrentColorProfile() {
return getColorProfile(
mPowerSaveEnabled,
- mIsBatteryDefender && mDisplayShieldEnabled,
+ mIsBatteryDefender,
mPluggedIn,
mLevel <= 20);
}
@@ -410,10 +409,6 @@
mBatteryEstimateFetcher = fetcher;
}
- void setDisplayShieldEnabled(boolean displayShieldEnabled) {
- mDisplayShieldEnabled = displayShieldEnabled;
- }
-
void updatePercentText() {
if (!newStatusBarIcons()) {
updatePercentTextLegacy();
@@ -659,7 +654,7 @@
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 4f13e6f..9a30c21 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.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 @@
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 d2c6227..b10d37e 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.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
@@ -61,14 +63,16 @@
/**
* Controls two ripple effects:
- * 1. Unlocked ripple: shows when authentication is successful
- * 2. UDFPS dwell ripple: shows when the user has their finger down on the UDFPS area and reacts
- * to errors and successes
+ * 1. Unlocked ripple: shows when authentication is successful
+ * 2. UDFPS dwell ripple: shows when the user has their finger down on the UDFPS area and reacts to
+ * errors and successes
*
* The ripple uses the accent color of the current theme.
*/
@SysUISingleton
-class AuthRippleController @Inject constructor(
+class AuthRippleController
+@Inject
+constructor(
private val sysuiContext: Context,
private val authController: AuthController,
private val configurationController: ConfigurationController,
@@ -92,8 +96,7 @@
KeyguardStateController.Callback,
WakefulnessLifecycle.Observer {
- @VisibleForTesting
- internal var startLightRevealScrimOnKeyguardFadingAway = false
+ @VisibleForTesting internal var startLightRevealScrimOnKeyguardFadingAway = false
var lightRevealScrimAnimator: ValueAnimator? = null
var fingerprintSensorLocation: Point? = null
private var faceSensorLocation: Point? = null
@@ -101,6 +104,7 @@
private var udfpsController: UdfpsController? = null
private var udfpsRadius: Float = -1f
+ private var udfpsType: FingerprintSensorType = FingerprintSensorType.UNKNOWN
override fun start() {
init()
@@ -141,15 +145,17 @@
private val biometricModeListener =
object : BiometricUnlockController.BiometricUnlockEventsListener {
override fun onBiometricUnlockedWithKeyguardDismissal(
- biometricSourceType: BiometricSourceType?
+ biometricSourceType: BiometricSourceType?
) {
DeviceEntryUdfpsRefactor.assertInLegacyMode()
if (biometricSourceType != null) {
showUnlockRippleInternal(biometricSourceType)
} else {
- logger.log(TAG,
- LogLevel.ERROR,
- "Unexpected scenario where biometricSourceType is null")
+ logger.log(
+ TAG,
+ LogLevel.ERROR,
+ "Unexpected scenario where biometricSourceType is null"
+ )
}
}
}
@@ -168,16 +174,16 @@
notificationShadeWindowController.setForcePluginOpen(false, this)
}
- @Deprecated("Update authRippleInteractor.showUnlockRipple instead of calling this.")
- fun showUnlockRipple(biometricSourceType: BiometricSourceType) {
- DeviceEntryUdfpsRefactor.assertInLegacyMode()
- showUnlockRippleInternal(biometricSourceType)
- }
+ @Deprecated("Update authRippleInteractor.showUnlockRipple instead of calling this.")
+ fun showUnlockRipple(biometricSourceType: BiometricSourceType) {
+ DeviceEntryUdfpsRefactor.assertInLegacyMode()
+ showUnlockRippleInternal(biometricSourceType)
+ }
private fun showUnlockRippleInternal(biometricSourceType: BiometricSourceType) {
val keyguardNotShowing = !keyguardStateController.isShowing
- val unlockNotAllowed = !keyguardUpdateMonitor
- .isUnlockingWithBiometricAllowed(biometricSourceType)
+ val unlockNotAllowed =
+ !keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(biometricSourceType)
if (keyguardNotShowing || unlockNotAllowed) {
logger.notShowingUnlockRipple(keyguardNotShowing, unlockNotAllowed)
return
@@ -187,30 +193,32 @@
if (biometricSourceType == BiometricSourceType.FINGERPRINT) {
fingerprintSensorLocation?.let {
mView.setFingerprintSensorLocation(it, udfpsRadius)
- circleReveal = CircleReveal(
+ circleReveal =
+ CircleReveal(
it.x,
it.y,
0,
Math.max(
- Math.max(it.x, displayMetrics.widthPixels - it.x),
- Math.max(it.y, displayMetrics.heightPixels - it.y)
+ Math.max(it.x, displayMetrics.widthPixels - it.x),
+ Math.max(it.y, displayMetrics.heightPixels - it.y)
)
- )
+ )
logger.showingUnlockRippleAt(it.x, it.y, "FP sensor radius: $udfpsRadius")
showUnlockedRipple()
}
} else if (biometricSourceType == BiometricSourceType.FACE) {
faceSensorLocation?.let {
mView.setSensorLocation(it)
- circleReveal = CircleReveal(
+ circleReveal =
+ CircleReveal(
it.x,
it.y,
0,
Math.max(
- Math.max(it.x, displayMetrics.widthPixels - it.x),
- Math.max(it.y, displayMetrics.heightPixels - it.y)
+ Math.max(it.x, displayMetrics.widthPixels - it.x),
+ Math.max(it.y, displayMetrics.heightPixels - it.y)
)
- )
+ )
logger.showingUnlockRippleAt(it.x, it.y, "Face unlock ripple")
showUnlockedRipple()
}
@@ -234,9 +242,7 @@
mView.startUnlockedRipple(
/* end runnable */
- Runnable {
- notificationShadeWindowController.setForcePluginOpen(false, this)
- }
+ Runnable { notificationShadeWindowController.setForcePluginOpen(false, this) }
)
}
@@ -248,32 +254,36 @@
if (keyguardStateController.isKeyguardFadingAway) {
if (startLightRevealScrimOnKeyguardFadingAway) {
lightRevealScrimAnimator?.cancel()
- lightRevealScrimAnimator = ValueAnimator.ofFloat(.1f, 1f).apply {
- interpolator = Interpolators.LINEAR_OUT_SLOW_IN
- duration = RIPPLE_ANIMATION_DURATION
- startDelay = keyguardStateController.keyguardFadingAwayDelay
- addUpdateListener { animator ->
- if (lightRevealScrim.revealEffect != circleReveal) {
- // if something else took over the reveal, let's cancel ourselves
- cancel()
- return@addUpdateListener
- }
- lightRevealScrim.revealAmount = animator.animatedValue as Float
- }
- addListener(object : AnimatorListenerAdapter() {
- override fun onAnimationEnd(animation: Animator) {
- // Reset light reveal scrim to the default, so the CentralSurfaces
- // can handle any subsequent light reveal changes
- // (ie: from dozing changes)
- if (lightRevealScrim.revealEffect == circleReveal) {
- lightRevealScrim.revealEffect = LiftReveal
+ lightRevealScrimAnimator =
+ ValueAnimator.ofFloat(.1f, 1f).apply {
+ interpolator = Interpolators.LINEAR_OUT_SLOW_IN
+ duration = RIPPLE_ANIMATION_DURATION
+ startDelay = keyguardStateController.keyguardFadingAwayDelay
+ addUpdateListener { animator ->
+ if (lightRevealScrim.revealEffect != circleReveal) {
+ // if something else took over the reveal, let's cancel ourselves
+ cancel()
+ return@addUpdateListener
}
-
- lightRevealScrimAnimator = null
+ lightRevealScrim.revealAmount = animator.animatedValue as Float
}
- })
- start()
- }
+ addListener(
+ object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator) {
+ // Reset light reveal scrim to the default, so the
+ // CentralSurfaces
+ // can handle any subsequent light reveal changes
+ // (ie: from dozing changes)
+ if (lightRevealScrim.revealEffect == circleReveal) {
+ lightRevealScrim.revealEffect = LiftReveal
+ }
+
+ lightRevealScrimAnimator = null
+ }
+ }
+ )
+ start()
+ }
startLightRevealScrimOnKeyguardFadingAway = false
}
}
@@ -297,8 +307,9 @@
}
private fun updateRippleColor() {
- mView.setLockScreenColor(Utils.getColorAttrDefaultColor(sysuiContext,
- R.attr.wallpaperTextColorAccent))
+ mView.setLockScreenColor(
+ Utils.getColorAttrDefaultColor(sysuiContext, R.attr.wallpaperTextColorAccent)
+ )
}
private fun showDwellRipple() {
@@ -321,46 +332,52 @@
}
}
- override fun onBiometricAuthFailed(biometricSourceType: BiometricSourceType) {
- if (biometricSourceType == BiometricSourceType.FINGERPRINT) {
- mView.retractDwellRipple()
+ override fun onBiometricAuthFailed(biometricSourceType: BiometricSourceType) {
+ if (biometricSourceType == BiometricSourceType.FINGERPRINT) {
+ mView.retractDwellRipple()
+ }
}
- }
- override fun onBiometricAcquired(
- biometricSourceType: BiometricSourceType,
- acquireInfo: Int
- ) {
- if (biometricSourceType == BiometricSourceType.FINGERPRINT &&
- BiometricFingerprintConstants.shouldDisableUdfpsDisplayMode(acquireInfo) &&
- acquireInfo != BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_GOOD) {
- // received an 'acquiredBad' message, so immediately retract
- mView.retractDwellRipple()
+ override fun onBiometricAcquired(
+ biometricSourceType: BiometricSourceType,
+ acquireInfo: Int
+ ) {
+ if (
+ biometricSourceType == BiometricSourceType.FINGERPRINT &&
+ BiometricFingerprintConstants.shouldDisableUdfpsDisplayMode(acquireInfo) &&
+ acquireInfo != BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_GOOD
+ ) {
+ // received an 'acquiredBad' message, so immediately retract
+ mView.retractDwellRipple()
+ }
}
- }
- override fun onKeyguardBouncerStateChanged(bouncerIsOrWillBeShowing: Boolean) {
- if (bouncerIsOrWillBeShowing) {
- mView.fadeDwellRipple()
+ override fun onKeyguardBouncerStateChanged(bouncerIsOrWillBeShowing: Boolean) {
+ if (bouncerIsOrWillBeShowing) {
+ mView.fadeDwellRipple()
+ }
}
}
- }
private val configurationChangedListener =
object : ConfigurationController.ConfigurationListener {
override fun onUiModeChanged() {
updateRippleColor()
}
+
override fun onThemeChanged() {
updateRippleColor()
}
- }
+ }
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()
}
}
@@ -386,6 +403,7 @@
if (it.size > 0) {
udfpsController = udfpsControllerProvider.get()
udfpsRadius = authController.udfpsRadius
+ udfpsType = it[0].sensorType.toSensorType()
if (mView.isAttachedToWindow) {
udfpsController?.addCallback(udfpsControllerCallback)
@@ -402,9 +420,11 @@
when (args[0]) {
"dwell" -> {
showDwellRipple()
- pw.println("lock screen dwell ripple: " +
+ pw.println(
+ "lock screen dwell ripple: " +
"\n\tsensorLocation=$fingerprintSensorLocation" +
- "\n\tudfpsRadius=$udfpsRadius")
+ "\n\tudfpsRadius=$udfpsRadius"
+ )
}
"fingerprint" -> {
pw.println("fingerprint ripple sensorLocation=$fingerprintSensorLocation")
@@ -416,9 +436,11 @@
showUnlockRippleInternal(BiometricSourceType.FACE)
}
"custom" -> {
- if (args.size != 3 ||
- args[1].toFloatOrNull() == null ||
- args[2].toFloatOrNull() == null) {
+ if (
+ args.size != 3 ||
+ args[1].toFloatOrNull() == null ||
+ args[2].toFloatOrNull() == null
+ ) {
invalidCommand(pw)
return
}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt
index 3c5e571..c28bce2 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt
@@ -134,7 +134,7 @@
// TODO(b/243695312): Encapsulate all of the show logic for the bouncer.
/** Show the bouncer if necessary and set the relevant states. */
@JvmOverloads
- fun show(isScrimmed: Boolean) {
+ fun show(isScrimmed: Boolean): Boolean {
// When the scene container framework is enabled, instead of calling this, call
// SceneInteractor#changeScene(Scenes.Bouncer, ...).
SceneContainerFlag.assertInLegacyMode()
@@ -146,44 +146,48 @@
"primaryBouncerDelegate is set. Let's exit early so we don't " +
"set the wrong primaryBouncer state."
)
- return
+ return false
}
- // Reset some states as we show the bouncer.
- repository.setKeyguardAuthenticatedBiometrics(null)
- repository.setPrimaryStartingToHide(false)
+ try {
+ Trace.beginSection("KeyguardBouncer#show")
+ // Reset some states as we show the bouncer.
+ repository.setKeyguardAuthenticatedBiometrics(null)
+ repository.setPrimaryStartingToHide(false)
- val resumeBouncer =
- (isBouncerShowing() || repository.primaryBouncerShowingSoon.value) &&
- needsFullscreenBouncer()
+ val resumeBouncer =
+ (isBouncerShowing() || repository.primaryBouncerShowingSoon.value) &&
+ needsFullscreenBouncer()
- Trace.beginSection("KeyguardBouncer#show")
- repository.setPrimaryScrimmed(isScrimmed)
- if (isScrimmed) {
- setPanelExpansion(KeyguardBouncerConstants.EXPANSION_VISIBLE)
+ repository.setPrimaryScrimmed(isScrimmed)
+ if (isScrimmed) {
+ setPanelExpansion(KeyguardBouncerConstants.EXPANSION_VISIBLE)
+ }
+
+ // In this special case, we want to hide the bouncer and show it again. We want to emit
+ // show(true) again so that we can reinflate the new view.
+ if (resumeBouncer) {
+ repository.setPrimaryShow(false)
+ }
+
+ if (primaryBouncerView.delegate?.showNextSecurityScreenOrFinish() == true) {
+ // Keyguard is done.
+ return false
+ }
+
+ repository.setPrimaryShowingSoon(true)
+ if (usePrimaryBouncerPassiveAuthDelay()) {
+ Log.d(TAG, "delay bouncer, passive auth may succeed")
+ mainHandler.postDelayed(showRunnable, passiveAuthBouncerDelay)
+ } else {
+ DejankUtils.postAfterTraversal(showRunnable)
+ }
+ keyguardStateController.notifyPrimaryBouncerShowing(true)
+ primaryBouncerCallbackInteractor.dispatchStartingToShow()
+ return true
+ } finally {
+ Trace.endSection()
}
-
- // In this special case, we want to hide the bouncer and show it again. We want to emit
- // show(true) again so that we can reinflate the new view.
- if (resumeBouncer) {
- repository.setPrimaryShow(false)
- }
-
- if (primaryBouncerView.delegate?.showNextSecurityScreenOrFinish() == true) {
- // Keyguard is done.
- return
- }
-
- repository.setPrimaryShowingSoon(true)
- if (usePrimaryBouncerPassiveAuthDelay()) {
- Log.d(TAG, "delay bouncer, passive auth may succeed")
- mainHandler.postDelayed(showRunnable, passiveAuthBouncerDelay)
- } else {
- DejankUtils.postAfterTraversal(showRunnable)
- }
- keyguardStateController.notifyPrimaryBouncerShowing(true)
- primaryBouncerCallbackInteractor.dispatchStartingToShow()
- Trace.endSection()
}
/** Sets the correct bouncer states to hide the bouncer. */
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 85e2bdb..b6ace81 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 @@
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 @@
attrs,
) {
+ init {
+ setupAccessibilityDelegate()
+ }
+
constructor(
context: Context,
attrs: AttributeSet?,
@@ -55,6 +63,7 @@
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 @@
var listener: Listener? = null
+ var accessibilityHintLongPressAction: AccessibilityAction? = null
+
private val interactionHandler: LongPressHandlingViewInteractionHandler by lazy {
LongPressHandlingViewInteractionHandler(
postDelayed = { block, timeoutMs ->
@@ -107,6 +118,51 @@
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/data/repository/CommunalPrefsRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalPrefsRepository.kt
index d8067b8..4de39c4 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 @@
/** 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 @@
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 @@
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 @@
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/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
index 3fffd76..e13161f 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.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.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 @@
@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 @@
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 @@
}
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 3517650..0b5f40d 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 @@
private val repository: CommunalPrefsRepository,
userInteractor: SelectedUserInteractor,
private val userTracker: UserTracker,
- @CommunalTableLog tableLogBuffer: TableLogBuffer
+ @CommunalTableLog tableLogBuffer: TableLogBuffer,
) {
val isCtaDismissed: Flow<Boolean> =
@@ -64,25 +63,6 @@
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/ui/viewmodel/CommunalEditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
index 7b0aadf..0353d2c 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 @@
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 7f8103e..6864f4e 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 @@
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 @@
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 e2ad774..3f937bb 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 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 @@
* 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 @@
* 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 @@
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 @@
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 ea0e59b..9b95ac4 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 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 @@
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 @@
*/
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 @@
}
/**
+ * 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 3294c81..b45ebd8 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 @@
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 53b9261..0e2e2e6 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 @@
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 0000000..e2aa911
--- /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 0000000..643e571
--- /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 af7ecf6..0e06117 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.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.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 @@
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 @@
// 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 3d3584e..d0beb7a 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 @@
// 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 893835a..59ec87a 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 @@
.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 cd28bec..8f50b03 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.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 @@
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 @@
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 ec03a6d..046e79c 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 @@
* 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 @@
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 7a06d2f..cd49c6a 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 @@
}
}
- /** 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 76f7749..1b9788f 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 @@
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 057b4f9..b387855 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 @@
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 bb4fb79..e9db1d2 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 @@
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 f1cbf25..1d2edc6 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 @@
/** 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 46ba5d1..8811908 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 @@
1f - it
}
},
+ onFinish = { 1f },
)
/** Bouncer container alpha */
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 46c5c18..c5d7b25 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 @@
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 1dbd500..c4abcd2 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.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 @@
// 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 8af5665..ee709c4 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.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 0000000..664d496
--- /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 720120b..5ea8c21 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 @@
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 b91891c..a300031 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.res.R
import javax.inject.Inject
import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
class ModesTile
@Inject
@@ -91,8 +92,8 @@
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 @@
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 fd1f3d8..4c6563d 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 @@
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 @@
}
}
}
+
+ 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 26b9a4c..7048ada 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 @@
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 8711e88..72f37fc 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 @@
resetShadeSessions()
handleKeyguardEnabledness()
notifyKeyguardDismissCallbacks()
+ refreshLockscreenEnabled()
} else {
sceneLogger.logFrameworkEnabled(
isEnabled = false,
@@ -186,7 +187,6 @@
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 @@
}
}
}
+
+ /**
+ * 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 4f27b9e..b3d5c9e 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 @@
original,
updates.component,
updates.owner,
- type.displayId
+ type.displayId,
)
}
return updated
@@ -120,6 +120,7 @@
return replaceWithScreenshot(
original = original,
componentName = topMainRootTask?.topActivity ?: defaultComponent,
+ taskId = topMainRootTask?.taskId,
owner = defaultOwner,
displayId = original.displayId
)
@@ -144,11 +145,12 @@
)
}
- 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 @@
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 d870fe6..d090aea 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.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 @@
*
* 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 @@
)
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 @@
* 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 @@
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 4f6a64f..bd08685 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 @@
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 5065baa..23e2620 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 @@
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 11ccdff..59fd0ca 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 @@
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 @@
}
}
}
- .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 bafec38..6ea72b9 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 @@
// 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 7dc9b25..b0c8321 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 @@
// 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 afa9cce..d9b0504 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 @@
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.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 @@
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 @@
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 @@
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 @@
}
}
}
- .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 @@
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 @@
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 @@
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 @@
EndCastScreenToOtherDeviceDialogDelegate(
endMediaProjectionDialogHelper,
context,
- stopAction = this::stopProjecting,
+ stopAction = this::stopProjectingFromDialog,
state,
)
@@ -228,7 +247,7 @@
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 6004365..2d9ccb7 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 @@
@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 @@
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 1eca827..72656ca 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 @@
// 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 0c34981..fcf3de4 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 @@
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.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 @@
@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 @@
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 @@
}
}
}
- // 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 @@
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 564f20e..d10bd77 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 @@
// 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 ddebd3a..85973fc 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 @@
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.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 @@
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 @@
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 @@
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 b2140f7..130b117 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 @@
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 @@
/** 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 40f86f9..17cf60b 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 @@
/** 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 0000000..92e72c2
--- /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 ee010f7..2fc366b 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 @@
/** 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 15c348e..b0d897d 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.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 @@
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 3dcaff3..b342722 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 @@
}
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 e48c28d..cb133ec 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 @@
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 55c6790..b1b2a65 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 @@
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 @@
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 @@
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 0000000..a6605f6
--- /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 0000000..e44a77c
--- /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 e038982..73ce48b 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 @@
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 @@
hideLocallyDismissedNotifsCoordinator: HideLocallyDismissedNotifsCoordinator,
hideNotifsForOtherUsersCoordinator: HideNotifsForOtherUsersCoordinator,
keyguardCoordinator: KeyguardCoordinator,
+ unseenKeyguardCoordinator: OriginalUnseenKeyguardCoordinator,
+ lockScreenMinimalismCoordinator: LockScreenMinimalismCoordinator,
rankingCoordinator: RankingCoordinator,
colorizedFgsCoordinator: ColorizedFgsCoordinator,
deviceProvisionedCoordinator: DeviceProvisionedCoordinator,
@@ -82,6 +88,11 @@
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 @@
}
// 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 @@
}
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 0000000..5b25b11
--- /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 caa6c17..71c98b8 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
@@ -150,8 +150,9 @@
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);
}
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 5adf31b..0c7ba15 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 @@
/** 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 @@
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 @@
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 bf44b9f..24b75d4 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 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 @@
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 @@
flowOf(false)
} else {
combine(hasPinnedRows, headsUpRepository.isHeadsUpAnimatingAway) {
- hasPinnedRows,
- animatingAway ->
- hasPinnedRows || animatingAway
- }
+ hasPinnedRows,
+ animatingAway ->
+ hasPinnedRows || animatingAway
+ }
}
}
@@ -127,6 +137,9 @@
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 85c66bd..948a3c2 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 @@
/** 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 b8af369..fe86375 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 @@
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 @@
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 @@
}
}
- 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 5584701..33b2564 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 @@
) : 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 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 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 0d83ace..8c95187 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 @@
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 @@
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 2e164d6..d481b50 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 @@
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 @@
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 c9ff589..042d1bc 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 @@
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 a85c87f..768a093 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 @@
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 @@
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 @@
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 @@
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()
+ )
}
}
- data class ButtonViewModel(
- val pendingIntent: PendingIntent?,
- @StringRes val labelRes: Int,
- @DrawableRes val iconRes: Int,
- )
+ val resetButtonModel: Flow<ButtonViewModel?> =
+ state.map {
+ it.resetAction?.let { action ->
+ ButtonViewModel.WithCustomAttrs(
+ action.actionIntent,
+ action.title, // "Reset",
+ action.getIcon()
+ )
+ }
+ }
+
+ 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 bf37036..06f3db5 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 @@
/** Helper for reading or using the minimalism prototype flag state. */
@Suppress("NOTHING_TO_INLINE")
object NotificationMinimalismPrototype {
+ const val FLAG_NAME = Flags.FLAG_NOTIFICATION_MINIMALISM_PROTOTYPE
- val version: Int by lazy {
- SystemProperties.getInt("persist.notification_minimalism_prototype.version", 2)
- }
+ /** A token used for dependency declaration */
+ val token: FlagToken
+ get() = FlagToken(FLAG_NAME, isEnabled)
- object V1 {
- /** The aconfig flag name */
- const val FLAG_NAME = Flags.FLAG_NOTIFICATION_MINIMALISM_PROTOTYPE
+ /** Is the heads-up cycling animation enabled */
+ @JvmStatic
+ inline val isEnabled
+ get() = Flags.notificationMinimalismPrototype()
- /** A token used for dependency declaration */
- val token: FlagToken
- get() = FlagToken(FLAG_NAME, isEnabled)
+ /**
+ * 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
+ )
- /** Is the heads-up cycling animation enabled */
- @JvmStatic
- inline val isEnabled
- get() = Flags.notificationMinimalismPrototype() && version == 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)
- /**
- * 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)
- }
+ /**
+ * 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/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
index fbddc06..7c3072d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
@@ -66,6 +66,8 @@
private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
private float mStackTop;
private float mStackCutoff;
+ private float mHeadsUpTop;
+ private float mHeadsUpBottom;
private int mScrollY;
private float mOverScrollTopAmount;
private float mOverScrollBottomAmount;
@@ -377,6 +379,30 @@
this.mStackCutoff = stackCutoff;
}
+ /** y coordinate of the top position of a pinned HUN */
+ public float getHeadsUpTop() {
+ if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return 0f;
+ return mHeadsUpTop;
+ }
+
+ /** @see #getHeadsUpTop() */
+ public void setHeadsUpTop(float mHeadsUpTop) {
+ if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
+ this.mHeadsUpTop = mHeadsUpTop;
+ }
+
+ /** the bottom-most y position where we can draw pinned HUNs */
+ public float getHeadsUpBottom() {
+ if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return 0f;
+ return mHeadsUpBottom;
+ }
+
+ /** @see #getHeadsUpBottom() */
+ public void setHeadsUpBottom(float headsUpBottom) {
+ if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
+ mHeadsUpBottom = headsUpBottom;
+ }
+
public int getScrollY() {
return mScrollY;
}
@@ -784,7 +810,9 @@
@Override
public void dump(PrintWriter pw, String[] args) {
pw.println("mStackTop=" + mStackTop);
- pw.println("mStackCutoff" + mStackCutoff);
+ pw.println("mStackCutoff=" + mStackCutoff);
+ pw.println("mHeadsUpTop=" + mHeadsUpTop);
+ pw.println("mHeadsUpBottom=" + mHeadsUpBottom);
pw.println("mTopPadding=" + mTopPadding);
pw.println("mStackTopMargin=" + mStackTopMargin);
pw.println("mStackTranslation=" + mStackTranslation);
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 2081adc..4a447b7 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
@@ -834,7 +834,7 @@
y = (int) mAmbientState.getStackCutoff();
drawDebugInfo(canvas, y, Color.MAGENTA, /* label= */ "getStackCutoff() = " + y);
- y = (int) mScrollViewFields.getHeadsUpTop();
+ y = (int) mAmbientState.getHeadsUpTop();
drawDebugInfo(canvas, y, Color.GREEN, /* label= */ "getHeadsUpTop() = " + y);
y += getTopHeadsUpHeight();
@@ -1222,7 +1222,12 @@
@Override
public void setHeadsUpTop(float headsUpTop) {
- mScrollViewFields.setHeadsUpTop(headsUpTop);
+ mAmbientState.setHeadsUpTop(headsUpTop);
+ }
+
+ @Override
+ public void setHeadsUpBottom(float headsUpBottom) {
+ mAmbientState.setHeadsUpBottom(headsUpBottom);
}
@Override
@@ -4857,14 +4862,20 @@
* @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());
+ + " addAnimation=" + addAnimation
+ + (row.getEntry() == null ? " entry NULL "
+ : " isSeenInShade=" + row.getEntry().isSeenInShade()
+ + " row=" + row.getEntry().getKey())
+ + " mIsExpanded=" + mIsExpanded
+ + " isHeadsUp=" + isHeadsUp);
}
- if (add) {
+ 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))) {
@@ -4894,6 +4905,7 @@
* @param bottomBarHeight the height of the bar on the bottom
*/
public void setHeadsUpBoundaries(int height, int bottomBarHeight) {
+ SceneContainerFlag.assertInLegacyMode();
mAmbientState.setMaxHeadsUpTranslation(height - bottomBarHeight);
mStackScrollAlgorithm.setHeadsUpAppearHeightBottom(height);
mStateAnimator.setHeadsUpAppearHeightBottom(height);
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 391bc43..06222fd 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 @@
/** 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 @@
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/notification/stack/ScrollViewFields.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt
index 97ec391..383d8b3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt
@@ -32,8 +32,6 @@
class ScrollViewFields {
/** Used to produce the clipping path */
var scrimClippingShape: ShadeScrimShape? = null
- /** Y coordinate in view pixels of the top of the HUN */
- var headsUpTop: Float = 0f
/** Whether the notifications are scrolled all the way to the top (i.e. when freshly opened) */
var isScrolledToTop: Boolean = true
@@ -74,7 +72,6 @@
fun dump(pw: IndentingPrintWriter) {
pw.printSection("StackViewStates") {
pw.println("scrimClippingShape", scrimClippingShape)
- pw.println("headsUpTop", headsUpTop)
pw.println("isScrolledToTop", isScrolledToTop)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
index 4282fa2..b801e5d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
@@ -79,9 +79,7 @@
private int mHeadsUpAppearHeightBottom;
private int mHeadsUpCyclingPadding;
- public StackScrollAlgorithm(
- Context context,
- ViewGroup hostView) {
+ public StackScrollAlgorithm(Context context, ViewGroup hostView) {
mHostView = hostView;
initView(context);
}
@@ -865,7 +863,10 @@
// Move the tracked heads up into position during the appear animation, by interpolating
// between the HUN inset (where it will appear as a HUN) and the end position in the shade
- float headsUpTranslation = mHeadsUpInset - ambientState.getStackTopMargin();
+ float headsUpTranslation =
+ SceneContainerFlag.isEnabled()
+ ? ambientState.getHeadsUpTop()
+ : mHeadsUpInset - ambientState.getStackTopMargin();
ExpandableNotificationRow trackedHeadsUpRow = ambientState.getTrackedHeadsUpRow();
if (trackedHeadsUpRow != null) {
ExpandableViewState childState = trackedHeadsUpRow.getViewState();
@@ -894,21 +895,44 @@
boolean isTopEntry = topHeadsUpEntry == row;
float unmodifiedEndLocation = childState.getYTranslation() + childState.height;
if (mIsExpanded) {
- if (shouldHunBeVisibleWhenScrolled(row.mustStayOnScreen(),
- childState.headsUpIsVisible, row.showingPulsing(),
- ambientState.isOnKeyguard(), row.getEntry().isStickyAndNotDemoted())) {
- // Ensure that the heads up is always visible even when scrolled off.
- // NSSL y starts at top of screen in non-split-shade, but below the qs offset
- // in split shade, so we only need to inset by the scrim padding in split shade.
- // TODO(b/332574413) get the clamp inset from HeadsUpNotificationPlaceholder
- final float clampInset = ambientState.getUseSplitShade()
- ? mNotificationScrimPadding : mQuickQsOffsetHeight;
- clampHunToTop(clampInset, ambientState.getStackTranslation(),
- row.getCollapsedHeight(), childState);
- if (isTopEntry && row.isAboveShelf()) {
- // the first hun can't get off screen.
- clampHunToMaxTranslation(ambientState, row, childState);
- childState.hidden = false;
+ if (SceneContainerFlag.isEnabled()) {
+ if (shouldHunBeVisibleWhenScrolled(row.mustStayOnScreen(),
+ childState.headsUpIsVisible, row.showingPulsing(),
+ ambientState.isOnKeyguard(), row.getEntry().isStickyAndNotDemoted())) {
+ clampHunToTop(
+ /* headsUpTop = */ headsUpTranslation,
+ /* collapsedHeight = */ row.getCollapsedHeight(),
+ /* viewState = */ childState
+ );
+ if (isTopEntry && row.isAboveShelf()) {
+ clampHunToMaxTranslation(
+ /* headsUpTop = */ headsUpTranslation,
+ /* headsUpBottom = */ ambientState.getHeadsUpBottom(),
+ /* viewState = */ childState
+ );
+ updateCornerRoundnessForPinnedHun(row, ambientState.getStackTop());
+ childState.hidden = false;
+ }
+ }
+ } else {
+ if (shouldHunBeVisibleWhenScrolled(row.mustStayOnScreen(),
+ childState.headsUpIsVisible, row.showingPulsing(),
+ ambientState.isOnKeyguard(), row.getEntry().isStickyAndNotDemoted())) {
+ // Ensure that the heads up is always visible even when scrolled off.
+ // NSSL y starts at top of screen in non-split-shade, but below the qs
+ // offset
+ // in split shade, so we only need to inset by the scrim padding in split
+ // shade.
+ final float clampInset = ambientState.getUseSplitShade()
+ ? mNotificationScrimPadding : mQuickQsOffsetHeight;
+ clampHunToTop(clampInset, ambientState.getStackTranslation(),
+ row.getCollapsedHeight(), childState);
+ if (isTopEntry && row.isAboveShelf()) {
+ // the first hun can't get off screen.
+ clampHunToMaxTranslation(ambientState, row, childState);
+ updateCornerRoundnessForPinnedHun(row, ambientState.getStackY());
+ childState.hidden = false;
+ }
}
}
}
@@ -1005,9 +1029,13 @@
@VisibleForTesting
void clampHunToTop(float clampInset, float stackTranslation, float collapsedHeight,
ExpandableViewState viewState) {
+ SceneContainerFlag.assertInLegacyMode();
+ clampHunToTop(clampInset + stackTranslation, collapsedHeight, viewState);
+ }
- final float newTranslation = Math.max(clampInset + stackTranslation,
- viewState.getYTranslation());
+ @VisibleForTesting
+ void clampHunToTop(float headsUpTop, float collapsedHeight, ExpandableViewState viewState) {
+ final float newTranslation = Math.max(headsUpTop, viewState.getYTranslation());
// Transition from collapsed pinned state to fully expanded state
// when the pinned HUN approaches its actual location (when scrolling back to top).
@@ -1020,9 +1048,12 @@
// while the rest of notifications are scrolled offscreen.
private void clampHunToMaxTranslation(AmbientState ambientState, ExpandableNotificationRow row,
ExpandableViewState childState) {
+ SceneContainerFlag.assertInLegacyMode();
float maxHeadsUpTranslation = ambientState.getMaxHeadsUpTranslation();
- final float maxShelfPosition = ambientState.getInnerHeight() + ambientState.getTopPadding()
- + ambientState.getStackTranslation();
+ final float maxShelfPosition =
+ ambientState.getInnerHeight()
+ + ambientState.getTopPadding()
+ + ambientState.getStackTranslation();
maxHeadsUpTranslation = Math.min(maxHeadsUpTranslation, maxShelfPosition);
final float bottomPosition = maxHeadsUpTranslation - row.getCollapsedHeight();
@@ -1030,13 +1061,20 @@
childState.height = (int) Math.min(childState.height, maxHeadsUpTranslation
- newTranslation);
childState.setYTranslation(newTranslation);
+ }
+ private void clampHunToMaxTranslation(float headsUpTop, float headsUpBottom,
+ ExpandableViewState viewState) {
+ if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
+ final float maxHeight = headsUpTop - headsUpBottom;
+ viewState.setYTranslation(Math.min(headsUpTop, viewState.getYTranslation()));
+ viewState.height = (int) Math.min(maxHeight, viewState.height);
+ }
+
+ private void updateCornerRoundnessForPinnedHun(ExpandableNotificationRow row, float stackTop) {
// Animate pinned HUN bottom corners to and from original roundness.
final float originalCornerRadius =
row.isLastInSection() ? 1f : (mSmallCornerRadius / mLargeCornerRadius);
- final float stackTop = SceneContainerFlag.isEnabled()
- ? ambientState.getStackTop()
- : ambientState.getStackY();
final float bottomValue = computeCornerRoundnessForPinnedHun(mHostView.getHeight(),
stackTop, getMaxAllowedChildHeight(row), originalCornerRadius);
row.requestBottomRoundness(bottomValue, STACK_SCROLL_ALGO);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt
index 762c507..6226fe7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt
@@ -59,6 +59,9 @@
/** set the y position in px of the top of the HUN in this view's coordinates */
fun setHeadsUpTop(headsUpTop: Float)
+ /** set the bottom-most y position in px, where we can draw HUNs in this view's coordinates */
+ fun setHeadsUpBottom(headsUpBottom: Float)
+
/** set whether the view has been scrolled all the way to the top */
fun setScrolledToTop(scrolledToTop: Boolean)
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 97266c5..86c7c6b 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.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 @@
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 ae98e1d..107bf1e 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 @@
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 @@
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 @@
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 @@
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 @@
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 a2e44df..8577d48 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.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 @@
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 @@
});
javaAdapter.alwaysCollectFlow(shadeInteractor.isAnyExpanded(),
this::onShadeOrQsExpanded);
+ mVisualStabilityProvider.addPersistentReorderingBannedListener(mOnReorderingBannedListener);
+ mVisualStabilityProvider.addPersistentReorderingAllowedListener(
+ mOnReorderingAllowedListener);
}
public void setAnimationStateHandler(AnimationStateHandler handler) {
@@ -379,6 +383,7 @@
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 @@
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 @@
}
@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 @@
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/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index 96127b6..8f2ad40 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -55,6 +55,7 @@
import com.android.keyguard.KeyguardViewController;
import com.android.keyguard.TrustGrantFlags;
import com.android.keyguard.ViewMediatorCallback;
+import com.android.systemui.Flags;
import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor;
import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor;
import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerCallbackInteractor;
@@ -708,15 +709,30 @@
* {@link #needsFullscreenBouncer()}.
*/
protected void showBouncerOrKeyguard(boolean hideBouncerWhenShowing) {
- if (needsFullscreenBouncer() && !mDozing) {
+ boolean isDozing = mDozing;
+ if (Flags.simPinRaceConditionOnRestart()) {
+ KeyguardState toState = mKeyguardTransitionInteractor.getTransitionState().getValue()
+ .getTo();
+ isDozing = mDozing || toState == KeyguardState.DOZING || toState == KeyguardState.AOD;
+ }
+ if (needsFullscreenBouncer() && !isDozing) {
// The keyguard might be showing (already). So we need to hide it.
if (!primaryBouncerIsShowing()) {
- mCentralSurfaces.hideKeyguard();
if (SceneContainerFlag.isEnabled()) {
+ mCentralSurfaces.hideKeyguard();
mSceneInteractorLazy.get().changeScene(
Scenes.Bouncer, "StatusBarKeyguardViewManager.showBouncerOrKeyguard");
} else {
- mPrimaryBouncerInteractor.show(/* isScrimmed= */ true);
+ if (Flags.simPinRaceConditionOnRestart()) {
+ if (mPrimaryBouncerInteractor.show(/* isScrimmed= */ true)) {
+ mCentralSurfaces.hideKeyguard();
+ } else {
+ mCentralSurfaces.showKeyguard();
+ }
+ } else {
+ mCentralSurfaces.hideKeyguard();
+ mPrimaryBouncerInteractor.show(/* isScrimmed= */ true);
+ }
}
} else {
Log.e(TAG, "Attempted to show the sim bouncer when it is already showing.");
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 aced0be..0320a7a 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 @@
}
@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 ae1898b..16bd7f8 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 @@
chipView.setOnClickListener(chipModel.onClickListener)
// Accessibility
- setChipAccessibility(chipModel, chipView)
+ setChipAccessibility(chipModel, chipView, chipBackgroundView)
// Colors
val textColor = chipModel.colors.text(chipContext)
@@ -122,7 +122,8 @@
// Notify listeners
listener.onOngoingActivityStatusChanged(
- hasOngoingActivity = true
+ hasOngoingActivity = true,
+ shouldAnimate = true,
)
}
is OngoingActivityChipModel.Hidden -> {
@@ -130,7 +131,8 @@
// b/192243808 and [Chronometer.start].
chipTimeView.stop()
listener.onOngoingActivityStatusChanged(
- hasOngoingActivity = false
+ hasOngoingActivity = false,
+ shouldAnimate = chipModel.shouldAnimate,
)
}
}
@@ -211,7 +213,11 @@
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 @@
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 @@
/** 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 40799583..a88c6d7 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 @@
@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 @@
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 @@
log { "Runnable is NULL, stop delete." }
return
}
- if (!NotificationThrottleHun.isEnabled) {
+ if (!isEnabled()) {
runnable.run()
return
}
@@ -185,7 +203,8 @@
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 @@
* 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 @@
/** 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 @@
/** 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 @@
}
fun getWaitingEntry(key: String): HeadsUpEntry? {
- if (!NotificationThrottleHun.isEnabled) {
+ if (!isEnabled()) {
return null
}
for (headsUpEntry in nextMap.keys) {
@@ -282,7 +301,7 @@
}
fun getWaitingEntryList(): List<HeadsUpEntry> {
- if (!NotificationThrottleHun.isEnabled) {
+ if (!isEnabled()) {
return mutableListOf()
}
return nextMap.keys.toList()
@@ -340,13 +359,15 @@
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 @@
"\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 220e729..a0eb989 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 @@
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 4838554..07bbca7 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 @@
* @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 @@
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 66ed092..ace4ce0 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 @@
class DeviceProvisioningInteractor
@Inject
constructor(
- repository: DeviceProvisioningRepository,
+ private val repository: DeviceProvisioningRepository,
) {
/**
* Whether this device has been provisioned.
@@ -34,4 +34,8 @@
* @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 e4d0668..7a521a6 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 @@
* 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 @@
}
}
.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 0000000..2b094d6
--- /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 0000000..91bfdff
--- /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 0000000..73d361f6
--- /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 0000000..5bd26cc
--- /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 0000000..e84c8b6
--- /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 c45f98e..066bfc5 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 @@
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.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.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 @@
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 @@
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 @@
KeyguardManager keyguardManager,
ActivityManager activityManager,
UserTracker userTracker,
- DumpManager dumpManager
+ DumpManager dumpManager,
+ AudioSharingInteractor audioSharingInteractor,
+ JavaAdapter javaAdapter
) {
mContext = context.getApplicationContext();
mPackageManager = packageManager;
@@ -200,6 +216,8 @@
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 @@
} 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 @@
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 @@
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 @@
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 @@
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 6b02e1a..0770d89 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 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 @@
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 @@
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/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
index 79933ee..a94ef36 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 @@
}
@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 1d78168..892375d 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 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 @@
@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 @@
@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 @@
@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 @@
@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 @@
@Test
public void testSmartspaceEnabledRemovesKeyguardStatusArea() {
- mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
-
when(mSmartspaceController.isEnabled()).thenReturn(true);
mController.init();
@@ -126,8 +116,6 @@
@Test
public void onLocaleListChangedRebuildsSmartspaceView() {
- mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
-
when(mSmartspaceController.isEnabled()).thenReturn(true);
mController.init();
@@ -138,8 +126,6 @@
@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 @@
@Test
public void testSmartspaceDisabledShowsKeyguardStatusArea() {
- mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
-
when(mSmartspaceController.isEnabled()).thenReturn(false);
mController.init();
@@ -163,8 +147,6 @@
@Test
public void testRefresh() {
- mSetFlagsRule.disableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT);
-
mController.refresh();
verify(mSmartspaceController).requestSmartspaceUpdate();
@@ -172,8 +154,6 @@
@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 @@
@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 @@
@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 @@
@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 @@
@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 @@
@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 @@
@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 @@
@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 83443be..0bf9d12 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.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 @@
// 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 @@
@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 b09357f..c51aa04 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 @@
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 639b53b..5600b87 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 @@
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 d940fc9..7a4bbfe 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 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 @@
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 @@
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 2bd0976..2aa33a17 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 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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
// 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 @@
@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 @@
@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 @@
@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 @@
@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 @@
}
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 6dc4b10..bbff539 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 @@
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.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.MockitoAnnotations
import org.mockito.MockitoSession
import org.mockito.quality.Strictness
-import javax.inject.Provider
-
@ExperimentalCoroutinesApi
@SmallTest
@@ -79,35 +82,28 @@
@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 @@
`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 @@
`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 @@
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 @@
captor.value.onBiometricAuthenticated(
0 /* userId */,
BiometricSourceType.FINGERPRINT /* type */,
- false /* isStrongBiometric */)
+ false /* isStrongBiometric */
+ )
// THEN no ripple
verify(rippleView, never()).startUnlockedRipple(any())
@@ -194,14 +201,19 @@
`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 @@
captor.value.onBiometricAuthenticated(
0 /* userId */,
BiometricSourceType.FACE /* type */,
- false /* isStrongBiometric */)
+ false /* isStrongBiometric */
+ )
verify(rippleView, never()).startUnlockedRipple(any())
}
@@ -233,18 +246,17 @@
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 @@
`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 @@
`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 @@
}
@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/controls/ui/DetailDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/DetailDialogTest.kt
index 10b3ce3..0489d81 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 @@
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/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
index fc3b35d..a3959d2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
@@ -1574,30 +1574,6 @@
}
@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/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 693a877..7cc9185 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 @@
}
@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 201ee88..1c99eff 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 @@
@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 @@
@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 4f4aac4..3b96be4 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 @@
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 9fb1aa7..e89abf6 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 @@
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/tiles/ModesTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ModesTileTest.kt
index 4c77fb8..27b6ea6 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 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.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 @@
@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 @@
}
)
- 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 3b0e194..bab9bbb 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 @@
@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 @@
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("The topComponent of the screenshot")
+ .that(result.topComponent)
+ .isEqualTo(ComponentName.unflattenFromString(FILES))
+
+ assertWithMessage("Task ID").that(result.taskId).isEqualTo(TASK_ID)
+ }
+
+ 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 169511f..86c9ab7 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 @@
}
@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 @@
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 b80d1a4..a5c4bcd 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 @@
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 @@
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 @@
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 @@
.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 90e8ea5f..905cc4c 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 @@
* 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 @@
* 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 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void onNotificationHeightChangeWhileOnKeyguardWillComputeMaxKeyguardNotifications() {
mStatusBarStateController.setState(KEYGUARD);
ArgumentCaptor<OnHeightChangedListener> captor =
@@ -124,6 +127,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void onNotificationHeightChangeWhileInShadeWillNotComputeMaxKeyguardNotifications() {
mStatusBarStateController.setState(SHADE);
ArgumentCaptor<OnHeightChangedListener> captor =
@@ -140,6 +144,7 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void testSetPanelScrimMinFractionWhenHeadsUpIsDragged() {
mNotificationPanelViewController.setHeadsUpDraggingStartingHeight(
mNotificationPanelViewController.getMaxPanelHeight() / 2);
@@ -288,6 +300,7 @@
}
@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 @@
}
@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 @@
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void testA11y_scrollForward() {
mAccessibilityDelegate.performAccessibilityAction(
mView,
@@ -483,6 +503,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void testA11y_scrollUp() {
mAccessibilityDelegate.performAccessibilityAction(
mView,
@@ -493,6 +514,7 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void testCanCollapsePanelOnTouch_trueForKeyGuard() {
mStatusBarStateController.setState(KEYGUARD);
@@ -653,6 +686,7 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void testCancelSwipeWhileLocked_notifiesKeyguardState() {
mStatusBarStateController.setState(KEYGUARD);
@@ -707,6 +744,7 @@
}
@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 @@
}
@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 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void testUnlockedSplitShadeTransitioningToKeyguard_closesQS() {
enableSplitShade(true);
mStatusBarStateController.setState(SHADE);
@@ -741,6 +781,7 @@
}
@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 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void testSwitchesToCorrectClockInSinglePaneShade() {
mStatusBarStateController.setState(KEYGUARD);
@@ -765,6 +807,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void testSwitchesToCorrectClockInSplitShade() {
mStatusBarStateController.setState(KEYGUARD);
enableSplitShade(/* enabled= */ true);
@@ -785,6 +828,7 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void onQsSetExpansionHeightCalled_qsFullyExpandedOnKeyguard_showNSSL() {
// GIVEN
mStatusBarStateController.setState(KEYGUARD);
@@ -883,6 +933,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void onQsSetExpansionHeightCalled_qsFullyExpandedOnKeyguard_hideNSSL() {
// GIVEN
mStatusBarStateController.setState(KEYGUARD);
@@ -904,6 +955,7 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void testDisplaysSmallClockOnLockscreenInSplitShadeWhenMediaIsPlaying() {
mStatusBarStateController.setState(KEYGUARD);
enableSplitShade(/* enabled= */ true);
@@ -978,6 +1033,7 @@
}
@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 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void testExpandWithQsMethodIsUsingLockscreenTransitionController() {
enableSplitShade(/* enabled= */ true);
mStatusBarStateController.setState(KEYGUARD);
@@ -1008,6 +1065,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void setKeyguardStatusBarAlpha_setsAlphaOnKeyguardStatusBarController() {
float statusBarAlpha = 0.5f;
@@ -1017,6 +1075,7 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void testQsExpansionChangedToDefaultWhenRotatingFromOrToSplitShade() {
when(mCommandQueue.panelsEnabled()).thenReturn(true);
@@ -1065,6 +1127,7 @@
}
@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 @@
}
@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 @@
}
@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 @@
@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 @@
}
@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 @@
}
@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 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void onLayoutChange_fullWidth_updatesQSWithFullWithTrue() {
setIsFullWidth(true);
@@ -1139,6 +1208,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void onLayoutChange_notFullWidth_updatesQSWithFullWithFalse() {
setIsFullWidth(false);
@@ -1146,6 +1216,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void onLayoutChange_qsNotSet_doesNotCrash() {
mQuickSettingsController.setQs(null);
@@ -1153,6 +1224,7 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void onSplitShadeChanged_alwaysResetsOverScrollState() {
enableSplitShade(true);
enableSplitShade(false);
@@ -1217,6 +1291,7 @@
* 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 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void onShadeFlingEnd_mExpandImmediateShouldBeReset() {
mNotificationPanelViewController.onFlingEnd(false);
@@ -1244,6 +1320,7 @@
}
@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 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void shadeFullyExpanded_inShadeState() {
mStatusBarStateController.setState(SHADE);
@@ -1265,6 +1343,7 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void shadeFullyExpanded_onKeyguard() {
mStatusBarStateController.setState(KEYGUARD);
@@ -1274,12 +1353,14 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@Test
+ @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
public void shadeNotExpanded_whenInputFocusTransferStartedButPanelsDisabled() {
when(mCommandQueue.panelsEnabled()).thenReturn(false);
@@ -1324,6 +1409,7 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 e1d92e7..64eadb7 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.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_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 @@
@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 74a2999..6f2302a 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.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 @@
@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 @@
}
@Test
+ @EnableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
fun handleDispatchTouchEvent_nsslMigrationOn_userActivity() {
- enableMigrateClocksFlag()
underTest.setStatusBarViewController(phoneStatusBarViewController)
interactionEventHandler.handleDispatchTouchEvent(DOWN_EVENT)
@@ -440,6 +443,7 @@
}
@Test
+ @EnableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
fun shouldInterceptTouchEvent_dozing_touchInLockIconArea_touchNotIntercepted() {
// GIVEN dozing
whenever(sysuiStatusBarStateController.isDozing).thenReturn(true)
@@ -452,13 +456,12 @@
// 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 @@
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 @@
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 @@
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 @@
}
@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 fec7424..ca29dd9 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 @@
}
@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 c9a7c82..02764f8 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.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 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 @@
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 @@
}
@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 @@
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 @@
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 @@
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 4728c64..b4a37ee 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.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 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 @@
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 @@
}
@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 @@
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 @@
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 @@
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 f87b17d..2658679 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.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 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 @@
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 @@
}
@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 @@
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 @@
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 0000000..b9049e8
--- /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 ca043f1..6e4d886 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 @@
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 b1a8d0b..ee249f0 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.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 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 @@
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 @@
val latest by collectLastValue(underTest.chip)
- assertThat(latest).isEqualTo(OngoingActivityChipModel.Hidden)
+ assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
}
@Test
@@ -230,7 +258,81 @@
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 acb005f..0407fc1 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 @@
@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 d87b3e2..4218be2 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 @@
}
@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 a925ccf..c971037 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 @@
@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/notification/stack/StackScrollAlgorithmTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
index c2a7b52..b7ebebe 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
@@ -126,6 +126,19 @@
}
@Test
+ @EnableSceneContainer
+ fun resetViewStates_defaultHun_yTranslationIsHeadsUpTop() {
+ val headsUpTop = 200f
+ ambientState.headsUpTop = headsUpTop
+
+ whenever(notificationRow.isPinned).thenReturn(true)
+ whenever(notificationRow.isHeadsUp).thenReturn(true)
+
+ resetViewStates_hunYTranslationIs(headsUpTop)
+ }
+
+ @Test
+ @DisableSceneContainer
fun resetViewStates_defaultHun_yTranslationIsInset() {
whenever(notificationRow.isPinned).thenReturn(true)
whenever(notificationRow.isHeadsUp).thenReturn(true)
@@ -133,6 +146,7 @@
}
@Test
+ @DisableSceneContainer
fun resetViewStates_defaultHunWithStackMargin_changesHunYTranslation() {
whenever(notificationRow.isPinned).thenReturn(true)
whenever(notificationRow.isHeadsUp).thenReturn(true)
@@ -140,7 +154,7 @@
}
@Test
- @DisableSceneContainer // TODO(b/332574413) cover hun bounds integration with tests
+ @DisableSceneContainer
fun resetViewStates_defaultHunWhenShadeIsOpening_yTranslationIsInset() {
whenever(notificationRow.isPinned).thenReturn(true)
whenever(notificationRow.isHeadsUp).thenReturn(true)
@@ -153,6 +167,7 @@
}
@Test
+ @DisableSceneContainer
@DisableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
fun resetViewStates_hunAnimatingAway_yTranslationIsInset() {
whenever(notificationRow.isHeadsUpAnimatingAway).thenReturn(true)
@@ -160,6 +175,7 @@
}
@Test
+ @DisableSceneContainer
@DisableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
fun resetViewStates_hunAnimatingAway_StackMarginChangesHunYTranslation() {
whenever(notificationRow.isHeadsUpAnimatingAway).thenReturn(true)
@@ -167,6 +183,7 @@
}
@Test
+ @DisableSceneContainer
@EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
fun resetViewStates_defaultHun_newHeadsUpAnim_yTranslationIsInset() {
whenever(notificationRow.isPinned).thenReturn(true)
@@ -175,6 +192,7 @@
}
@Test
+ @DisableSceneContainer
@EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
fun resetViewStates_defaultHunWithStackMargin_newHeadsUpAnim_changesHunYTranslation() {
whenever(notificationRow.isPinned).thenReturn(true)
@@ -183,7 +201,98 @@
}
@Test
- @DisableSceneContainer // TODO(b/332574413) cover hun bounds integration with tests
+ @EnableSceneContainer
+ fun resetViewStates_defaultHunInShade_stackTopEqualsHunTop_hunHasFullHeight() {
+ // Given: headsUpTop == stackTop -> haven't scrolled the stack yet
+ val headsUpTop = 150f
+ val collapsedHeight = 100
+ val intrinsicHeight = 300
+ fakeHunInShade(
+ headsUpTop = headsUpTop,
+ stackTop = headsUpTop,
+ collapsedHeight = collapsedHeight,
+ intrinsicHeight = intrinsicHeight,
+ )
+
+ // When
+ stackScrollAlgorithm.resetViewStates(ambientState, 0)
+
+ // Then: HUN is at the headsUpTop
+ assertThat(notificationRow.viewState.yTranslation).isEqualTo(headsUpTop)
+ // And: HUN has its full height
+ assertThat(notificationRow.viewState.height).isEqualTo(intrinsicHeight)
+ }
+
+ @Test
+ @EnableSceneContainer
+ fun resetViewStates_defaultHunInShade_stackTopGreaterThanHeadsUpTop_hunClampedToHeadsUpTop() {
+ // Given: headsUpTop < stackTop -> scrolled the stack a little bit
+ val stackTop = -25f
+ val headsUpTop = 150f
+ val collapsedHeight = 100
+ val intrinsicHeight = 300
+ fakeHunInShade(
+ headsUpTop = headsUpTop,
+ stackTop = stackTop,
+ collapsedHeight = collapsedHeight,
+ intrinsicHeight = intrinsicHeight
+ )
+
+ // When
+ stackScrollAlgorithm.resetViewStates(ambientState, 0)
+
+ // Then: HUN is translated to the headsUpTop
+ assertThat(notificationRow.viewState.yTranslation).isEqualTo(headsUpTop)
+ // And: HUN is clipped to the available space
+ // newTranslation = max(150, -25)
+ // distToReal = 150 - (-25)
+ // height = max(300 - 175, 100)
+ assertThat(notificationRow.viewState.height).isEqualTo(125)
+ }
+
+ @Test
+ @EnableSceneContainer
+ fun resetViewStates_defaultHunInShade_stackOverscrolledHun_hunClampedToHeadsUpTop() {
+ // Given: headsUpTop << stackTop -> stack has fully overscrolled the HUN
+ val stackTop = -500f
+ val headsUpTop = 150f
+ val collapsedHeight = 100
+ val intrinsicHeight = 300
+ fakeHunInShade(
+ headsUpTop = headsUpTop,
+ stackTop = stackTop,
+ collapsedHeight = collapsedHeight,
+ intrinsicHeight = intrinsicHeight
+ )
+
+ // When
+ stackScrollAlgorithm.resetViewStates(ambientState, 0)
+
+ // Then: HUN is translated to the headsUpTop
+ assertThat(notificationRow.viewState.yTranslation).isEqualTo(headsUpTop)
+ // And: HUN is clipped to its collapsed height
+ assertThat(notificationRow.viewState.height).isEqualTo(collapsedHeight)
+ }
+
+ @Test
+ @EnableSceneContainer
+ fun resetViewStates_defaultHun_showingQS_hunTranslatedToHeadsUpTop() {
+ // Given: the shade is open and scrolled to the bottom to show the QuickSettings
+ val headsUpTop = 2000f
+ fakeHunInShade(
+ headsUpTop = headsUpTop,
+ stackTop = 2600f, // stack scrolled below the screen
+ stackCutoff = 4000f,
+ collapsedHeight = 100,
+ intrinsicHeight = 300
+ )
+ whenever(notificationRow.isAboveShelf).thenReturn(true)
+
+ resetViewStates_hunYTranslationIs(headsUpTop)
+ }
+
+ @Test
+ @DisableSceneContainer
@EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
fun resetViewStates_defaultHun_showingQS_newHeadsUpAnim_hunTranslatedToMax() {
// Given: the shade is open and scrolled to the bottom to show the QuickSettings
@@ -200,7 +309,7 @@
}
@Test
- @DisableSceneContainer // TODO(b/332574413) cover hun bounds integration with tests
+ @DisableSceneContainer
@EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
fun resetViewStates_hunAnimatingAway_showingQS_newHeadsUpAnim_hunTranslatedToBottomOfScreen() {
// Given: the shade is open and scrolled to the bottom to show the QuickSettings
@@ -245,6 +354,7 @@
}
@Test
+ @DisableSceneContainer
@EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
fun resetViewStates_hunAnimatingAwayWhileDozing_yTranslationIsInset() {
whenever(notificationRow.isHeadsUpAnimatingAway).thenReturn(true)
@@ -255,6 +365,7 @@
}
@Test
+ @DisableSceneContainer
@EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
fun resetViewStates_hunAnimatingAwayWhileDozing_hasStackMargin_changesHunYTranslation() {
whenever(notificationRow.isHeadsUpAnimatingAway).thenReturn(true)
@@ -744,8 +855,7 @@
expandableViewState.yTranslation = 50f
stackScrollAlgorithm.clampHunToTop(
- /* quickQsOffsetHeight= */ 10f,
- /* stackTranslation= */ 0f,
+ /* headsUpTop= */ 10f,
/* collapsedHeight= */ 1f,
expandableViewState
)
@@ -760,8 +870,7 @@
expandableViewState.yTranslation = -10f
stackScrollAlgorithm.clampHunToTop(
- /* quickQsOffsetHeight= */ 10f,
- /* stackTranslation= */ 0f,
+ /* headsUpTop= */ 10f,
/* collapsedHeight= */ 1f,
expandableViewState
)
@@ -777,8 +886,7 @@
expandableViewState.yTranslation = -100f
stackScrollAlgorithm.clampHunToTop(
- /* quickQsOffsetHeight= */ 10f,
- /* stackTranslation= */ 0f,
+ /* headsUpTop= */ 10f,
/* collapsedHeight= */ 10f,
expandableViewState
)
@@ -796,8 +904,7 @@
expandableViewState.yTranslation = 5f
stackScrollAlgorithm.clampHunToTop(
- /* quickQsOffsetHeight= */ 10f,
- /* stackTranslation= */ 0f,
+ /* headsUpTop= */ 10f,
/* collapsedHeight= */ 10f,
expandableViewState
)
@@ -1309,6 +1416,33 @@
expect.that(notificationRow.viewState.alpha).isEqualTo(expectedAlpha)
}
+
+ /** fakes the notification row under test, to be a HUN in a fully opened shade */
+ private fun fakeHunInShade(
+ headsUpTop: Float,
+ collapsedHeight: Int,
+ intrinsicHeight: Int,
+ stackTop: Float,
+ stackCutoff: Float = 2000f,
+ fullStackHeight: Float = 3000f
+ ) {
+ ambientState.headsUpTop = headsUpTop
+ ambientState.stackTop = stackTop
+ ambientState.stackCutoff = stackCutoff
+
+ // shade is fully open
+ ambientState.expansionFraction = 1.0f
+ with(fullStackHeight) {
+ ambientState.stackHeight = this
+ ambientState.stackEndHeight = this
+ }
+ stackScrollAlgorithm.setIsExpanded(true)
+
+ whenever(notificationRow.mustStayOnScreen()).thenReturn(true)
+ whenever(notificationRow.isHeadsUp).thenReturn(true)
+ whenever(notificationRow.collapsedHeight).thenReturn(collapsedHeight)
+ whenever(notificationRow.intrinsicHeight).thenReturn(intrinsicHeight)
+ }
}
private fun mockExpandableNotificationRow(): ExpandableNotificationRow {
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 fea0e72..8dfbb37 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 @@
}
@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 0e4d892..31f93b4 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
@@ -116,6 +116,7 @@
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.Answers;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
@@ -176,6 +177,8 @@
private ViewRootImpl mViewRootImpl;
@Mock
private WindowOnBackInvokedDispatcher mOnBackInvokedDispatcher;
+ @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+ private KeyguardTransitionInteractor mKeyguardTransitionInteractor;
@Captor
private ArgumentCaptor<OnBackInvokedCallback> mBackCallbackCaptor;
@Captor
@@ -223,7 +226,7 @@
mAlternateBouncerInteractor,
mUdfpsOverlayInteractor,
mActivityStarter,
- mock(KeyguardTransitionInteractor.class),
+ mKeyguardTransitionInteractor,
StandardTestDispatcher(null, null),
() -> mock(WindowManagerLockscreenVisibilityInteractor.class),
() -> mock(KeyguardDismissActionInteractor.class),
@@ -1056,6 +1059,7 @@
@Test
@DisableSceneContainer
+ @DisableFlags(Flags.FLAG_SIM_PIN_RACE_CONDITION_ON_RESTART)
public void testShowBouncerOrKeyguard_needsFullScreen() {
when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(
KeyguardSecurityModel.SecurityMode.SimPin);
@@ -1066,6 +1070,22 @@
@Test
@DisableSceneContainer
+ @EnableFlags(Flags.FLAG_SIM_PIN_RACE_CONDITION_ON_RESTART)
+ public void testShowBouncerOrKeyguard_showsKeyguardIfShowBouncerReturnsFalse() {
+ when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(
+ KeyguardSecurityModel.SecurityMode.SimPin);
+ when(mPrimaryBouncerInteractor.show(true)).thenReturn(false);
+ when(mKeyguardTransitionInteractor.getTransitionState().getValue().getTo())
+ .thenReturn(KeyguardState.LOCKSCREEN);
+
+ reset(mCentralSurfaces);
+ mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false);
+ verify(mPrimaryBouncerInteractor).show(true);
+ verify(mCentralSurfaces).showKeyguard();
+ }
+
+ @Test
+ @DisableSceneContainer
public void testShowBouncerOrKeyguard_needsFullScreen_bouncerAlreadyShowing() {
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 01540e7..58ad835 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 @@
// 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 @@
// 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 @@
// 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 @@
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 @@
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 @@
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 @@
// 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 @@
// 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 @@
// 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 @@
// 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 94159bc..60750cf 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 @@
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 d3f1125..cefdf7e 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 @@
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 cc2ef53..12cfdcf 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.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 f40282f..a8271fe 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.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 @@
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 f737148..3f5dc82 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 @@
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.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.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.Mock;
import org.mockito.MockitoAnnotations;
+import java.util.Objects;
import java.util.concurrent.Executor;
@RunWith(AndroidJUnit4.class)
@@ -104,6 +112,10 @@
private UserTracker mUserTracker;
@Mock
private DumpManager mDumpManager;
+ @Mock
+ private AudioSharingInteractor mAudioSharingInteractor;
+ @Mock
+ private JavaAdapter mJavaAdapter;
@Before
@@ -124,11 +136,26 @@
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 @@
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 @@
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 cdfcca6..b5cbf59 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 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 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 @@
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 ac42319..60b5b5d 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.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.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 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 @@
@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 @@
@Mock
private KeyguardBypassController mKeyguardBypassController;
@Mock
- private FloatingContentCoordinator mFloatingContentCoordinator;
- @Mock
private BubbleDataRepository mDataRepository;
@Mock
private NotificationShadeWindowView mNotificationShadeWindowView;
@@ -372,6 +374,7 @@
@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 @@
mShellCommandHandler,
mShellController,
mBubbleData,
- mFloatingContentCoordinator,
+ new FloatingContentCoordinator(),
mDataRepository,
mStatusBarService,
mWindowManager,
@@ -571,12 +574,32 @@
}
@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 @@
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/FakeCommunalPrefsRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalPrefsRepository.kt
index 1da1fb2..5e870b1 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 @@
/** 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/domain/interactor/CommunalInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt
index eb92785..4ad046c 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.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 @@
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/deviceentry/data/repository/FakeDeviceEntryRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FakeDeviceEntryRepository.kt
index 045bd5d..2dcd275 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 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 f73f43d..edf4bcc 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 0000000..5b2dc2b
--- /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 0000000..8f84e04
--- /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 126d858..4634a7f 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 @@
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 @@
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 3d85a4a..c7dfd5c 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 @@
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 1ed7a47..651a0f7 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 @@
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 e4bb166..c2a6f7d 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 @@
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 8ed7f96..0770009 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 @@
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 da956ec..8b4de2b 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 @@
* 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 0000000..77d97bb
--- /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 0000000..77aeb44
--- /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 492e87b..7e8f1a9 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.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 @@
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/notification/stack/data/repository/HeadsUpNotificationsRepositoryExt.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationsRepositoryExt.kt
deleted file mode 100644
index 9be7dfe..0000000
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationsRepositoryExt.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.notification.stack.data.repository
-
-import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository
-
-fun FakeHeadsUpNotificationRepository.setNotifications(notifications: List<HeadsUpRowRepository>) {
- setNotifications(*notifications.toTypedArray())
-}
-
-fun FakeHeadsUpNotificationRepository.setNotifications(vararg notifications: HeadsUpRowRepository) {
- this.activeHeadsUpRows.value = notifications.toSet()
-}
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 9247e88..e3176f1 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 @@
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 0000000..db95fad
--- /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 0000000..ddc66c7
--- /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 0000000..ec8e907
--- /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 0000000..d952d07
--- /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 49e793f..4357f2b 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.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 @@
}
}
+ /**
+ * @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 @@
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 @@
}
}
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 b4efae3..8e2e0ad 100644
--- a/services/accessibility/accessibility.aconfig
+++ b/services/accessibility/accessibility.aconfig
@@ -114,6 +114,13 @@
}
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 d7da2f0..a5ec2ba 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 @@
+ 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 @@
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 @@
// 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 7d9d660..ee7d0ae 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_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 @@
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/android/app/usage/UsageStatsManagerInternal.java b/services/core/java/android/app/usage/UsageStatsManagerInternal.java
index 23c008e..fc00987 100644
--- a/services/core/java/android/app/usage/UsageStatsManagerInternal.java
+++ b/services/core/java/android/app/usage/UsageStatsManagerInternal.java
@@ -74,6 +74,18 @@
public abstract void reportEvent(String packageName, @UserIdInt int userId, int eventType);
/**
+ * Reports an event to the UsageStatsManager for all users. <br/>
+ * <em>Note: Starting from {@link android.os.Build.VERSION_CODES#R Android R}, if the user's
+ * device is not in an unlocked state (as defined by {@link UserManager#isUserUnlocked()}),
+ * then this event will be added to a queue and processed once the device is unlocked.</em>
+ *
+ * @param packageName The package for which this event occurred.
+ * @param eventType The event that occurred. Valid values can be found at
+ * {@link UsageEvents}
+ */
+ public abstract void reportEventForAllUsers(String packageName, int eventType);
+
+ /**
* Reports a configuration change to the UsageStatsManager. <br/>
* <em>Note: Starting from {@link android.os.Build.VERSION_CODES#R Android R}, if the user's
* device is not in an unlocked state (as defined by {@link UserManager#isUserUnlocked()}),
diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java
index 2d91331..458749d 100644
--- a/services/core/java/com/android/server/accounts/AccountManagerService.java
+++ b/services/core/java/com/android/server/accounts/AccountManagerService.java
@@ -185,12 +185,6 @@
final Context mContext;
- private static final int[] INTERESTING_APP_OPS = new int[] {
- AppOpsManager.OP_GET_ACCOUNTS,
- AppOpsManager.OP_READ_CONTACTS,
- AppOpsManager.OP_WRITE_CONTACTS,
- };
-
private final PackageManager mPackageManager;
private final AppOpsManager mAppOpsManager;
private UserManager mUserManager;
@@ -394,47 +388,74 @@
}.register(mContext, mHandler.getLooper(), UserHandle.ALL, true);
// Cancel account request notification if an app op was preventing the account access
- for (int i = 0; i < INTERESTING_APP_OPS.length; ++i) {
- mAppOpsManager.startWatchingMode(INTERESTING_APP_OPS[i], null,
- new OnInterestingAppOpChangedListener());
- }
+ mAppOpsManager.startWatchingMode(AppOpsManager.OP_GET_ACCOUNTS, null,
+ new AppOpsManager.OnOpChangedInternalListener() {
+ @Override
+ public void onOpChanged(int op, String packageName) {
+ try {
+ final int userId = ActivityManager.getCurrentUser();
+ final int uid = mPackageManager.getPackageUidAsUser(packageName, userId);
+ final int mode = mAppOpsManager.checkOpNoThrow(
+ AppOpsManager.OP_GET_ACCOUNTS, uid, packageName);
+ if (mode == AppOpsManager.MODE_ALLOWED) {
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ UserAccounts accounts = getUserAccounts(userId);
+ cancelAccountAccessRequestNotificationIfNeeded(
+ packageName, uid, true, accounts);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+ } catch (NameNotFoundException e) {
+ /* ignore */
+ } catch (SQLiteCantOpenDatabaseException e) {
+ Log.w(TAG, "Can't read accounts database", e);
+ return;
+ }
+ }
+ });
- // Clear the accounts cache on permission changes.
- // The specific permissions we care about are backed by AppOps, so just
- // let the change events on those handle clearing any notifications.
- mPackageManager.addOnPermissionsChangeListener((int uid) -> {
+ // Cancel account request notification if a permission was preventing the account access
+ mPackageManager.addOnPermissionsChangeListener(
+ (int uid) -> {
+ // Permission changes cause requires updating accounts cache.
AccountManager.invalidateLocalAccountsDataCaches();
+
+ Account[] accounts = null;
+ String[] packageNames = mPackageManager.getPackagesForUid(uid);
+ if (packageNames != null) {
+ final int userId = UserHandle.getUserId(uid);
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ for (String packageName : packageNames) {
+ // if app asked for permission we need to cancel notification even
+ // for O+ applications.
+ if (mPackageManager.checkPermission(
+ Manifest.permission.GET_ACCOUNTS,
+ packageName) != PackageManager.PERMISSION_GRANTED) {
+ continue;
+ }
+
+ if (accounts == null) {
+ accounts = getAccountsOrEmptyArray(null, userId, "android");
+ if (ArrayUtils.isEmpty(accounts)) {
+ return;
+ }
+ }
+ UserAccounts userAccounts = getUserAccounts(UserHandle.getUserId(uid));
+ for (Account account : accounts) {
+ cancelAccountAccessRequestNotificationIfNeeded(
+ account, uid, packageName, true, userAccounts);
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
});
}
- private class OnInterestingAppOpChangedListener implements AppOpsManager.OnOpChangedListener {
- @Override
- public void onOpChanged(String op, String packageName) {
- final int userId = ActivityManager.getCurrentUser();
- final int packageUid;
- try {
- packageUid = mPackageManager.getPackageUidAsUser(packageName, userId);
- } catch (NameNotFoundException e) {
- /* ignore */
- return;
- }
-
- final int mode = mAppOpsManager.checkOpNoThrow(op, packageUid, packageName);
- if (mode != AppOpsManager.MODE_ALLOWED) {
- return;
- }
-
- final long identity = Binder.clearCallingIdentity();
- try {
- cancelAccountAccessRequestNotificationIfNeeded(
- packageName, packageUid, true, getUserAccounts(userId));
- } catch (SQLiteCantOpenDatabaseException e) {
- Log.w(TAG, "Can't read accounts database", e);
- } finally {
- Binder.restoreCallingIdentity(identity);
- }
- }
- }
boolean getBindInstantServiceAllowed(int userId) {
return mAuthenticatorCache.getBindInstantServiceAllowed(userId);
diff --git a/services/core/java/com/android/server/am/ApplicationThreadDeferred.java b/services/core/java/com/android/server/am/ApplicationThreadDeferred.java
index b0f9b53..193edd2 100644
--- a/services/core/java/com/android/server/am/ApplicationThreadDeferred.java
+++ b/services/core/java/com/android/server/am/ApplicationThreadDeferred.java
@@ -38,7 +38,7 @@
*
* {@hide}
*/
-class ApplicationThreadDeferred extends ApplicationThreadFilter {
+final class ApplicationThreadDeferred extends IApplicationThread.Delegator {
static final String TAG = TAG_WITH_CLASS_NAME ? "ApplicationThreadDeferred" : TAG_AM;
@@ -87,11 +87,15 @@
// When true, binder calls to paused processes will be deferred until the process is unpaused.
private final boolean mDefer;
+ // The base thread, because Delegator does not expose it.
+ private final IApplicationThread mBase;
+
/** Create an instance with a base thread and a deferral enable flag. */
@VisibleForTesting
public ApplicationThreadDeferred(IApplicationThread thread, boolean defer) {
super(thread);
+ mBase = thread;
mDefer = defer;
mOperations[CLEAR_DNS_CACHE] = () -> { super.clearDnsCache(); };
@@ -105,6 +109,15 @@
this(thread, deferBindersWhenPaused());
}
+ /**
+ * Return the implementation's value of asBinder(). super.asBinder() is not a real Binder
+ * object.
+ */
+ @Override
+ public android.os.IBinder asBinder() {
+ return mBase.asBinder();
+ }
+
/** The process is being paused. Start deferring calls. */
void onProcessPaused() {
synchronized (mLock) {
diff --git a/services/core/java/com/android/server/am/ApplicationThreadFilter.java b/services/core/java/com/android/server/am/ApplicationThreadFilter.java
deleted file mode 100644
index d049305..0000000
--- a/services/core/java/com/android/server/am/ApplicationThreadFilter.java
+++ /dev/null
@@ -1,603 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.am;
-
-
-class ApplicationThreadFilter implements android.app.IApplicationThread {
- private final android.app.IApplicationThread mBase;
- public ApplicationThreadFilter(android.app.IApplicationThread base) { mBase = base; }
- android.app.IApplicationThread getBase() { return mBase; }
- public android.os.IBinder asBinder() {
- return mBase.asBinder();
- }
-
- @Override
- public void scheduleReceiver(android.content.Intent intent,
- android.content.pm.ActivityInfo info,
- android.content.res.CompatibilityInfo compatInfo,
- int resultCode,
- String data,
- android.os.Bundle extras,
- boolean ordered,
- boolean assumeDelivered,
- int sendingUser,
- int processState,
- int sentFromUid,
- String sentFromPackage)
- throws android.os.RemoteException {
- mBase.scheduleReceiver(intent,
- info,
- compatInfo,
- resultCode,
- data,
- extras,
- ordered,
- assumeDelivered,
- sendingUser,
- processState,
- sentFromUid,
- sentFromPackage);
- }
- @Override
- public void scheduleReceiverList(java.util.List<android.app.ReceiverInfo> info)
- throws android.os.RemoteException {
- mBase.scheduleReceiverList(info);
- }
- @Override
- public void scheduleCreateService(android.os.IBinder token,
- android.content.pm.ServiceInfo info,
- android.content.res.CompatibilityInfo compatInfo,
- int processState)
- throws android.os.RemoteException {
- mBase.scheduleCreateService(token,
- info,
- compatInfo,
- processState);
- }
- @Override
- public void scheduleStopService(android.os.IBinder token)
- throws android.os.RemoteException {
- mBase.scheduleStopService(token);
- }
- @Override
- public void bindApplication(String packageName,
- android.content.pm.ApplicationInfo info,
- String sdkSandboxClientAppVolumeUuid,
- String sdkSandboxClientAppPackage,
- boolean isSdkInSandbox,
- android.content.pm.ProviderInfoList providerList,
- android.content.ComponentName testName,
- android.app.ProfilerInfo profilerInfo,
- android.os.Bundle testArguments,
- android.app.IInstrumentationWatcher testWatcher,
- android.app.IUiAutomationConnection uiAutomationConnection,
- int debugMode,
- boolean enableBinderTracking,
- boolean trackAllocation,
- boolean restrictedBackupMode,
- boolean persistent,
- android.content.res.Configuration config,
- android.content.res.CompatibilityInfo compatInfo,
- java.util.Map services,
- android.os.Bundle coreSettings,
- String buildSerial,
- android.content.AutofillOptions autofillOptions,
- android.content.ContentCaptureOptions contentCaptureOptions,
- long[] disabledCompatChanges,
- long[] loggableCompatChanges,
- android.os.SharedMemory serializedSystemFontMap,
- long startRequestedElapsedTime,
- long startRequestedUptime)
- throws android.os.RemoteException {
- mBase.bindApplication(packageName,
- info,
- sdkSandboxClientAppVolumeUuid,
- sdkSandboxClientAppPackage,
- isSdkInSandbox,
- providerList,
- testName,
- profilerInfo,
- testArguments,
- testWatcher,
- uiAutomationConnection,
- debugMode,
- enableBinderTracking,
- trackAllocation,
- restrictedBackupMode,
- persistent,
- config,
- compatInfo,
- services,
- coreSettings,
- buildSerial,
- autofillOptions,
- contentCaptureOptions,
- disabledCompatChanges,
- loggableCompatChanges,
- serializedSystemFontMap,
- startRequestedElapsedTime,
- startRequestedUptime);
- }
- @Override
- public void runIsolatedEntryPoint(String entryPoint,
- String[] entryPointArgs)
- throws android.os.RemoteException {
- mBase.runIsolatedEntryPoint(entryPoint,
- entryPointArgs);
- }
- @Override
- public void scheduleExit()
- throws android.os.RemoteException {
- mBase.scheduleExit();
- }
- @Override
- public void scheduleServiceArgs(android.os.IBinder token,
- android.content.pm.ParceledListSlice args)
- throws android.os.RemoteException {
- mBase.scheduleServiceArgs(token,
- args);
- }
- @Override
- public void updateTimeZone()
- throws android.os.RemoteException {
- mBase.updateTimeZone();
- }
- @Override
- public void processInBackground()
- throws android.os.RemoteException {
- mBase.processInBackground();
- }
- @Override
- public void scheduleBindService(android.os.IBinder token,
- android.content.Intent intent,
- boolean rebind,
- int processState,
- long bindSeq)
- throws android.os.RemoteException {
- mBase.scheduleBindService(token,
- intent,
- rebind,
- processState,
- bindSeq);
- }
- @Override
- public void scheduleUnbindService(android.os.IBinder token,
- android.content.Intent intent)
- throws android.os.RemoteException {
- mBase.scheduleUnbindService(token,
- intent);
- }
- @Override
- public void dumpService(android.os.ParcelFileDescriptor fd,
- android.os.IBinder servicetoken,
- String[] args)
- throws android.os.RemoteException {
- mBase.dumpService(fd,
- servicetoken,
- args);
- }
- @Override
- public void scheduleRegisteredReceiver(android.content.IIntentReceiver receiver,
- android.content.Intent intent,
- int resultCode,
- String data,
- android.os.Bundle extras,
- boolean ordered,
- boolean sticky,
- boolean assumeDelivered,
- int sendingUser,
- int processState,
- int sentFromUid,
- String sentFromPackage)
- throws android.os.RemoteException {
- mBase.scheduleRegisteredReceiver(receiver,
- intent,
- resultCode,
- data,
- extras,
- ordered,
- sticky,
- assumeDelivered,
- sendingUser,
- processState,
- sentFromUid,
- sentFromPackage);
- }
- @Override
- public void scheduleLowMemory()
- throws android.os.RemoteException {
- mBase.scheduleLowMemory();
- }
- @Override
- public void profilerControl(boolean start,
- android.app.ProfilerInfo profilerInfo,
- int profileType)
- throws android.os.RemoteException {
- mBase.profilerControl(start,
- profilerInfo,
- profileType);
- }
- @Override
- public void setSchedulingGroup(int group)
- throws android.os.RemoteException {
- mBase.setSchedulingGroup(group);
- }
- @Override
- public void scheduleCreateBackupAgent(android.content.pm.ApplicationInfo app,
- int backupMode,
- int userId,
- int operationType)
- throws android.os.RemoteException {
- mBase.scheduleCreateBackupAgent(app,
- backupMode,
- userId,
- operationType);
- }
- @Override
- public void scheduleDestroyBackupAgent(android.content.pm.ApplicationInfo app,
- int userId)
- throws android.os.RemoteException {
- mBase.scheduleDestroyBackupAgent(app,
- userId);
- }
- @Override
- public void scheduleOnNewSceneTransitionInfo(android.os.IBinder token,
- android.app.ActivityOptions.SceneTransitionInfo info)
- throws android.os.RemoteException {
- mBase.scheduleOnNewSceneTransitionInfo(token,
- info);
- }
- @Override
- public void scheduleSuicide()
- throws android.os.RemoteException {
- mBase.scheduleSuicide();
- }
- @Override
- public void dispatchPackageBroadcast(int cmd,
- String[] packages)
- throws android.os.RemoteException {
- mBase.dispatchPackageBroadcast(cmd,
- packages);
- }
- @Override
- public void scheduleCrash(String msg,
- int typeId,
- android.os.Bundle extras)
- throws android.os.RemoteException {
- mBase.scheduleCrash(msg,
- typeId,
- extras);
- }
- @Override
- public void dumpHeap(boolean managed,
- boolean mallocInfo,
- boolean runGc,
- String dumpBitmaps,
- String path,
- android.os.ParcelFileDescriptor fd,
- android.os.RemoteCallback finishCallback)
- throws android.os.RemoteException {
- mBase.dumpHeap(managed,
- mallocInfo,
- runGc,
- dumpBitmaps,
- path,
- fd,
- finishCallback);
- }
- @Override
- public void dumpActivity(android.os.ParcelFileDescriptor fd,
- android.os.IBinder servicetoken,
- String prefix,
- String[] args)
- throws android.os.RemoteException {
- mBase.dumpActivity(fd,
- servicetoken,
- prefix,
- args);
- }
- @Override
- public void dumpResources(android.os.ParcelFileDescriptor fd,
- android.os.RemoteCallback finishCallback)
- throws android.os.RemoteException {
- mBase.dumpResources(fd,
- finishCallback);
- }
- @Override
- public void clearDnsCache()
- throws android.os.RemoteException {
- mBase.clearDnsCache();
- }
- @Override
- public void updateHttpProxy()
- throws android.os.RemoteException {
- mBase.updateHttpProxy();
- }
- @Override
- public void setCoreSettings(android.os.Bundle coreSettings)
- throws android.os.RemoteException {
- mBase.setCoreSettings(coreSettings);
- }
- @Override
- public void updatePackageCompatibilityInfo(String pkg,
- android.content.res.CompatibilityInfo info)
- throws android.os.RemoteException {
- mBase.updatePackageCompatibilityInfo(pkg,
- info);
- }
- @Override
- public void scheduleTrimMemory(int level)
- throws android.os.RemoteException {
- mBase.scheduleTrimMemory(level);
- }
- @Override
- public void dumpMemInfo(android.os.ParcelFileDescriptor fd,
- android.os.Debug.MemoryInfo mem,
- boolean checkin,
- boolean dumpInfo,
- boolean dumpDalvik,
- boolean dumpSummaryOnly,
- boolean dumpUnreachable,
- boolean dumpAllocatorLogs,
- String[] args)
- throws android.os.RemoteException {
- mBase.dumpMemInfo(fd,
- mem,
- checkin,
- dumpInfo,
- dumpDalvik,
- dumpSummaryOnly,
- dumpUnreachable,
- dumpAllocatorLogs,
- args);
- }
- @Override
- public void dumpMemInfoProto(android.os.ParcelFileDescriptor fd,
- android.os.Debug.MemoryInfo mem,
- boolean dumpInfo,
- boolean dumpDalvik,
- boolean dumpSummaryOnly,
- boolean dumpUnreachable,
- String[] args)
- throws android.os.RemoteException {
- mBase.dumpMemInfoProto(fd,
- mem,
- dumpInfo,
- dumpDalvik,
- dumpSummaryOnly,
- dumpUnreachable,
- args);
- }
- @Override
- public void dumpGfxInfo(android.os.ParcelFileDescriptor fd,
- String[] args)
- throws android.os.RemoteException {
- mBase.dumpGfxInfo(fd,
- args);
- }
- @Override
- public void dumpCacheInfo(android.os.ParcelFileDescriptor fd,
- String[] args)
- throws android.os.RemoteException {
- mBase.dumpCacheInfo(fd,
- args);
- }
- @Override
- public void dumpProvider(android.os.ParcelFileDescriptor fd,
- android.os.IBinder servicetoken,
- String[] args)
- throws android.os.RemoteException {
- mBase.dumpProvider(fd,
- servicetoken,
- args);
- }
- @Override
- public void dumpDbInfo(android.os.ParcelFileDescriptor fd,
- String[] args)
- throws android.os.RemoteException {
- mBase.dumpDbInfo(fd,
- args);
- }
- @Override
- public void unstableProviderDied(android.os.IBinder provider)
- throws android.os.RemoteException {
- mBase.unstableProviderDied(provider);
- }
- @Override
- public void requestAssistContextExtras(android.os.IBinder activityToken,
- android.os.IBinder requestToken,
- int requestType,
- int sessionId,
- int flags)
- throws android.os.RemoteException {
- mBase.requestAssistContextExtras(activityToken,
- requestToken,
- requestType,
- sessionId,
- flags);
- }
- @Override
- public void scheduleTranslucentConversionComplete(android.os.IBinder token,
- boolean timeout)
- throws android.os.RemoteException {
- mBase.scheduleTranslucentConversionComplete(token,
- timeout);
- }
- @Override
- public void setProcessState(int state)
- throws android.os.RemoteException {
- mBase.setProcessState(state);
- }
- @Override
- public void scheduleInstallProvider(android.content.pm.ProviderInfo provider)
- throws android.os.RemoteException {
- mBase.scheduleInstallProvider(provider);
- }
- @Override
- public void updateTimePrefs(int timeFormatPreference)
- throws android.os.RemoteException {
- mBase.updateTimePrefs(timeFormatPreference);
- }
- @Override
- public void scheduleEnterAnimationComplete(android.os.IBinder token)
- throws android.os.RemoteException {
- mBase.scheduleEnterAnimationComplete(token);
- }
- @Override
- public void notifyCleartextNetwork(byte[] firstPacket)
- throws android.os.RemoteException {
- mBase.notifyCleartextNetwork(firstPacket);
- }
- @Override
- public void startBinderTracking()
- throws android.os.RemoteException {
- mBase.startBinderTracking();
- }
- @Override
- public void stopBinderTrackingAndDump(android.os.ParcelFileDescriptor fd)
- throws android.os.RemoteException {
- mBase.stopBinderTrackingAndDump(fd);
- }
- @Override
- public void scheduleLocalVoiceInteractionStarted(android.os.IBinder token,
- com.android.internal.app.IVoiceInteractor voiceInteractor)
- throws android.os.RemoteException {
- mBase.scheduleLocalVoiceInteractionStarted(token,
- voiceInteractor);
- }
- @Override
- public void handleTrustStorageUpdate()
- throws android.os.RemoteException {
- mBase.handleTrustStorageUpdate();
- }
- @Override
- public void attachAgent(String path)
- throws android.os.RemoteException {
- mBase.attachAgent(path);
- }
- @Override
- public void attachStartupAgents(String dataDir)
- throws android.os.RemoteException {
- mBase.attachStartupAgents(dataDir);
- }
- @Override
- public void scheduleApplicationInfoChanged(android.content.pm.ApplicationInfo ai)
- throws android.os.RemoteException {
- mBase.scheduleApplicationInfoChanged(ai);
- }
- @Override
- public void setNetworkBlockSeq(long procStateSeq)
- throws android.os.RemoteException {
- mBase.setNetworkBlockSeq(procStateSeq);
- }
- @Override
- public void scheduleTransaction(android.app.servertransaction.ClientTransaction transaction)
- throws android.os.RemoteException {
- mBase.scheduleTransaction(transaction);
- }
- @Override
- public void scheduleTaskFragmentTransaction(android.window.ITaskFragmentOrganizer organizer,
- android.window.TaskFragmentTransaction transaction)
- throws android.os.RemoteException {
- mBase.scheduleTaskFragmentTransaction(organizer,
- transaction);
- }
- @Override
- public void requestDirectActions(android.os.IBinder activityToken,
- com.android.internal.app.IVoiceInteractor intractor,
- android.os.RemoteCallback cancellationCallback,
- android.os.RemoteCallback callback)
- throws android.os.RemoteException {
- mBase.requestDirectActions(activityToken,
- intractor,
- cancellationCallback,
- callback);
- }
- @Override
- public void performDirectAction(android.os.IBinder activityToken,
- String actionId,
- android.os.Bundle arguments,
- android.os.RemoteCallback cancellationCallback,
- android.os.RemoteCallback resultCallback)
- throws android.os.RemoteException {
- mBase.performDirectAction(activityToken,
- actionId,
- arguments,
- cancellationCallback,
- resultCallback);
- }
- @Override
- public void notifyContentProviderPublishStatus(android.app.ContentProviderHolder holder,
- String authorities,
- int userId,
- boolean published)
- throws android.os.RemoteException {
- mBase.notifyContentProviderPublishStatus(holder,
- authorities,
- userId,
- published);
- }
- @Override
- public void instrumentWithoutRestart(android.content.ComponentName instrumentationName,
- android.os.Bundle instrumentationArgs,
- android.app.IInstrumentationWatcher instrumentationWatcher,
- android.app.IUiAutomationConnection instrumentationUiConnection,
- android.content.pm.ApplicationInfo targetInfo)
- throws android.os.RemoteException {
- mBase.instrumentWithoutRestart(instrumentationName,
- instrumentationArgs,
- instrumentationWatcher,
- instrumentationUiConnection,
- targetInfo);
- }
- @Override
- public void updateUiTranslationState(android.os.IBinder activityToken,
- int state,
- android.view.translation.TranslationSpec sourceSpec,
- android.view.translation.TranslationSpec targetSpec,
- java.util.List<android.view.autofill.AutofillId> viewIds,
- android.view.translation.UiTranslationSpec uiTranslationSpec)
- throws android.os.RemoteException {
- mBase.updateUiTranslationState(activityToken,
- state,
- sourceSpec,
- targetSpec,
- viewIds,
- uiTranslationSpec);
- }
- @Override
- public void scheduleTimeoutService(android.os.IBinder token,
- int startId)
- throws android.os.RemoteException {
- mBase.scheduleTimeoutService(token,
- startId);
- }
- @Override
- public void scheduleTimeoutServiceForType(android.os.IBinder token,
- int startId,
- int fgsType)
- throws android.os.RemoteException {
- mBase.scheduleTimeoutServiceForType(token,
- startId,
- fgsType);
- }
- @Override
- public void schedulePing(android.os.RemoteCallback pong)
- throws android.os.RemoteException {
- mBase.schedulePing(pong);
- }
-}
diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
index aeebae4..a7b2eb1 100644
--- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
+++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
@@ -2135,8 +2135,13 @@
final boolean targetedBroadcast = r.intent.getComponent() != null;
final boolean targetedSelf = Objects.equals(r.callerPackage, receiverPackageName);
if (targetedBroadcast && !targetedSelf) {
- mService.mUsageStatsService.reportEvent(receiverPackageName,
- r.userId, Event.APP_COMPONENT_USED);
+ if (r.userId == UserHandle.USER_ALL) {
+ mService.mUsageStatsService.reportEventForAllUsers(
+ receiverPackageName, Event.APP_COMPONENT_USED);
+ } else {
+ mService.mUsageStatsService.reportEvent(receiverPackageName,
+ r.userId, Event.APP_COMPONENT_USED);
+ }
}
mService.notifyPackageUse(receiverPackageName,
diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java
index b058bd8..504c54a 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 @@
// 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/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index 8de1552..25b9228 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -834,6 +834,14 @@
return isDeviceActiveForCommunication(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
}
+ /*package*/ boolean isBluetoothBleHeadsetActive() {
+ return isDeviceActiveForCommunication(AudioDeviceInfo.TYPE_BLE_HEADSET);
+ }
+
+ /*package*/ boolean isBluetoothBleSpeakerActive() {
+ return isDeviceActiveForCommunication(AudioDeviceInfo.TYPE_BLE_SPEAKER);
+ }
+
/*package*/ boolean isDeviceConnected(@NonNull AudioDeviceAttributes device) {
synchronized (mDeviceStateLock) {
return mDeviceInventory.isDeviceConnected(device);
@@ -1863,7 +1871,10 @@
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 02aa6f5..ca69f31 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 @@
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 0d309eb..ed22b4c 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 @@
// 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 @@
audioserverPermissions() ?
initializeAudioServerPermissionProvider(
context, audioPolicyFacade, audioserverLifecycleExecutor) :
- null
+ null,
+ audioserverLifecycleExecutor
);
}
@@ -1145,13 +1148,16 @@
* {@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 @@
mAppOps = appOps;
mPermissionProvider = permissionProvider;
+ mAudioServerLifecycleExecutor = audioserverLifecycleExecutor;
mAudioSystem = audioSystem;
mSystemServer = systemServer;
@@ -1170,6 +1177,34 @@
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);
@@ -5719,16 +5754,25 @@
|| ringerMode == AudioManager.RINGER_MODE_SILENT;
final boolean shouldRingSco = ringerMode == AudioManager.RINGER_MODE_VIBRATE
&& mDeviceBroker.isBluetoothScoActive();
- // Ask audio policy engine to force use Bluetooth SCO channel if needed
+ final boolean shouldRingBle = ringerMode == AudioManager.RINGER_MODE_VIBRATE
+ && (mDeviceBroker.isBluetoothBleHeadsetActive()
+ || mDeviceBroker.isBluetoothBleSpeakerActive());
+ // Ask audio policy engine to force use Bluetooth SCO/BLE channel if needed
final String eventSource = "muteRingerModeStreams() from u/pid:" + Binder.getCallingUid()
+ "/" + Binder.getCallingPid();
+ int forceUse = AudioSystem.FORCE_NONE;
+ if (shouldRingSco) {
+ forceUse = AudioSystem.FORCE_BT_SCO;
+ } else if (shouldRingBle) {
+ forceUse = AudioSystem.FORCE_BT_BLE;
+ }
sendMsg(mAudioHandler, MSG_SET_FORCE_USE, SENDMSG_QUEUE, AudioSystem.FOR_VIBRATE_RINGING,
- shouldRingSco ? AudioSystem.FORCE_BT_SCO : AudioSystem.FORCE_NONE, eventSource, 0);
+ forceUse, eventSource, 0);
for (int streamType = numStreamTypes - 1; streamType >= 0; streamType--) {
final boolean isMuted = isStreamMutedByRingerOrZenMode(streamType);
final boolean muteAllowedBySco =
- !(shouldRingSco && streamType == AudioSystem.STREAM_RING);
+ !((shouldRingSco || shouldRingBle) && streamType == AudioSystem.STREAM_RING);
final boolean shouldZenMute = isStreamAffectedByCurrentZen(streamType);
final boolean shouldMute = shouldZenMute || (ringerModeMute
&& isStreamAffectedByRingerMode(streamType) && muteAllowedBySco);
@@ -11965,29 +12009,6 @@
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 7f4bc74..d083c68 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 @@
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 57bffa7..ce92dfb 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 @@
}
// 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 @@
//----------------------------------------------------------------------
// 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 @@
}
}
- /*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 @@
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 @@
}
/**
- * 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 @@
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 @@
*
* @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 @@
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 @@
}
}
- /*package*/ synchronized void onBroadcastScoConnectionState(int state) {
+ @GuardedBy("mDeviceBroker.mDeviceStateLock")
+ /*package*/ void onBroadcastScoConnectionState(int state) {
if (state == mScoConnectionState) {
return;
}
@@ -563,8 +578,8 @@
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 @@
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 @@
}
}
+ @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 @@
}
}
- // 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 @@
mDeviceBroker.postBroadcastScoConnectionState(state);
}
+ @GuardedBy("mDeviceBroker.mDeviceStateLock")
@Nullable AudioDeviceAttributes getHeadsetAudioDevice() {
if (mBluetoothHeadsetDevice == null) {
return null;
@@ -837,6 +854,7 @@
return getHeadsetAudioDevice(mBluetoothHeadsetDevice);
}
+ @GuardedBy("mDeviceBroker.mDeviceStateLock")
private @NonNull AudioDeviceAttributes getHeadsetAudioDevice(BluetoothDevice btDevice) {
AudioDeviceAttributes deviceAttr = mResolvedScoAudioDevices.get(btDevice);
if (deviceAttr != null) {
@@ -876,30 +894,44 @@
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 @@
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 @@
//----------------------------------------------------------------------
- // @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 @@
}
}
- 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 @@
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 @@
* @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 7a055d1..76b4263 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 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/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 5c1e783..1177be2 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 @@
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 @@
// 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 @@
// 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 @@
// 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 @@
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 @@
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 @@
mBrightnessTracker = brightnessTracker;
mOnBrightnessChangeRunnable = onBrightnessChangeRunnable;
- PowerManager pm = context.getSystemService(PowerManager.class);
-
final Resources resources = context.getResources();
// DOZE AND DIM SETTINGS
@@ -573,8 +570,7 @@
mBrightnessRangeController = mInjector.getBrightnessRangeController(hbmController,
modeChangeCallback, mDisplayDeviceConfig, mHandler, flags,
- mDisplayDevice.getDisplayTokenLocked(),
- mDisplayDevice.getDisplayDeviceInfoLocked());
+ displayToken, displayDeviceInfo);
mDisplayBrightnessController =
new DisplayBrightnessController(context, null,
@@ -588,8 +584,8 @@
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 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 @@
changed = true;
mDisplayDevice = device;
mUniqueDisplayId = uniqueId;
+ mPhysicalDisplayName = displayName;
mDisplayStatsId = mUniqueDisplayId.hashCode();
mDisplayDeviceConfig = config;
mThermalBrightnessThrottlingDataId = thermalBrightnessThrottlingDataId;
@@ -895,7 +893,7 @@
mBrightnessClamperController.onDisplayChanged(
new BrightnessClamperController.DisplayDeviceData(uniqueId,
thermalBrightnessThrottlingDataId, powerThrottlingDataId,
- config, mDisplayId));
+ config, info.width, info.height, token, mDisplayId));
if (changed) {
updatePowerState();
@@ -1552,10 +1550,9 @@
// 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 @@
// 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 @@
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 82b401a..5cc603c 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.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 @@
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 @@
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 @@
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 @@
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 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 @@
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 @@
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 88d2c00..afab743 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.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 @@
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 @@
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 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 @@
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 @@
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 @@
}
/**
- * 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 @@
@NonNull
private final String mPowerThrottlingDataId;
@NonNull
- private final DisplayDeviceConfig mDisplayDeviceConfig;
+ final DisplayDeviceConfig mDisplayDeviceConfig;
- private final int mDisplayId;
+ final int mWidth;
+
+ 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 @@
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 a829866..2ee70fd 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 @@
// noop
}
+ // Called in DisplayControllerHandler
@Override
public void stop() {
- // noop
+ unregisterHdrListener();
}
+
@Override
public boolean shouldListenToLightSensor() {
return false;
@@ -48,4 +118,117 @@
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 1f46af8..bb2efa1 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 @@
// 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 @@
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 8ca0458..99f4747 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.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 @@
* 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 @@
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);
}
- final AdditionalSubtypeMap newMap = AdditionalSubtypeUtils.load(userId);
- sPerUserMap.put(userId, newMap);
- return newMap;
+ if (map == null) {
+ Slog.e(TAG, "get(userId=" + userId + ") is called before loadInitialDataAndGet()."
+ + " Returning an empty map");
+ return AdditionalSubtypeMap.EMPTY_MAP;
+ }
+ return map;
}
- @GuardedBy("ImfLock.class")
+ /**
+ * 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);
+ }
+ return map;
+ }
+
+ /**
+ * 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 @@
}
@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 f61ca61..c82e5be 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 @@
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 @@
* 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 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 @@
}
@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 @@
}
@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 8fd2033..9921927 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_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 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.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 @@
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 @@
private final UserManagerInternal mUserManagerInternal;
@MultiUserUnawareField
private final InputMethodMenuController mMenuController;
+ private final InputMethodMenuControllerNew mMenuControllerNew;
@GuardedBy("ImfLock.class")
@MultiUserUnawareField
@@ -566,7 +574,9 @@
}
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 @@
}
break;
}
- case STYLUS_HANDWRITING_ENABLED: {
+ case Settings.Secure.STYLUS_HANDWRITING_ENABLED: {
InputMethodManager.invalidateLocalStylusHandwritingAvailabilityCaches();
InputMethodManager
.invalidateLocalConnectionlessStylusHandwritingAvailabilityCaches();
@@ -631,7 +641,15 @@
}
}
}
- 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 @@
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();
@@ -1015,7 +1018,7 @@
// Called directly from UserManagerService. Do not block the calling thread.
final int userId = user.id;
AdditionalSubtypeMapRepository.onUserCreated(userId);
- initializeUsersAsync(new int[userId]);
+ initializeUsersAsync(new int[]{ userId });
}
@Override
@@ -1023,7 +1026,7 @@
// 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 @@
@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 @@
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 @@
: bindingControllerFactory);
mMenuController = new InputMethodMenuController(this);
+ mMenuControllerNew = Flags.imeSwitcherRevamp()
+ ? new InputMethodMenuControllerNew() : null;
mVisibilityStateComputer = new ImeVisibilityStateComputer(this);
mVisibilityApplier = new DefaultImeVisibilityApplier(this);
@@ -1331,10 +1324,42 @@
}
}
+ 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");
@@ -1410,9 +1435,9 @@
(windowToken, imeVisible) -> {
if (Flags.refactorInsetsController()) {
if (imeVisible) {
- showSoftInputInternal(windowToken);
+ showCurrentInputInternal(windowToken);
} else {
- hideSoftInputInternal(windowToken);
+ hideCurrentInputInternal(windowToken);
}
}
});
@@ -1586,8 +1611,8 @@
// 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 +1807,11 @@
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 +1913,7 @@
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 +2628,12 @@
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 +2648,7 @@
|| (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 +2659,27 @@
}
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);
+ }
+
+ /**
+ * Checks whether there at least two subtypes that should be shown for the IME switcher menu,
+ * across all enabled IMEs for the given user.
+ *
+ * @param nonAuxOnly whether to check only for non auxiliary subtypes.
+ * @param settings the input method settings under the given user ID.
+ */
+ private static boolean hasMultipleSubtypesForSwitcher(boolean nonAuxOnly,
+ @NonNull InputMethodSettings settings) {
List<InputMethodInfo> imes = settings.getEnabledInputMethodListWithFilter(
InputMethodInfo::shouldShowInInputMethodPicker);
final int numImes = imes.size();
@@ -2654,7 +2709,9 @@
}
}
}
- if (nonAuxCount > 1 || auxCount > 1) {
+ if (Flags.imeSwitcherRevamp() && nonAuxOnly) {
+ return nonAuxCount > 1;
+ } else if (nonAuxCount > 1 || auxCount > 1) {
return true;
} else if (nonAuxCount == 1 && auxCount == 1) {
if (nonAuxSubtype != null && auxSubtype != null
@@ -2780,7 +2837,10 @@
}
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).
@@ -2799,7 +2859,9 @@
@GuardedBy("ImfLock.class")
void updateFromSettingsLocked(boolean enabledMayChange, @UserIdInt int userId) {
updateInputMethodsFromSettingsLocked(enabledMayChange, userId);
- mMenuController.updateKeyboardFromSettingsLocked();
+ if (!Flags.imeSwitcherRevamp()) {
+ mMenuController.updateKeyboardFromSettingsLocked();
+ }
}
/**
@@ -3083,8 +3145,8 @@
}
}
- 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) {
@@ -3103,8 +3165,8 @@
}
}
- 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) {
@@ -3965,10 +4027,70 @@
@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) {
@@ -3976,16 +4098,45 @@
if (!calledWithValidTokenLocked(token, userData)) {
return;
}
- showInputMethodPickerFromSystem(
- InputMethodManager.SHOW_IM_PICKER_MODE_INCLUDE_AUXILIARY_SUBTYPES, displayId);
+ onImeSwitchButtonClickLocked(token, displayId, userData);
}
}
@IInputMethodManagerImpl.PermissionVerified(Manifest.permission.WRITE_SECURE_SETTINGS)
@Override
public void onImeSwitchButtonClickFromSystem(int displayId) {
- showInputMethodPickerFromSystem(
- InputMethodManager.SHOW_IM_PICKER_MODE_INCLUDE_AUXILIARY_SUBTYPES, displayId);
+ synchronized (ImfLock.class) {
+ final int userId = mCurrentUserId;
+ final var userData = getUserData(userId);
+ final var bindingController = userData.mBindingController;
+ final var curToken = bindingController.getCurToken();
+ if (curToken == null) {
+ return;
+ }
+
+ onImeSwitchButtonClickLocked(curToken, displayId, userData);
+ }
+ }
+
+ /**
+ * Handles a click on the IME switch button. Depending on the number of enabled IME subtypes,
+ * this will either switch to the next IME/subtype, or show the input method picker dialog.
+ *
+ * @param token The token identifying the input method that triggered this.
+ * @param displayId The ID of the display where the input method picker dialog should be shown.
+ * @param userData The data of the user for which to switch IMEs or show the picker dialog.
+ */
+ @GuardedBy("ImfLock.class")
+ private void onImeSwitchButtonClickLocked(@NonNull IBinder token, int displayId,
+ @NonNull UserData userData) {
+ final int userId = userData.mUserId;
+ final var settings = InputMethodSettingsRepository.get(userId);
+ if (hasMultipleSubtypesForSwitcher(true /* nonAuxOnly */, settings)) {
+ switchToNextInputMethodLocked(token, false /* onlyCurrentIme */, userData);
+ } else {
+ showInputMethodPickerFromSystem(
+ InputMethodManager.SHOW_IM_PICKER_MODE_INCLUDE_AUXILIARY_SUBTYPES, displayId);
+ }
}
@NonNull
@@ -4582,7 +4733,10 @@
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);
}
@@ -4627,8 +4781,7 @@
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 {
@@ -4888,8 +5041,9 @@
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);
@@ -4902,12 +5056,35 @@
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;
@@ -4978,7 +5155,9 @@
// --------------------------------------------------------------
case MSG_HARD_KEYBOARD_SWITCH_CHANGED:
- mMenuController.handleHardKeyboardStatusChange(msg.arg1 == 1);
+ if (!Flags.imeSwitcherRevamp()) {
+ mMenuController.handleHardKeyboardStatusChange(msg.arg1 == 1);
+ }
synchronized (ImfLock.class) {
sendOnNavButtonFlagsChangedToAllImesLocked();
}
@@ -5548,7 +5727,8 @@
}
@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)
@@ -5556,7 +5736,7 @@
.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)
@@ -5565,6 +5745,7 @@
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;
}
@@ -5710,9 +5891,10 @@
}
@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);
}
}
@@ -5809,7 +5991,12 @@
// 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();
+ }
}
}
}
@@ -5828,6 +6015,15 @@
}
@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) {
@@ -6071,7 +6267,6 @@
boolean isCritical) {
IInputMethodInvoker method;
ClientState client;
- ClientState focusedWindowClient;
final Printer p = new PrintWriterPrinter(pw);
@@ -6149,6 +6344,10 @@
};
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());
@@ -6595,7 +6794,7 @@
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 0000000..045414b
--- /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 50ba364..1b84036 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 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 @@
}
@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 ec5c9e6..be57321 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.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 @@
@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/locksettings/LockSettingsShellCommand.java b/services/core/java/com/android/server/locksettings/LockSettingsShellCommand.java
index 17f2fcc..bb35b37 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 @@
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 ba7d3b8..d9f3622 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 @@
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/stats/pull/RawSettingsTelemetryUtil.java b/services/core/java/com/android/server/stats/pull/RawSettingsTelemetryUtils.java
similarity index 95%
rename from services/core/java/com/android/server/stats/pull/RawSettingsTelemetryUtil.java
rename to services/core/java/com/android/server/stats/pull/RawSettingsTelemetryUtils.java
index 905f38a..1e3203d 100644
--- a/services/core/java/com/android/server/stats/pull/RawSettingsTelemetryUtil.java
+++ b/services/core/java/com/android/server/stats/pull/RawSettingsTelemetryUtils.java
@@ -29,9 +29,9 @@
* is acceptable for telemetry system.
* For instance, raw setting values could be hard to visualize on dashboards, etc.
*/
-public final class RawSettingsTelemetryUtil {
+public final class RawSettingsTelemetryUtils {
- private static final String TAG = "SettingsTelemetryUtil";
+ private static final String TAG = "SettingsTelemetryUtils";
/**
* Get string that should be written as a value of settingKey and should be sent to telemetry
diff --git a/services/core/java/com/android/server/stats/pull/SettingsStatsUtil.java b/services/core/java/com/android/server/stats/pull/SettingsStatsUtil.java
index 2a4a39c..f745089 100644
--- a/services/core/java/com/android/server/stats/pull/SettingsStatsUtil.java
+++ b/services/core/java/com/android/server/stats/pull/SettingsStatsUtil.java
@@ -107,8 +107,8 @@
}
for (String key : proto.element) {
final String value = Settings.System.getStringForUser(resolver, key, userId);
- final String telemetryValue =
- RawSettingsTelemetryUtil.getTelemetrySettingFromRawVal(context, key, value);
+ final String telemetryValue = RawSettingsTelemetryUtils
+ .getTelemetrySettingFromRawVal(context, key, value);
output.add(createStatsEvent(atomTag, key, telemetryValue, userId,
flagsData.mDataType));
}
diff --git a/services/core/java/com/android/server/webkit/SystemImpl.java b/services/core/java/com/android/server/webkit/SystemImpl.java
index a821f545..c4d601d 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 @@
private static final String TAG_SIGNATURE = "signature";
private static final String TAG_FALLBACK = "isFallback";
private static final String PIN_GROUP = "webview";
+
+ private final Context mContext;
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();
- }
+ SystemImpl(Context context) {
+ mContext = context;
- public static SystemImpl getInstance() {
- return LazyHolder.INSTANCE;
- }
-
- private SystemImpl() {
int numFallbackPackages = 0;
int numAvailableByDefaultPackages = 0;
XmlResourceParser parser = null;
@@ -184,14 +178,14 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 ad32f62..3b77d07 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 @@
* @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 @@
* 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 043470f..7acb864 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 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 dcf20f9..b9be4a2 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 @@
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 @@
private final Object mLock = new Object();
- WebViewUpdateServiceImpl(Context context, SystemInterface systemInterface) {
- mContext = context;
+ WebViewUpdateServiceImpl(SystemInterface systemInterface) {
mSystemInterface = systemInterface;
}
@@ -173,7 +170,7 @@
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 @@
// 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 @@
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 @@
oldPackage = mCurrentWebViewPackage;
if (newProviderName != null) {
- mSystemInterface.updateUserSetting(mContext, newProviderName);
+ mSystemInterface.updateUserSetting(newProviderName);
}
try {
@@ -447,7 +442,7 @@
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 @@
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 @@
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 @@
@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 @@
@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 @@
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 @@
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 993597e..307c15b 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 @@
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 @@
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 @@
}
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 @@
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 @@
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 @@
// 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 @@
oldPackage = mCurrentWebViewPackage;
if (newProviderName != null) {
- mSystemInterface.updateUserSetting(mContext, newProviderName);
+ mSystemInterface.updateUserSetting(newProviderName);
}
try {
@@ -493,7 +486,7 @@
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 @@
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 @@
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 @@
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/AbsAppSnapshotController.java b/services/core/java/com/android/server/wm/AbsAppSnapshotController.java
index 2d8aa3f..09de01e 100644
--- a/services/core/java/com/android/server/wm/AbsAppSnapshotController.java
+++ b/services/core/java/com/android/server/wm/AbsAppSnapshotController.java
@@ -137,7 +137,7 @@
}
abstract ActivityRecord getTopActivity(TYPE source);
- abstract ActivityRecord getTopFullscreenActivity(TYPE source);
+ abstract WindowState getTopFullscreenWindow(TYPE source);
abstract ActivityManager.TaskDescription getTaskDescription(TYPE source);
/**
* Find the window for a given task to take a snapshot. Top child of the task is usually the one
@@ -465,10 +465,7 @@
*/
@WindowInsetsController.Appearance
private int getAppearance(TYPE source) {
- final ActivityRecord topFullscreenActivity = getTopFullscreenActivity(source);
- final WindowState topFullscreenWindow = topFullscreenActivity != null
- ? topFullscreenActivity.findMainWindow()
- : null;
+ final WindowState topFullscreenWindow = getTopFullscreenWindow(source);
if (topFullscreenWindow != null) {
return topFullscreenWindow.mAttrs.insetsFlags.appearance;
}
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 3076b87..400919a 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -53,6 +53,7 @@
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.app.WindowConfiguration.activityTypeToString;
+import static android.app.WindowConfiguration.isFloating;
import static android.app.admin.DevicePolicyResources.Drawables.Source.PROFILE_SWITCH_ANIMATION;
import static android.app.admin.DevicePolicyResources.Drawables.Style.OUTLINE;
import static android.app.admin.DevicePolicyResources.Drawables.WORK_PROFILE_ICON;
@@ -233,7 +234,7 @@
import static com.android.server.wm.ActivityTaskManagerService.RELAUNCH_REASON_NONE;
import static com.android.server.wm.ActivityTaskManagerService.RELAUNCH_REASON_WINDOWING_MODE_RESIZE;
import static com.android.server.wm.ActivityTaskManagerService.getInputDispatchingTimeoutMillisLocked;
-import static com.android.server.wm.DesktopModeLaunchParamsModifier.canEnterDesktopMode;
+import static com.android.server.wm.DesktopModeHelper.canEnterDesktopMode;
import static com.android.server.wm.IdentifierProto.HASH_CODE;
import static com.android.server.wm.IdentifierProto.TITLE;
import static com.android.server.wm.IdentifierProto.USER_ID;
@@ -335,6 +336,7 @@
import android.service.dreams.DreamActivity;
import android.service.voice.IVoiceInteractionSession;
import android.util.ArraySet;
+import android.util.DisplayMetrics;
import android.util.EventLog;
import android.util.Log;
import android.util.MergedConfiguration;
@@ -2865,7 +2867,10 @@
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();
}
@@ -2896,6 +2901,12 @@
|| 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();
}
@@ -8637,14 +8648,7 @@
resolvedConfig.windowConfiguration.setMaxBounds(mTmpBounds);
}
- applySizeOverrideIfNeeded(
- mDisplayContent,
- info.applicationInfo,
- newParentConfiguration,
- resolvedConfig,
- mOptOutEdgeToEdge,
- hasFixedRotationTransform(),
- getCompatDisplayInsets() != null);
+ applySizeOverrideIfNeeded(newParentConfiguration, parentWindowingMode, resolvedConfig);
mResolveConfigHint.resetTmpOverrides();
logAppCompatState();
@@ -8654,6 +8658,100 @@
return Rect.copyOrNull(mResolveConfigHint.mParentAppBoundsOverride);
}
+ /**
+ * If necessary, override configuration fields related to app bounds.
+ * This will happen when the app is targeting SDK earlier than 35.
+ * The insets and configuration has decoupled since SDK level 35, to make the system
+ * compatible to existing apps, override the configuration with legacy metrics. In legacy
+ * metrics, fields such as appBounds will exclude some of the system bar areas.
+ * The override contains all potentially affected fields in Configuration, including
+ * screenWidthDp, screenHeightDp, smallestScreenWidthDp, and orientation.
+ * All overrides to those fields should be in this method.
+ *
+ * TODO: Consider integrate this with computeConfigByResolveHint()
+ */
+ private void applySizeOverrideIfNeeded(Configuration newParentConfiguration,
+ int parentWindowingMode, Configuration inOutConfig) {
+ if (mDisplayContent == null) {
+ return;
+ }
+ final Rect parentBounds = newParentConfiguration.windowConfiguration.getBounds();
+ int rotation = newParentConfiguration.windowConfiguration.getRotation();
+ if (rotation == ROTATION_UNDEFINED && !isFixedRotationTransforming()) {
+ rotation = mDisplayContent.getRotation();
+ }
+ if (!mOptOutEdgeToEdge && (!mResolveConfigHint.mUseOverrideInsetsForConfig
+ || getCompatDisplayInsets() != null
+ || (isFloating(parentWindowingMode)
+ // Check the requested windowing mode of activity as well in case it is
+ // switching between PiP and fullscreen.
+ && (inOutConfig.windowConfiguration.getWindowingMode()
+ == WINDOWING_MODE_UNDEFINED
+ || isFloating(inOutConfig.windowConfiguration.getWindowingMode())))
+ || rotation == ROTATION_UNDEFINED)) {
+ // If the insets configuration decoupled logic is not enabled for the app, or the app
+ // already has a compat override, or the context doesn't contain enough info to
+ // calculate the override, skip the override.
+ return;
+ }
+ // Make sure the orientation related fields will be updated by the override insets, because
+ // fixed rotation has assigned the fields from display's configuration.
+ if (hasFixedRotationTransform()) {
+ inOutConfig.windowConfiguration.setAppBounds(null);
+ inOutConfig.screenWidthDp = Configuration.SCREEN_WIDTH_DP_UNDEFINED;
+ inOutConfig.screenHeightDp = Configuration.SCREEN_HEIGHT_DP_UNDEFINED;
+ inOutConfig.smallestScreenWidthDp = Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED;
+ inOutConfig.orientation = ORIENTATION_UNDEFINED;
+ }
+
+ // Override starts here.
+ final boolean rotated = (rotation == ROTATION_90 || rotation == ROTATION_270);
+ final int dw = rotated ? mDisplayContent.mBaseDisplayHeight
+ : mDisplayContent.mBaseDisplayWidth;
+ final int dh = rotated ? mDisplayContent.mBaseDisplayWidth
+ : mDisplayContent.mBaseDisplayHeight;
+ final Rect nonDecorInsets = mDisplayContent.getDisplayPolicy()
+ .getDecorInsetsInfo(rotation, dw, dh).mOverrideNonDecorInsets;
+ // This should be the only place override the configuration for ActivityRecord. Override
+ // the value if not calculated yet.
+ Rect outAppBounds = inOutConfig.windowConfiguration.getAppBounds();
+ if (outAppBounds == null || outAppBounds.isEmpty()) {
+ inOutConfig.windowConfiguration.setAppBounds(parentBounds);
+ outAppBounds = inOutConfig.windowConfiguration.getAppBounds();
+ outAppBounds.inset(nonDecorInsets);
+ }
+ float density = inOutConfig.densityDpi;
+ if (density == Configuration.DENSITY_DPI_UNDEFINED) {
+ density = newParentConfiguration.densityDpi;
+ }
+ density *= DisplayMetrics.DENSITY_DEFAULT_SCALE;
+ if (inOutConfig.screenWidthDp == Configuration.SCREEN_WIDTH_DP_UNDEFINED) {
+ inOutConfig.screenWidthDp = (int) (outAppBounds.width() / density + 0.5f);
+ }
+ if (inOutConfig.screenHeightDp == Configuration.SCREEN_HEIGHT_DP_UNDEFINED) {
+ inOutConfig.screenHeightDp = (int) (outAppBounds.height() / density + 0.5f);
+ }
+ if (inOutConfig.smallestScreenWidthDp
+ == Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED
+ && parentWindowingMode == WINDOWING_MODE_FULLSCREEN) {
+ // For the case of PIP transition and multi-window environment, the
+ // smallestScreenWidthDp is handled already. Override only if the app is in
+ // fullscreen.
+ final DisplayInfo info = new DisplayInfo(mDisplayContent.getDisplayInfo());
+ mDisplayContent.computeSizeRanges(info, rotated, dw, dh,
+ mDisplayContent.getDisplayMetrics().density,
+ inOutConfig, true /* overrideConfig */);
+ }
+
+ // It's possible that screen size will be considered in different orientation with or
+ // without considering the system bar insets. Override orientation as well.
+ if (inOutConfig.orientation == ORIENTATION_UNDEFINED) {
+ inOutConfig.orientation =
+ (inOutConfig.screenWidthDp <= inOutConfig.screenHeightDp)
+ ? ORIENTATION_PORTRAIT : ORIENTATION_LANDSCAPE;
+ }
+ }
+
private void computeConfigByResolveHint(@NonNull Configuration resolvedConfig,
@NonNull Configuration parentConfig) {
task.computeConfigResourceOverrides(resolvedConfig, parentConfig, mResolveConfigHint);
@@ -8875,7 +8973,7 @@
* <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.
@@ -8884,7 +8982,7 @@
* </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 0c32dfc..bc82271 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 @@
}
final boolean cycleThroughStop =
- mWmService.mLetterboxConfiguration
+ mWmService.mAppCompatConfiguration
.isCameraCompatRefreshCycleThroughStopEnabled()
&& !activity.mAppCompatController.getAppCompatCameraOverrides()
.shouldRefreshActivityViaPauseForCameraCompat();
@@ -114,7 +114,7 @@
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/ActivitySnapshotController.java b/services/core/java/com/android/server/wm/ActivitySnapshotController.java
index 62fb4bf..48bc813 100644
--- a/services/core/java/com/android/server/wm/ActivitySnapshotController.java
+++ b/services/core/java/com/android/server/wm/ActivitySnapshotController.java
@@ -590,10 +590,8 @@
return activity;
}
- @Override
- ActivityRecord getTopFullscreenActivity(ActivityRecord activity) {
- final WindowState win = activity.findMainWindow();
- return (win != null && win.mAttrs.isFullscreen()) ? activity : null;
+ WindowState getTopFullscreenWindow(ActivityRecord activity) {
+ return activity.findMainWindow();
}
@Override
diff --git a/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java b/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java
index cf008e7..05d4c82 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 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 @@
@NonNull
private final ActivityRecord mActivityRecord;
@NonNull
- private final LetterboxConfiguration mLetterboxConfiguration;
+ private final AppCompatConfiguration mAppCompatConfiguration;
@NonNull
private final UserAspectRatioState mUserAspectRatioState;
@@ -80,12 +80,12 @@
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 @@
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 @@
boolean isUserFullscreenOverrideEnabled() {
if (mAllowUserAspectRatioOverrideOptProp.isFalse()
|| mAllowUserAspectRatioFullscreenOverrideOptProp.isFalse()
- || !mLetterboxConfiguration.isUserAppAspectRatioFullscreenEnabled()) {
+ || !mAppCompatConfiguration.isUserAppAspectRatioFullscreenEnabled()) {
return false;
}
return true;
@@ -168,7 +168,7 @@
// 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 @@
: 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 @@
}
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 a7d2ecc..b23e75a 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.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 0d108e1..93a8663 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 @@
@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 @@
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 @@
* 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 53729a2..1562cf6 100644
--- a/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java
+++ b/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java
@@ -45,9 +45,9 @@
// 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()
- && DesktopModeLaunchParamsModifier.canEnterDesktopMode(wmService.mContext);
+ && DesktopModeHelper.canEnterDesktopMode(wmService.mContext);
if (needsDisplayRotationCompatPolicy || needsCameraCompatFreeformPolicy) {
mCameraStateMonitor = new CameraStateMonitor(displayContent, wmService.mH);
mActivityRefresher = new ActivityRefresher(wmService, wmService.mH);
diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/AppCompatConfiguration.java
similarity index 96%
rename from services/core/java/com/android/server/wm/LetterboxConfiguration.java
rename to services/core/java/com/android/server/wm/AppCompatConfiguration.java
index 0161ae5..ffa4251 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.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 @@
// 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 @@
// 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 @@
}
@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 @@
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 @@
/**
* 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 @@
}
/**
- * 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 @@
/**
* 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 @@
}
/**
- * 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 @@
: 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 @@
"mLetterboxBookModePositionMultiplier");
}
- /*
+ /**
* Whether horizontal reachability repositioning is allowed for letterboxed fullscreen apps in
* landscape device orientation.
*/
@@ -751,7 +751,7 @@
return mIsHorizontalReachabilityEnabled;
}
- /*
+ /**
* Whether vertical reachability repositioning is allowed for letterboxed fullscreen apps in
* portrait device orientation.
*/
@@ -759,7 +759,7 @@
return mIsVerticalReachabilityEnabled;
}
- /*
+ /**
* Whether automatic horizontal reachability repositioning in book mode is allowed for
* letterboxed fullscreen apps in landscape device orientation.
*/
@@ -821,7 +821,7 @@
R.bool.config_letterboxIsAutomaticReachabilityInBookModeEnabled);
}
- /*
+ /**
* Gets default horizontal position of the letterboxed app window when horizontal reachability
* is enabled.
*
@@ -833,7 +833,7 @@
return mDefaultPositionForHorizontalReachability;
}
- /*
+ /**
* Gets default vertical position of the letterboxed app window when vertical reachability is
* enabled.
*
@@ -889,7 +889,7 @@
*/
void setPersistentLetterboxPositionForHorizontalReachability(boolean forBookMode,
@LetterboxHorizontalReachabilityPosition int position) {
- mLetterboxConfigurationPersister.setLetterboxPositionForHorizontalReachability(
+ mAppCompatConfigurationPersister.setLetterboxPositionForHorizontalReachability(
forBookMode, position);
}
@@ -899,7 +899,7 @@
*/
void setPersistentLetterboxPositionForVerticalReachability(boolean forTabletopMode,
@LetterboxVerticalReachabilityPosition int position) {
- mLetterboxConfigurationPersister.setLetterboxPositionForVerticalReachability(
+ mAppCompatConfigurationPersister.setLetterboxPositionForVerticalReachability(
forTabletopMode, position);
}
@@ -909,11 +909,11 @@
* 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 @@
* 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 @@
? 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 @@
*/
float getHorizontalMultiplierForReachability(boolean isDeviceInBookMode) {
final int letterboxPositionForHorizontalReachability =
- mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability(
+ mAppCompatConfigurationPersister.getLetterboxPositionForHorizontalReachability(
isDeviceInBookMode);
switch (letterboxPositionForHorizontalReachability) {
case LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT:
@@ -985,7 +985,7 @@
}
}
- /*
+ /**
* 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 @@
*/
float getVerticalMultiplierForReachability(boolean isDeviceInTabletopMode) {
final int letterboxPositionForVerticalReachability =
- mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability(
+ mAppCompatConfigurationPersister.getLetterboxPositionForVerticalReachability(
isDeviceInTabletopMode);
switch (letterboxPositionForVerticalReachability) {
case LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP:
@@ -1009,23 +1009,23 @@
}
}
- /*
+ /**
* 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 @@
|| 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 @@
/**
* 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 @@
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 @@
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
similarity index 96%
rename from services/core/java/com/android/server/wm/LetterboxConfigurationPersister.java
rename to services/core/java/com/android/server/wm/AppCompatConfigurationPersister.java
index 38aa903..852ce04 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.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 @@
/**
* 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 @@
@NonNull
private final PersisterQueue mPersisterQueue;
- LetterboxConfigurationPersister(
+ AppCompatConfigurationPersister(
@NonNull Supplier<Integer> defaultHorizontalReachabilitySupplier,
@NonNull Supplier<Integer> defaultVerticalReachabilitySupplier,
@NonNull Supplier<Integer> defaultBookModeReachabilitySupplier,
@@ -106,7 +106,7 @@
}
@VisibleForTesting
- LetterboxConfigurationPersister(
+ AppCompatConfigurationPersister(
@NonNull Supplier<Integer> defaultHorizontalReachabilitySupplier,
@NonNull Supplier<Integer> defaultVerticalReachabilitySupplier,
@NonNull Supplier<Integer> defaultBookModeReachabilitySupplier,
@@ -233,7 +233,7 @@
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 @@
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 @@
} 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 3eed96d..f9e2507 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 @@
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 9bf8011..0adf825 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 @@
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 @@
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 f6f93f9..b611ba9 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 @@
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 @@
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/ConfigurationContainer.java b/services/core/java/com/android/server/wm/ConfigurationContainer.java
index a38ac9b..efd5202 100644
--- a/services/core/java/com/android/server/wm/ConfigurationContainer.java
+++ b/services/core/java/com/android/server/wm/ConfigurationContainer.java
@@ -22,23 +22,14 @@
import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
-import static android.app.WindowConfiguration.ROTATION_UNDEFINED;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.app.WindowConfiguration.activityTypeToString;
-import static android.app.WindowConfiguration.isFloating;
import static android.app.WindowConfiguration.windowingModeToString;
import static android.app.WindowConfigurationProto.WINDOWING_MODE;
import static android.content.ConfigurationProto.WINDOW_CONFIGURATION;
-import static android.content.pm.ActivityInfo.INSETS_DECOUPLED_CONFIGURATION_ENFORCED;
-import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_INSETS_DECOUPLED_CONFIGURATION;
-import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
-import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
-import static android.content.res.Configuration.ORIENTATION_UNDEFINED;
-import static android.view.Surface.ROTATION_270;
-import static android.view.Surface.ROTATION_90;
import static com.android.server.wm.ConfigurationContainerProto.FULL_CONFIGURATION;
import static com.android.server.wm.ConfigurationContainerProto.MERGED_OVERRIDE_CONFIGURATION;
@@ -47,14 +38,11 @@
import android.annotation.CallSuper;
import android.annotation.NonNull;
import android.app.WindowConfiguration;
-import android.content.pm.ApplicationInfo;
import android.content.res.Configuration;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.LocaleList;
-import android.util.DisplayMetrics;
import android.util.proto.ProtoOutputStream;
-import android.view.DisplayInfo;
import com.android.internal.annotations.VisibleForTesting;
@@ -185,111 +173,6 @@
mResolvedOverrideConfiguration.setTo(mRequestedOverrideConfiguration);
}
- /**
- * If necessary, override configuration fields related to app bounds.
- * This will happen when the app is targeting SDK earlier than 35.
- * The insets and configuration has decoupled since SDK level 35, to make the system
- * compatible to existing apps, override the configuration with legacy metrics. In legacy
- * metrics, fields such as appBounds will exclude some of the system bar areas.
- * The override contains all potentially affected fields in Configuration, including
- * screenWidthDp, screenHeightDp, smallestScreenWidthDp, and orientation.
- * All overrides to those fields should be in this method.
- *
- * TODO: Consider integrate this with computeConfigByResolveHint()
- */
- static void applySizeOverrideIfNeeded(DisplayContent displayContent, ApplicationInfo appInfo,
- Configuration newParentConfiguration, Configuration inOutConfig,
- boolean optOutEdgeToEdge, boolean hasFixedRotationTransform,
- boolean hasCompatDisplayInsets) {
- if (displayContent == null) {
- return;
- }
- final boolean useOverrideInsetsForConfig =
- displayContent.mWmService.mFlags.mInsetsDecoupledConfiguration
- ? !appInfo.isChangeEnabled(INSETS_DECOUPLED_CONFIGURATION_ENFORCED)
- && !appInfo.isChangeEnabled(
- OVERRIDE_ENABLE_INSETS_DECOUPLED_CONFIGURATION)
- : appInfo.isChangeEnabled(OVERRIDE_ENABLE_INSETS_DECOUPLED_CONFIGURATION);
- final int parentWindowingMode =
- newParentConfiguration.windowConfiguration.getWindowingMode();
- final boolean isFloating = isFloating(parentWindowingMode)
- // Check the requested windowing mode of activity as well in case it is
- // switching between PiP and fullscreen.
- && (inOutConfig.windowConfiguration.getWindowingMode() == WINDOWING_MODE_UNDEFINED
- || isFloating(inOutConfig.windowConfiguration.getWindowingMode()));
- final Rect parentBounds = newParentConfiguration.windowConfiguration.getBounds();
- int rotation = newParentConfiguration.windowConfiguration.getRotation();
- if (rotation == ROTATION_UNDEFINED && !hasFixedRotationTransform) {
- rotation = displayContent.getRotation();
- }
- if (!optOutEdgeToEdge && (!useOverrideInsetsForConfig
- || hasCompatDisplayInsets
- || isFloating
- || rotation == ROTATION_UNDEFINED)) {
- // If the insets configuration decoupled logic is not enabled for the app, or the app
- // already has a compat override, or the context doesn't contain enough info to
- // calculate the override, skip the override.
- return;
- }
- // Make sure the orientation related fields will be updated by the override insets, because
- // fixed rotation has assigned the fields from display's configuration.
- if (hasFixedRotationTransform) {
- inOutConfig.windowConfiguration.setAppBounds(null);
- inOutConfig.screenWidthDp = Configuration.SCREEN_WIDTH_DP_UNDEFINED;
- inOutConfig.screenHeightDp = Configuration.SCREEN_HEIGHT_DP_UNDEFINED;
- inOutConfig.smallestScreenWidthDp = Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED;
- inOutConfig.orientation = ORIENTATION_UNDEFINED;
- }
-
- // Override starts here.
- final boolean rotated = (rotation == ROTATION_90 || rotation == ROTATION_270);
- final int dw = rotated
- ? displayContent.mBaseDisplayHeight
- : displayContent.mBaseDisplayWidth;
- final int dh = rotated
- ? displayContent.mBaseDisplayWidth
- : displayContent.mBaseDisplayHeight;
- final Rect nonDecorFrame = displayContent.getDisplayPolicy()
- .getDecorInsetsInfo(rotation, dw, dh).mOverrideNonDecorFrame;
- // This should be the only place override the configuration for ActivityRecord. Override
- // the value if not calculated yet.
- Rect outAppBounds = inOutConfig.windowConfiguration.getAppBounds();
- if (outAppBounds == null || outAppBounds.isEmpty()) {
- inOutConfig.windowConfiguration.setAppBounds(parentBounds);
- outAppBounds = inOutConfig.windowConfiguration.getAppBounds();
- outAppBounds.intersect(nonDecorFrame);
- }
- float density = inOutConfig.densityDpi;
- if (density == Configuration.DENSITY_DPI_UNDEFINED) {
- density = newParentConfiguration.densityDpi;
- }
- density *= DisplayMetrics.DENSITY_DEFAULT_SCALE;
- if (inOutConfig.screenWidthDp == Configuration.SCREEN_WIDTH_DP_UNDEFINED) {
- inOutConfig.screenWidthDp = (int) (outAppBounds.width() / density + 0.5f);
- }
- if (inOutConfig.screenHeightDp == Configuration.SCREEN_HEIGHT_DP_UNDEFINED) {
- inOutConfig.screenHeightDp = (int) (outAppBounds.height() / density + 0.5f);
- }
- if (inOutConfig.smallestScreenWidthDp == Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED
- && parentWindowingMode == WINDOWING_MODE_FULLSCREEN) {
- // For the case of PIP transition and multi-window environment, the
- // smallestScreenWidthDp is handled already. Override only if the app is in
- // fullscreen.
- final DisplayInfo info = new DisplayInfo(displayContent.getDisplayInfo());
- displayContent.computeSizeRanges(info, rotated, dw, dh,
- displayContent.getDisplayMetrics().density,
- inOutConfig, true /* overrideConfig */);
- }
-
- // It's possible that screen size will be considered in different orientation with or
- // without considering the system bar insets. Override orientation as well.
- if (inOutConfig.orientation == ORIENTATION_UNDEFINED) {
- inOutConfig.orientation = (inOutConfig.screenWidthDp <= inOutConfig.screenHeightDp)
- ? ORIENTATION_PORTRAIT
- : ORIENTATION_LANDSCAPE;
- }
- }
-
/** Returns {@code true} if requested override override configuration is not empty. */
boolean hasRequestedOverrideConfiguration() {
return mHasOverrideConfiguration;
diff --git a/services/core/java/com/android/server/wm/DesktopModeBoundsCalculator.java b/services/core/java/com/android/server/wm/DesktopModeBoundsCalculator.java
new file mode 100644
index 0000000..3ecdff6
--- /dev/null
+++ b/services/core/java/com/android/server/wm/DesktopModeBoundsCalculator.java
@@ -0,0 +1,357 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static android.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;
+
+/**
+ * Calculates the value of the {@link LaunchParamsController.LaunchParams} bounds for the
+ * {@link DesktopModeLaunchParamsModifier}.
+ */
+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,
+ * android manifest and task configuration.
+ */
+ static void updateInitialBounds(@NonNull Task task, @Nullable ActivityInfo.WindowLayout layout,
+ @Nullable ActivityRecord activity, @Nullable ActivityOptions options,
+ @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 Rect stableBounds = new Rect();
+ task.getDisplayArea().getStableRect(stableBounds);
+
+ if (options != null && options.getLaunchBounds() != null) {
+ outBounds.set(options.getLaunchBounds());
+ logger.accept("inherit-from-options=" + outBounds);
+ } else if (layout != null) {
+ final int verticalGravity = layout.gravity & Gravity.VERTICAL_GRAVITY_MASK;
+ final int horizontalGravity = layout.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
+ if (layout.hasSpecifiedSize()) {
+ calculateLayoutBounds(stableBounds, layout, outBounds,
+ 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) {
+ outBounds.set(calculateInitialBounds(task, activity, stableBounds));
+ applyLayoutGravity(verticalGravity, horizontalGravity, outBounds,
+ stableBounds);
+ logger.accept("layout specifies gravity, applying desired bounds and gravity");
+ }
+ } else {
+ outBounds.set(calculateInitialBounds(task, activity, stableBounds));
+ logger.accept("layout not specified, applying desired 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 @NonNull Rect centerInScreen(@NonNull Size desiredSize,
+ @NonNull Rect screenBounds) {
+ // 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
new file mode 100644
index 0000000..e0c0c2c
--- /dev/null
+++ b/services/core/java/com/android/server/wm/DesktopModeHelper.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.os.SystemProperties;
+
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.wm.utils.DesktopModeFlagsUtil;
+
+/**
+ * Constants for desktop mode feature
+ */
+public final class DesktopModeHelper {
+ /**
+ * 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);
+
+ /** Whether desktop mode is enabled. */
+ static boolean isDesktopModeEnabled(@NonNull Context context) {
+ return DesktopModeFlagsUtil.DESKTOP_WINDOWING_MODE.isEnabled(context);
+ }
+
+ /**
+ * Return {@code true} if desktop mode should be restricted to supported devices.
+ */
+ @VisibleForTesting
+ static boolean shouldEnforceDeviceRestrictions() {
+ return ENFORCE_DEVICE_RESTRICTIONS;
+ }
+
+ /**
+ * Return {@code true} if the current device supports desktop mode.
+ */
+ // TODO(b/337819319): use a companion object instead.
+ @VisibleForTesting
+ static boolean isDesktopModeSupported(@NonNull Context context) {
+ return context.getResources().getBoolean(R.bool.config_isDesktopModeSupported);
+ }
+
+ /**
+ * Return {@code true} if desktop mode can be entered on the current device.
+ */
+ static boolean canEnterDesktopMode(@NonNull Context context) {
+ 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 66653ca..548addb 100644
--- a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
+++ b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
@@ -18,46 +18,28 @@
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.LaunchParamsUtil.applyLayoutGravity;
-import static com.android.server.wm.LaunchParamsUtil.calculateLayoutBounds;
+import static com.android.server.wm.DesktopModeHelper.canEnterDesktopMode;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityOptions;
import android.content.Context;
import android.content.pm.ActivityInfo;
-import android.graphics.Rect;
-import android.os.SystemProperties;
-import android.util.Size;
import android.util.Slog;
-import android.view.Gravity;
-import com.android.internal.R;
-import com.android.internal.annotations.VisibleForTesting;
import com.android.server.wm.LaunchParamsController.LaunchParamsModifier;
-import com.android.window.flags.Flags;
/**
* The class that defines default launch params for tasks in desktop mode
*/
-public class DesktopModeLaunchParamsModifier implements LaunchParamsModifier {
+class DesktopModeLaunchParamsModifier implements LaunchParamsModifier {
private static final String TAG =
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;
@@ -67,8 +49,8 @@
public int onCalculate(@Nullable Task task, @Nullable ActivityInfo.WindowLayout layout,
@Nullable ActivityRecord activity, @Nullable ActivityRecord source,
@Nullable ActivityOptions options, @Nullable ActivityStarter.Request request, int phase,
- LaunchParamsController.LaunchParams currentParams,
- LaunchParamsController.LaunchParams outParams) {
+ @NonNull LaunchParamsController.LaunchParams currentParams,
+ @NonNull LaunchParamsController.LaunchParams outParams) {
initLogBuilder(task, activity);
int result = calculate(task, layout, activity, source, options, request, phase,
@@ -80,15 +62,15 @@
private int calculate(@Nullable Task task, @Nullable ActivityInfo.WindowLayout layout,
@Nullable ActivityRecord activity, @Nullable ActivityRecord source,
@Nullable ActivityOptions options, @Nullable ActivityStarter.Request request, int phase,
- LaunchParamsController.LaunchParams currentParams,
- LaunchParamsController.LaunchParams outParams) {
+ @NonNull LaunchParamsController.LaunchParams currentParams,
+ @NonNull LaunchParamsController.LaunchParams outParams) {
if (!canEnterDesktopMode(mContext)) {
appendLog("desktop mode is not enabled, skipping");
return RESULT_SKIP;
}
- if (task == null) {
+ if (task == null || !task.isAttached()) {
appendLog("task null, skipping");
return RESULT_SKIP;
}
@@ -123,59 +105,12 @@
return RESULT_SKIP;
}
- // 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 Rect stableBounds = new Rect();
- task.getDisplayArea().getStableRect(stableBounds);
- final int desiredWidth = (int) (stableBounds.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
- final int desiredHeight = (int) (stableBounds.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
-
- if (options != null && options.getLaunchBounds() != null) {
- outParams.mBounds.set(options.getLaunchBounds());
- appendLog("inherit-from-options=" + outParams.mBounds);
- } else if (layout != null) {
- final int verticalGravity = layout.gravity & Gravity.VERTICAL_GRAVITY_MASK;
- final int horizontalGravity = layout.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
- if (layout.hasSpecifiedSize()) {
- calculateLayoutBounds(stableBounds, layout, outParams.mBounds,
- new Size(desiredWidth, desiredHeight));
- applyLayoutGravity(verticalGravity, horizontalGravity, outParams.mBounds,
- stableBounds);
- appendLog("layout specifies sizes, inheriting size and applying gravity");
- } else if (verticalGravity > 0 || horizontalGravity > 0) {
- calculateAndCentreInitialBounds(task, outParams);
- applyLayoutGravity(verticalGravity, horizontalGravity, outParams.mBounds,
- stableBounds);
- appendLog("layout specifies gravity, applying desired bounds and gravity");
- }
- } else {
- calculateAndCentreInitialBounds(task, outParams);
- appendLog("layout not specified, applying desired bounds");
- }
-
+ DesktopModeBoundsCalculator.updateInitialBounds(task, layout, activity, options,
+ outParams.mBounds, this::appendLog);
appendLog("final desktop mode task bounds set to %s", outParams.mBounds);
return RESULT_CONTINUE;
}
- /**
- * Calculates the initial height and width of a task in desktop mode and centers it within the
- * window bounds.
- */
- private void calculateAndCentreInitialBounds(Task task,
- LaunchParamsController.LaunchParams outParams) {
- // TODO(b/319819547): Account for app constraints so apps do not become letterboxed
- final Rect screenBounds = task.getDisplayArea().getBounds();
- // 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);
- outParams.mBounds.right = desiredWidth;
- outParams.mBounds.bottom = desiredHeight;
- outParams.mBounds.offset(screenBounds.centerX() - outParams.mBounds.centerX(),
- screenBounds.centerY() - outParams.mBounds.centerY());
- }
-
private void initLogBuilder(Task task, ActivityRecord activity) {
if (DEBUG) {
mLogBuilder = new StringBuilder(
@@ -190,34 +125,4 @@
private void outputLog() {
if (DEBUG) Slog.d(TAG, mLogBuilder.toString());
}
-
- /** Whether desktop mode is enabled. */
- static boolean isDesktopModeEnabled() {
- return Flags.enableDesktopWindowingMode();
- }
-
- /**
- * Return {@code true} if desktop mode should be restricted to supported devices.
- */
- @VisibleForTesting
- static boolean enforceDeviceRestrictions() {
- return ENFORCE_DEVICE_RESTRICTIONS;
- }
-
- /**
- * Return {@code true} if the current device supports desktop mode.
- */
- // TODO(b/337819319): use a companion object instead.
- @VisibleForTesting
- static boolean isDesktopModeSupported(@NonNull Context context) {
- return context.getResources().getBoolean(R.bool.config_isDesktopModeSupported);
- }
-
- /**
- * Return {@code true} if desktop mode can be entered on the current device.
- */
- static boolean canEnterDesktopMode(@NonNull Context context) {
- return isDesktopModeEnabled()
- && (!enforceDeviceRestrictions() || isDesktopModeSupported(context));
- }
}
diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java
index d2976b0..8272e16 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 @@
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 1a0124a..63fe94c 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 @@
* </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 de70c4d..094434d 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 @@
@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 @@
* <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 3483842..dcadb0f 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 @@
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 235d6cd..be8e806 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_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 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 @@
private final Point mTmpPoint = new Point();
- private final LetterboxConfiguration mLetterboxConfiguration;
+ private final AppCompatConfiguration mAppCompatConfiguration;
private final ActivityRecord mActivityRecord;
@@ -95,7 +95,7 @@
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 @@
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 @@
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 @@
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 @@
}
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 @@
* @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 @@
.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 @@
}
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 @@
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 @@
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 @@
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 @@
.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 @@
.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 @@
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 @@
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 @@
+ "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 @@
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 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 @@
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 @@
}
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 @@
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 @@
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 @@
}
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 @@
}
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 @@
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/StartingSurfaceController.java b/services/core/java/com/android/server/wm/StartingSurfaceController.java
index cff40c7..a1e6701 100644
--- a/services/core/java/com/android/server/wm/StartingSurfaceController.java
+++ b/services/core/java/com/android/server/wm/StartingSurfaceController.java
@@ -146,7 +146,10 @@
+ activity);
return null;
}
- final WindowState mainWindow = activity.findMainWindow(false);
+ // For snapshot surface, the top activity could be trampoline activity, so here should
+ // search for top fullscreen activity in the task.
+ final WindowState mainWindow = task
+ .getTopFullscreenMainWindow(false /* includeStartingApp */);
if (mainWindow == null) {
Slog.w(TAG, "TaskSnapshotSurface.create: no main window in " + activity);
return null;
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 74f8a068..8f83a7c 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -3007,11 +3007,17 @@
return r.getTask().mTaskId != taskId && r.token != notTop && r.canBeTopRunning();
}
- ActivityRecord getTopFullscreenActivity() {
- return getActivity((r) -> {
- final WindowState win = r.findMainWindow();
- return (win != null && win.mAttrs.isFullscreen());
+ WindowState getTopFullscreenMainWindow(boolean includeStartingApp) {
+ final WindowState[] candidate = new WindowState[1];
+ getActivity((r) -> {
+ final WindowState win = r.findMainWindow(includeStartingApp);
+ if (win != null && win.mAttrs.isFullscreen()) {
+ candidate[0] = win;
+ return true;
+ }
+ return false;
});
+ return candidate[0];
}
/**
@@ -3377,7 +3383,7 @@
// 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();
}
@@ -3579,14 +3585,12 @@
// starting window because persisted configuration does not effect to Task.
info.taskInfo.configuration.setTo(activity.getConfiguration());
if (!Flags.drawSnapshotAspectRatioMatch()) {
- final ActivityRecord topFullscreenActivity = getTopFullscreenActivity();
- if (topFullscreenActivity != null) {
- final WindowState mainWindow = topFullscreenActivity.findMainWindow(false);
- if (mainWindow != null) {
- info.topOpaqueWindowInsetsState =
- mainWindow.getInsetsStateWithVisibilityOverride();
- info.topOpaqueWindowLayoutParams = mainWindow.getAttrs();
- }
+ final WindowState mainWindow =
+ getTopFullscreenMainWindow(false /* includeStartingApp */);
+ if (mainWindow != null) {
+ info.topOpaqueWindowInsetsState =
+ mainWindow.getInsetsStateWithVisibilityOverride();
+ info.topOpaqueWindowLayoutParams = mainWindow.getAttrs();
}
}
return info;
diff --git a/services/core/java/com/android/server/wm/TaskSnapshotController.java b/services/core/java/com/android/server/wm/TaskSnapshotController.java
index 4218f8f..57c7753 100644
--- a/services/core/java/com/android/server/wm/TaskSnapshotController.java
+++ b/services/core/java/com/android/server/wm/TaskSnapshotController.java
@@ -261,8 +261,8 @@
}
@Override
- ActivityRecord getTopFullscreenActivity(Task source) {
- return source.getTopFullscreenActivity();
+ WindowState getTopFullscreenWindow(Task source) {
+ return source.getTopFullscreenMainWindow(true /* includeStartingApp */);
}
@Override
diff --git a/services/core/java/com/android/server/wm/TransparentPolicy.java b/services/core/java/com/android/server/wm/TransparentPolicy.java
index cdb14ab..2f46103 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 @@
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 ebbf6e3..acd8b3f 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.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 @@
private boolean mAnimationsDisabled = false;
boolean mPointerLocationEnabled = false;
- final LetterboxConfiguration mLetterboxConfiguration;
+ final AppCompatConfiguration mAppCompatConfiguration;
private boolean mIsIgnoreOrientationRequestDisabled;
@@ -1294,7 +1294,7 @@
| 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 @@
*/
boolean isIgnoreOrientationRequestDisabled() {
return mIsIgnoreOrientationRequestDisabled
- || !mLetterboxConfiguration.isIgnoreOrientationRequestAllowed();
+ || !mAppCompatConfiguration.isIgnoreOrientationRequestAllowed();
}
@Override
@@ -9992,7 +9992,7 @@
*/
@Override
public int getLetterboxBackgroundColorInArgb() {
- return mLetterboxConfiguration.getLetterboxBackgroundColor().toArgb();
+ return mAppCompatConfiguration.getLetterboxBackgroundColor().toArgb();
}
/**
@@ -10000,8 +10000,8 @@
*/
@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 6febe80..51d5bc0 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 @@
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 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 @@
// 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 @@
return -1;
}
synchronized (mInternal.mGlobalLock) {
- mLetterboxConfiguration.setFixedOrientationLetterboxAspectRatio(aspectRatio);
+ mAppCompatConfiguration.setFixedOrientationLetterboxAspectRatio(aspectRatio);
}
return 0;
}
@@ -698,7 +698,7 @@
return -1;
}
synchronized (mInternal.mGlobalLock) {
- mLetterboxConfiguration.setDefaultMinAspectRatioForUnresizableApps(aspectRatio);
+ mAppCompatConfiguration.setDefaultMinAspectRatioForUnresizableApps(aspectRatio);
}
return 0;
}
@@ -717,7 +717,7 @@
return -1;
}
synchronized (mInternal.mGlobalLock) {
- mLetterboxConfiguration.setLetterboxActivityCornersRadius(cornersRadius);
+ mAppCompatConfiguration.setLetterboxActivityCornersRadius(cornersRadius);
}
return 0;
}
@@ -752,7 +752,7 @@
return -1;
}
synchronized (mInternal.mGlobalLock) {
- mLetterboxConfiguration.setLetterboxBackgroundTypeOverride(backgroundType);
+ mAppCompatConfiguration.setLetterboxBackgroundTypeOverride(backgroundType);
}
return 0;
}
@@ -770,7 +770,7 @@
return -1;
}
synchronized (mInternal.mGlobalLock) {
- mLetterboxConfiguration.setLetterboxBackgroundColorResourceId(colorId);
+ mAppCompatConfiguration.setLetterboxBackgroundColorResourceId(colorId);
}
return 0;
}
@@ -787,7 +787,7 @@
return -1;
}
synchronized (mInternal.mGlobalLock) {
- mLetterboxConfiguration.setLetterboxBackgroundColor(color);
+ mAppCompatConfiguration.setLetterboxBackgroundColor(color);
}
return 0;
}
@@ -809,7 +809,7 @@
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 @@
return -1;
}
synchronized (mInternal.mGlobalLock) {
- mLetterboxConfiguration.setLetterboxBackgroundWallpaperDarkScrimAlpha(alpha);
+ mAppCompatConfiguration.setLetterboxBackgroundWallpaperDarkScrimAlpha(alpha);
}
return 0;
}
@@ -849,7 +849,7 @@
}
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 @@
}
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 @@
return -1;
}
synchronized (mInternal.mGlobalLock) {
- mLetterboxConfiguration.setDefaultPositionForHorizontalReachability(position);
+ mAppCompatConfiguration.setDefaultPositionForHorizontalReachability(position);
}
return 0;
}
@@ -939,7 +939,7 @@
return -1;
}
synchronized (mInternal.mGlobalLock) {
- mLetterboxConfiguration.setDefaultPositionForVerticalReachability(position);
+ mAppCompatConfiguration.setDefaultPositionForVerticalReachability(position);
}
return 0;
}
@@ -970,7 +970,7 @@
return -1;
}
synchronized (mInternal.mGlobalLock) {
- mLetterboxConfiguration.setPersistentLetterboxPositionForHorizontalReachability(
+ mAppCompatConfiguration.setPersistentLetterboxPositionForHorizontalReachability(
false /* IsInBookMode */, position);
}
return 0;
@@ -1002,7 +1002,7 @@
return -1;
}
synchronized (mInternal.mGlobalLock) {
- mLetterboxConfiguration.setPersistentLetterboxPositionForVerticalReachability(
+ mAppCompatConfiguration.setPersistentLetterboxPositionForVerticalReachability(
false /* forTabletopMode */, position);
}
return 0;
@@ -1074,15 +1074,15 @@
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 @@
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 @@
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 @@
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 @@
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 bbd9c0a..12c5073 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 @@
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.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 @@
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 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 @@
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 @@
.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 @@
}
}
+ 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;
@@ -1655,22 +1674,6 @@
// Otherwise if other places send wpc.getConfiguration() to client, the configuration may
// be ignored due to the seq is older.
resolvedConfig.seq = newParentConfig.seq;
-
- if (mConfigActivityRecord != null) {
- // Let the activity decide whether to apply the size override.
- return;
- }
- final DisplayContent displayContent = mAtm.mWindowManager != null
- ? mAtm.mWindowManager.getDefaultDisplayContentLocked()
- : null;
- applySizeOverrideIfNeeded(
- displayContent,
- mInfo,
- newParentConfig,
- resolvedConfig,
- false /* optOutEdgeToEdge */,
- false /* hasFixedRotationTransform */,
- false /* hasCompatDisplayInsets */);
}
void dispatchConfiguration(@NonNull Configuration config) {
@@ -2121,6 +2124,9 @@
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 9ebb89d..a36cff6 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 @@
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 @@
@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 0000000..4211764
--- /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 9dc70af..4ef9cf4 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 <stdio.h>
+#include <sys/epoll.h>
+#include <sys/ioctl.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 "MtpDescriptors.h"
-
-#include <stdio.h>
-#include <sys/types.h>
-#include <sys/stat.h>
-#include <fcntl.h>
-#include <sys/ioctl.h>
-#include <linux/usb/f_accessory.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 @@
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 @@
{"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 @@
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 5719810..4d6a90c 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 struct {
jclass clazz;
- jmethodID notifyConfigurationChanged;
jmethodID notifyInputDevicesChanged;
jmethodID notifySwitch;
jmethodID notifyInputChannelBroken;
@@ -314,7 +313,6 @@
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 @@
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 @@
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 6464081..314ff9d 100644
--- a/services/core/jni/onload.cpp
+++ b/services/core/jni/onload.cpp
@@ -37,7 +37,7 @@
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 @@
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 dc8cec9..6a0dd5a 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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 032d6b5..8cc7383 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 @@
/**
* @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 @@
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 @@
});
// Set admin.
- setActiveAdmin(profileOwner, /* refreshing= */ true, userId);
+ setActiveAdmin(profileOwner, /* refreshing= */ true, userId, null);
setProfileOwner(profileOwner, userId);
synchronized (getLockObject()) {
@@ -21883,7 +21890,7 @@
@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 eb893fc..0cd5b47 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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
}
} 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/inputmethodservice/InputMethodServiceTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java
index ce68b86..0787058 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java
@@ -60,6 +60,7 @@
import org.junit.runner.RunWith;
import java.io.IOException;
+import java.util.Objects;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -769,7 +770,8 @@
}
/**
- * Verifies that clicking on the IME switch button shows the Input Method Switcher Menu.
+ * Verifies that clicking on the IME switch button either shows the Input Method Switcher Menu,
+ * or switches the input method.
*/
@Test
public void testImeSwitchButtonClick() throws Exception {
@@ -794,13 +796,16 @@
assertThat(mInputMethodService.isInputViewShown()).isTrue();
final var imm = mContext.getSystemService(InputMethodManager.class);
+ final var initialInfo = imm.getCurrentInputMethodInfo();
final var imeSwitchButtonUiObject = getUiObjectById(INPUT_METHOD_NAV_IME_SWITCHER_ID);
imeSwitchButtonUiObject.click();
mInstrumentation.waitForIdleSync();
- assertWithMessage("Input Method Switcher Menu is shown")
- .that(isInputMethodPickerShown(imm))
+ final var newInfo = imm.getCurrentInputMethodInfo();
+
+ assertWithMessage("Input Method Switcher Menu is shown or input method was switched")
+ .that(isInputMethodPickerShown(imm) || !Objects.equals(initialInfo, newInfo))
.isTrue();
assertThat(mInputMethodService.isInputViewShown()).isTrue();
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 c2a069d..267ce26 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java
@@ -161,6 +161,7 @@
.spyStatic(InputMethodUtils.class)
.mockStatic(ServiceManager.class)
.spyStatic(AdditionalSubtypeMapRepository.class)
+ .spyStatic(AdditionalSubtypeUtils.class)
.startMocking();
mContext = spy(InstrumentationRegistry.getInstrumentation().getContext());
@@ -235,6 +236,7 @@
// 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 +269,10 @@
LocalServices.removeServiceForTest(InputMethodManagerInternal.class);
lifecycle.onStart();
+ // Emulate that the user initialization is done.
+ AdditionalSubtypeMapRepository.ensureInitializedAndGet(mCallingUserId);
+ mInputMethodManagerService.getUserData(mCallingUserId).mBackgroundLoadLatch.countDown();
+
// After this boot phase, services can broadcast Intents.
lifecycle.onBootPhase(SystemService.PHASE_ACTIVITY_MANAGER_READY);
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 624c897..c6aea5a 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 @@
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 @@
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 397d77c..26f6e91 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 @@
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 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 e982153..e04716e 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.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.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 @@
@Mock
private BrightnessModifier mMockModifier;
@Mock
+ private TestStatefulModifier mMockStatefulModifier;
+ @Mock
+ private TestDisplayListenerModifier mMockDisplayListenerModifier;
+ @Mock
private DisplayManagerInternal.DisplayPowerRequest mMockRequest;
@Mock
@@ -99,7 +104,8 @@
@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 @@
}
@Test
+ public void testOnDisplayChanged_DelegatesToDisplayListeners() {
+ mClamperController.onDisplayChanged(mMockDisplayDeviceData);
+
+ verify(mMockDisplayListenerModifier).onDisplayChanged(mMockDisplayDeviceData);
+ }
+
+ @Test
public void testOnDisplayChanged_doesNotRestartLightSensor() {
mClamperController.onDisplayChanged(mMockDisplayDeviceData);
@@ -189,6 +202,8 @@
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 @@
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 @@
@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 0000000..5fd848f
--- /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 0000000..e9ec811
--- /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 72883e2..5bd919f 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 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 @@
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 @@
}
@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 1dbd532..8656b99 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 @@
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/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java b/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java
index 758c84a..ef9580c 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 @@
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 2cb02bd..4645156 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 @@
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 037c3c0..b7100ea 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 @@
.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 27b552f..746645a 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 @@
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 8e34ee1..e45ab31 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 @@
@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 8a9538f..ebdde94 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 @@
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 54d1138..cbf7935 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 @@
}
@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 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 @@
}
@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 @@
}
@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 e181a51..06479c8 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 @@
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 @@
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 @@
};
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 @@
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 @@
// 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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
runWebViewBootPreparationOnMainSync();
checkPreparationPhasesForPackage(primaryPackage, 1 /* first preparation phase */);
- mTestSystemImpl.setMultiProcessSetting(null /* context */, settingValue);
+ mTestSystemImpl.setMultiProcessSetting(settingValue);
assertEquals(expectEnabled, mWebViewUpdateServiceImpl.isMultiProcessEnabled());
}
@@ -1492,7 +1491,7 @@
};
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 @@
// 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 @@
// 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 @@
// 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 @@
// 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/wmtests/OWNERS b/services/tests/wmtests/OWNERS
index 78b867f..51519fd 100644
--- a/services/tests/wmtests/OWNERS
+++ b/services/tests/wmtests/OWNERS
@@ -4,4 +4,7 @@
# Voice Interaction
per-file *Assist* = file:/core/java/android/service/voice/OWNERS
-natanieljr@google.com
\ No newline at end of file
+# Keyboard shortcuts
+per-file res/xml/bookmarks.xml = file:/services/core/java/com/android/server/input/OWNERS
+
+natanieljr@google.com
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 b4505fa..24fc7ee 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 @@
@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 @@
}
@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 a3252f8..6ad1044 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 @@
@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 @@
@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 @@
@Test
public void testShouldRefreshActivity_refreshDisabled() throws Exception {
- when(mLetterboxConfiguration.isCameraCompatRefreshEnabled())
+ when(mAppCompatConfiguration.isCameraCompatRefreshEnabled())
.thenReturn(false);
configureActivityAndDisplay();
mActivityRefresher.addEvaluator(mEvaluatorTrue);
@@ -146,7 +146,7 @@
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 2d94b34..d8c7fb3 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 @@
* 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 006b370..0b1bb0f 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 @@
* 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);
@@ -168,7 +168,7 @@
// TODO(b/350460645): Create Desktop Windowing Robot to reuse common functionalities.
void allowEnterDesktopMode(boolean isAllowed) {
doReturn(isAllowed).when(() ->
- DesktopModeLaunchParamsModifier.canEnterDesktopMode(any()));
+ DesktopModeHelper.canEnterDesktopMode(any()));
}
private AppCompatCameraPolicy getTopAppCompatCameraPolicy() {
diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationPersisterTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationPersisterTest.java
similarity index 89%
rename from services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationPersisterTest.java
rename to services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationPersisterTest.java
index 3fcec96..c952e2f 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 @@
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.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 @@
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 @@
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 @@
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 @@
@Test
public void test_whenStoreIsCreated_valuesAreDefaults() {
final int positionForHorizontalReachability =
- mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability(
+ mAppCompatConfigurationPersister.getLetterboxPositionForHorizontalReachability(
false);
final int defaultPositionForHorizontalReachability =
mContext.getResources().getInteger(
@@ -110,7 +110,7 @@
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 @@
@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 @@
@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 @@
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 @@
@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 @@
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 @@
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
similarity index 68%
rename from services/tests/wmtests/src/com/android/server/wm/AppCompatLetterboxConfigurationRobot.java
rename to services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationRobot.java
index e1da913..cb3cf6b 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 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
similarity index 74%
rename from services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationTest.java
rename to services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationTest.java
index 79e401c..6efd7de 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 @@
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.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 @@
@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 @@
/* 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 @@
/* 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 @@
@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 @@
@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 @@
@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 @@
.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 35c2ee0..634453f 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 @@
* 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 aa520e9..ad34a6b 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 @@
*/
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 de16e38..92f246b 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 @@
@NonNull
private final AppCompatActivityRobot mActivityRobot;
@NonNull
- private final AppCompatLetterboxConfigurationRobot mConfigurationRobot;
+ private final AppCompatConfigurationRobot mConfigurationRobot;
@NonNull
private final AppCompatComponentPropRobot mOptPropRobot;
@@ -42,7 +42,7 @@
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 @@
}
@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 11e6d90..eaa1641 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 @@
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 @@
@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 @@
@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 1c8dc05..12f5714f 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 @@
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 @@
@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 ad3fba3..b687042 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_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 @@
@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 @@
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+ @DisableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
public void testUsesDesiredBoundsIfEmptyLayoutAndActivityOptionsBounds() {
setupDesktopModeLaunchParamsModifier();
@@ -169,6 +184,209 @@
(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 @@
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
public void testNonEmptyLayoutBounds_CenterToDisplay() {
setupDesktopModeLaunchParamsModifier();
@@ -207,6 +426,7 @@
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
public void testNonEmptyLayoutBounds_LeftGravity() {
setupDesktopModeLaunchParamsModifier();
@@ -222,6 +442,7 @@
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
public void testNonEmptyLayoutBounds_TopGravity() {
setupDesktopModeLaunchParamsModifier();
@@ -237,6 +458,7 @@
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
public void testNonEmptyLayoutBounds_TopLeftGravity() {
setupDesktopModeLaunchParamsModifier();
@@ -252,6 +474,7 @@
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
public void testNonEmptyLayoutBounds_RightGravity() {
setupDesktopModeLaunchParamsModifier();
@@ -267,6 +490,7 @@
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
public void testNonEmptyLayoutBounds_BottomGravity() {
setupDesktopModeLaunchParamsModifier();
@@ -282,6 +506,7 @@
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
public void testNonEmptyLayoutBounds_RightBottomGravity() {
setupDesktopModeLaunchParamsModifier();
@@ -297,6 +522,7 @@
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
public void testNonEmptyLayoutFractionBounds() {
setupDesktopModeLaunchParamsModifier();
@@ -312,6 +538,7 @@
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
public void testNonEmptyLayoutBoundsRespectsGravityWithEmptySize_LeftGravity() {
setupDesktopModeLaunchParamsModifier();
@@ -327,6 +554,7 @@
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
public void testNonEmptyLayoutBoundsRespectsGravityWithEmptySize_TopGravity() {
setupDesktopModeLaunchParamsModifier();
@@ -342,6 +570,7 @@
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
public void testNonEmptyLayoutBoundsRespectsGravityWithEmptySize_TopLeftGravity() {
setupDesktopModeLaunchParamsModifier();
@@ -359,6 +588,7 @@
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
public void testNonEmptyLayoutBoundsRespectsGravityWithEmptySize_RightGravity() {
setupDesktopModeLaunchParamsModifier();
@@ -374,6 +604,7 @@
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
public void testNonEmptyLayoutBoundsRespectsGravityWithEmptySize_BottomGravity() {
setupDesktopModeLaunchParamsModifier();
@@ -389,6 +620,7 @@
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
public void testNonEmptyLayoutBoundsRespectsGravityWithEmptySize_BottomRightGravity() {
setupDesktopModeLaunchParamsModifier();
@@ -422,6 +654,38 @@
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);
@@ -430,8 +694,8 @@
private void setupDesktopModeLaunchParamsModifier(boolean isDesktopModeSupported,
boolean enforceDeviceRestrictions) {
doReturn(isDesktopModeSupported)
- .when(() -> DesktopModeLaunchParamsModifier.isDesktopModeSupported(any()));
+ .when(() -> DesktopModeHelper.isDesktopModeSupported(any()));
doReturn(enforceDeviceRestrictions)
- .when(DesktopModeLaunchParamsModifier::enforceDeviceRestrictions);
+ .when(DesktopModeHelper::shouldEnforceDeviceRestrictions);
}
}
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 65aaf60..ab0c8d4 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 @@
@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 @@
// test misc display overrides
assertEquals(ignoreOrientationRequests, testDisplayContent.mSetIgnoreOrientationRequest);
assertEquals(fixedOrientationLetterboxRatio,
- mWm.mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio(),
+ mWm.mAppCompatConfiguration.getFixedOrientationLetterboxAspectRatio(),
0 /* delta */);
}
@@ -2647,7 +2647,7 @@
// test misc display overrides
assertEquals(ignoreOrientationRequests, testDisplayContent.mSetIgnoreOrientationRequest);
assertEquals(fixedOrientationLetterboxRatio,
- mWm.mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio(),
+ mWm.mAppCompatConfiguration.getFixedOrientationLetterboxAspectRatio(),
0 /* delta */);
}
@@ -2870,7 +2870,7 @@
@Test
public void cameraCompatFreeformFlagEnabled_cameraCompatFreeformPolicyNotNull() {
doReturn(true).when(() ->
- DesktopModeLaunchParamsModifier.canEnterDesktopMode(any()));
+ DesktopModeHelper.canEnterDesktopMode(any()));
assertTrue(createNewDisplay().mAppCompatCameraPolicy.hasCameraCompatFreeformPolicy());
}
@@ -2879,7 +2879,7 @@
@Test
public void cameraCompatFreeformFlagNotEnabled_cameraCompatFreeformPolicyIsNull() {
doReturn(true).when(() ->
- DesktopModeLaunchParamsModifier.canEnterDesktopMode(any()));
+ DesktopModeHelper.canEnterDesktopMode(any()));
assertFalse(createNewDisplay().mAppCompatCameraPolicy.hasCameraCompatFreeformPolicy());
}
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 d7814ac..e9fcc40 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 @@
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 @@
@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 @@
@Test
public void testOnScreenRotationAnimationFinished_treatmentNotEnabled_doNotShowToast() {
- when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled())
+ when(mAppCompatConfiguration.isCameraCompatTreatmentEnabled())
.thenReturn(false);
mDisplayRotationCompatPolicy.onScreenRotationAnimationFinished();
@@ -239,7 +239,7 @@
@Test
public void testTreatmentNotEnabled_noForceRotationOrRefresh() throws Exception {
- when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled())
+ when(mAppCompatConfiguration.isCameraCompatTreatmentEnabled())
.thenReturn(false);
configureActivity(SCREEN_ORIENTATION_PORTRAIT);
@@ -253,7 +253,7 @@
@Test
public void testTreatmentDisabledViaDeviceConfig_noForceRotationOrRefresh() throws Exception {
- when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled())
+ when(mAppCompatConfiguration.isCameraCompatTreatmentEnabled())
.thenReturn(false);
configureActivity(SCREEN_ORIENTATION_PORTRAIT);
@@ -480,7 +480,7 @@
@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 @@
@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 b105703..5e8f347 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 @@
private DisplayRotationImmersiveAppCompatPolicy mPolicy;
- private LetterboxConfiguration mMockLetterboxConfiguration;
+ private AppCompatConfiguration mMockAppCompatConfiguration;
private ActivityRecord mMockActivityRecord;
private Task mMockTask;
private WindowState mMockWindowState;
@@ -77,15 +77,15 @@
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 @@
@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 c42367e..d318f00 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 @@
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 @@
/*centerX=*/ 1, /*centerY=*/ 1)
);
insets.setRoundedCorners(roundedCorners);
- mLetterboxConfiguration.setLetterboxActivityCornersRadius(-1);
+ mAppCompatConfiguration.setLetterboxActivityCornersRadius(-1);
assertEquals(expectedRadius, mController.getRoundedCornersRadius(mainWindow));
}
@@ -251,7 +251,7 @@
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 @@
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 @@
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 @@
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 @@
@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 @@
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 @@
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 @@
@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 @@
@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 @@
@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 @@
@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 @@
@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 @@
@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 @@
@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 @@
@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 @@
@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 @@
@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 0bf27d1..f93ffb8 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 @@
}
}
+ @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 ae88b1b..8981f71 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.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 @@
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 @@
.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 @@
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())
@@ -1658,7 +1658,7 @@
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
public void testCompatScaling_freeformUnresizeableApp_smallerThanParent_upScaled() {
doReturn(true).when(() ->
- DesktopModeLaunchParamsModifier.canEnterDesktopMode(any()));
+ DesktopModeHelper.canEnterDesktopMode(any()));
final int dw = 600;
final int dh = 800;
final DisplayContent display = new TestDisplayContent.Builder(mAtm, dw, dh)
@@ -1869,7 +1869,7 @@
// 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 @@
// 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 @@
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 @@
// 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 @@
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 @@
// 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 @@
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 @@
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 @@
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 @@
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 @@
.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 @@
.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 @@
.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 @@
.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 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 @@
// 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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
// 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 @@
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 @@
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 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 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 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 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 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 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 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 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 @@
@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 @@
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 @@
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 @@
// 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 @@
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 @@
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 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 @@
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 @@
// 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 @@
// 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 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 @@
// 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 @@
// 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 @@
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 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 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 @@
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 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 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 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 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 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 @@
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 @@
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 4220f31..d5d2847 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
@@ -202,7 +202,8 @@
.mockStatic(DisplayControl.class, mockStubOnly)
.mockStatic(LockGuard.class, mockStubOnly)
.mockStatic(Watchdog.class, mockStubOnly)
- .spyStatic(DesktopModeLaunchParamsModifier.class)
+ .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 76690ec..0bf850a 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 @@
.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 @@
.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 f07b402..cbf17c4 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 @@
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 593e983..22def51 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 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 @@
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 89abe2f..39640fb 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.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 @@
}
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/WindowProcessControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java
index 0cb22ad..e6648da 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java
@@ -18,7 +18,6 @@
import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
-import static android.content.pm.ActivityInfo.INSETS_DECOUPLED_CONFIGURATION_ENFORCED;
import static android.content.res.Configuration.GRAMMATICAL_GENDER_NOT_SPECIFIED;
import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
@@ -87,7 +86,6 @@
ApplicationInfo info = mock(ApplicationInfo.class);
info.packageName = "test.package.name";
- doReturn(true).when(info).isChangeEnabled(INSETS_DECOUPLED_CONFIGURATION_ENFORCED);
mWpc = new WindowProcessController(
mAtm, info, null, 0, -1, null, mMockListener);
mWpc.setThread(mock(IApplicationThread.class));
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 b512aa8..ea2abf7 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.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 @@
// 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 @@
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 0000000..e5f2f89
--- /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/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java
index bf46154..010a322 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsService.java
@@ -1064,10 +1064,8 @@
synchronized (mReportedEvents) {
LinkedList<Event> events = mReportedEvents.get(userId);
if (events == null) {
- // TODO (b/347644400): callers of this API should verify that the userId passed to
- // this method exists - there is currently a known case where USER_ALL is passed
- // here and it would be added to the queue, never to be flushed correctly. The logic
- // below should only remain as a last-resort catch-all fix.
+ // Callers of this API should verify that the userId passed to this method exists.
+ // The logic below should only remain as a last-resort catch-all fix.
final UserManagerInternal umi = LocalServices.getService(UserManagerInternal.class);
if (umi == null || (umi != null && !umi.exists(userId))) {
// The userId passed is a non-existent user so don't report the event.
@@ -3239,6 +3237,18 @@
}
@Override
+ public void reportEventForAllUsers(String packageName, int eventType) {
+ if (packageName == null) {
+ Slog.w(TAG, "Event reported without a package name, eventType:" + eventType);
+ return;
+ }
+
+ Event event = new Event(eventType, SystemClock.elapsedRealtime());
+ event.mPackage = packageName;
+ reportEventToAllUserId(event);
+ }
+
+ @Override
public void reportConfigurationChange(Configuration config, int userId) {
if (config == null) {
Slog.w(TAG, "Configuration event reported with a null config");
diff --git a/services/usb/java/com/android/server/usb/UsbDeviceManager.java b/services/usb/java/com/android/server/usb/UsbDeviceManager.java
index 175a09d..1404413 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.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 @@
"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 @@
private static UsbGadgetHal mUsbGadgetHal;
+ private final boolean mEnableUdcSysfsUsbStateUpdate;
+ private String mUdcName = "";
+
/**
* Counter for tracking UsbOperation operations.
*/
@@ -260,12 +268,9 @@
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 @@
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 @@
// 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 @@
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 7607c64..92af034 100644
--- a/telephony/OWNERS
+++ b/telephony/OWNERS
@@ -15,4 +15,4 @@
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/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt
index 5121f667..8811e00 100644
--- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt
+++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt
@@ -17,6 +17,7 @@
package com.android.server.wm.flicker.helpers
import android.graphics.Rect
+import android.platform.uiautomator_helpers.DeviceHelpers
import android.tools.device.apphelpers.IStandardAppHelper
import android.tools.helpers.SYSTEMUI_PACKAGE
import android.tools.traces.parsers.WindowManagerStateHelper
@@ -26,6 +27,7 @@
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiObject2
import androidx.test.uiautomator.Until
+import java.time.Duration
/**
* Wrapper class around App helper classes. This class adds functionality to the apps that the
@@ -41,16 +43,6 @@
RIGHT_BOTTOM
}
- private val TIMEOUT_MS = 3_000L
- private val CAPTION = "desktop_mode_caption"
- private val CAPTION_HANDLE = "caption_handle"
- private val MAXIMIZE_BUTTON = "maximize_window"
- private val MAXIMIZE_BUTTON_VIEW = "maximize_button_view"
- private val CLOSE_BUTTON = "close_window"
-
- private val caption: BySelector
- get() = By.res(SYSTEMUI_PACKAGE, CAPTION)
-
/** Wait for an app moved to desktop to finish its transition. */
private fun waitForAppToMoveToDesktop(wmHelper: WindowManagerStateHelper) {
wmHelper
@@ -123,7 +115,7 @@
)
error("expected a freeform window with caption but window is not in freeform mode")
val captions =
- device.wait(Until.findObjects(caption), TIMEOUT_MS)
+ device.wait(Until.findObjects(caption), TIMEOUT.toMillis())
?: error("Unable to find view $caption\n")
return captions.find {
@@ -200,6 +192,33 @@
device.drag(startX, startY, endX, endY, 100)
}
+ fun enterDesktopModeFromAppHandleMenu(
+ wmHelper: WindowManagerStateHelper,
+ device: UiDevice) {
+ val windowRect = wmHelper.getWindowRegion(innerHelper).bounds
+ val startX = windowRect.centerX()
+ // Click a little under the top to prevent opening the notification shade.
+ val startY = 10
+
+ // Click on the app handle coordinates.
+ device.click(startX, startY)
+ wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify()
+
+ val pill = getAppHandlePillForWindow()
+ val desktopModeButton =
+ pill
+ ?.children
+ ?.find { it.resourceName.endsWith(DESKTOP_MODE_BUTTON) }
+
+ desktopModeButton?.click()
+ wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify()
+ }
+
+ private fun getAppHandlePillForWindow(): UiObject2? {
+ val pillContainer: BySelector = By.res(SYSTEMUI_PACKAGE, PILL_CONTAINER)
+ return DeviceHelpers.waitForObj(pillContainer, TIMEOUT)
+ }
+
/** Wait for transition to full screen to finish. */
private fun waitForTransitionToFullscreen(wmHelper: WindowManagerStateHelper) {
wmHelper
@@ -208,4 +227,15 @@
.withAppTransitionIdle()
.waitForAndVerify()
}
+
+ private companion object {
+ val TIMEOUT = Duration.ofSeconds(3)
+ val CAPTION = "desktop_mode_caption"
+ val MAXIMIZE_BUTTON_VIEW = "maximize_button_view"
+ val CLOSE_BUTTON = "close_window"
+ val PILL_CONTAINER = "windowing_pill"
+ val DESKTOP_MODE_BUTTON = "desktop_button"
+ val caption: BySelector
+ get() = By.res(SYSTEMUI_PACKAGE, CAPTION)
+ }
}
diff --git a/tools/aapt2/cmd/Util.cpp b/tools/aapt2/cmd/Util.cpp
index e839fc1..7739171 100644
--- a/tools/aapt2/cmd/Util.cpp
+++ b/tools/aapt2/cmd/Util.cpp
@@ -137,22 +137,25 @@
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 35bc637..7818340 100644
--- a/tools/aapt2/cmd/Util_test.cpp
+++ b/tools/aapt2/cmd/Util_test.cpp
@@ -383,7 +383,7 @@
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_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 277a508..5ecf5cf 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 @@
/** 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 @@
/** 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 14e65e5..e3aa50a 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.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 @@
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 283ccbc..6470c06 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.w3c.dom.Element;
import java.nio.file.Paths;
+import java.util.List;
@RunWith(JUnit4.class)
public class AndroidSafetyLabelTest {
@@ -37,12 +38,16 @@
"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 @@
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 @@
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 b557fea..cc58a61 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 @@
@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 0000000..a4472b1
--- /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 7cd510f..fc8ff00 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.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 @@
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 @@
@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));
+ /** 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);
+ }
+
+ /** 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()
- .createFromHrElement(safetyLabelsEle, DEFAULT_VERSION));
+ () -> new SafetyLabelsFactory().createFromOdElement(ele, 2L));
}
- private void odToHrExpectException(String fileName)
- throws ParserConfigurationException, IOException, SAXException {
- var safetyLabelsEle =
- TestUtils.getElementFromResource(Paths.get(SAFETY_LABELS_OD_PATH, fileName));
- assertThrows(
- MalformedXmlException.class,
- () ->
- new SafetyLabelsFactory()
- .createFromOdElement(safetyLabelsEle, DEFAULT_VERSION));
- }
-
- 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 0000000..9d197a2
--- /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 9dcc652..04bcd78 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 @@
"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 @@
@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 0000000..ebb2e93
--- /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 6547fb9..b27d6dd 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 @@
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 @@
@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);
}
- private void testHrToOdTransparencyInfo(String fileName) throws Exception {
+ /** 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);
+ }
+
+ /** 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 0000000..7e984e3
--- /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 810078e..01fd718 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 0000000..1384a2f
--- /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 0000000..f96535b
--- /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 0000000..d7a4e1a
--- /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
similarity index 100%
rename from tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/valid-empty.xml
rename to 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
similarity index 100%
rename from tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/with-developer-info.xml
rename to tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/with-app-info-v2-and-dev-info-v1.xml
diff --git a/wifi/wifi.aconfig b/wifi/wifi.aconfig
index c1effe1..f7162f6 100644
--- a/wifi/wifi.aconfig
+++ b/wifi/wifi.aconfig
@@ -25,3 +25,13 @@
description: "Add API to migrate all values from Legacy Keystore to the new Wifi Blobstore database"
bug: "332560152"
}
+
+flag {
+ name: "hotspot_network_unknown_status_resets_connecting_state"
+ namespace: "wifi"
+ description: "Reset the connecting state flags when the hotspot network updates to unknown."
+ bug: "329670511"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}