Merge "Pass tracing instance index to start and stop callbacks" into main
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/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/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/com/android/internal/policy/FoldLockSettingsObserver.java b/core/java/com/android/internal/policy/FoldLockSettingsObserver.java
new file mode 100644
index 0000000..c6fba8a
--- /dev/null
+++ b/core/java/com/android/internal/policy/FoldLockSettingsObserver.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.policy;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.os.Handler;
+import android.os.UserHandle;
+import android.provider.Settings;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import java.util.Set;
+
+/**
+ * A ContentObserver that listens for changes in the "Continue using apps on fold" setting. This
+ * setting determines a device's behavior when the user folds the device.
+ * @hide
+ *
+ * Keep the setting values in this class in sync with the values in
+ * {@link com.android.server.utils.FoldSettingProvider} and
+ * {@link com.android.settings.display.FoldLockBehaviorSettings}
+ */
+public class FoldLockSettingsObserver extends ContentObserver {
+ /** The setting for "stay awake on fold". */
+ public static final String SETTING_VALUE_STAY_AWAKE_ON_FOLD = "stay_awake_on_fold_key";
+ /** The setting for "swipe up to continue". */
+ public static final String SETTING_VALUE_SELECTIVE_STAY_AWAKE = "selective_stay_awake_key";
+ /** The setting for "always sleep on fold". */
+ public static final String SETTING_VALUE_SLEEP_ON_FOLD = "sleep_on_fold_key";
+ public static final String SETTING_VALUE_DEFAULT = SETTING_VALUE_SELECTIVE_STAY_AWAKE;
+ private static final Set<String> SETTING_VALUES = Set.of(SETTING_VALUE_STAY_AWAKE_ON_FOLD,
+ SETTING_VALUE_SELECTIVE_STAY_AWAKE, SETTING_VALUE_SLEEP_ON_FOLD);
+
+ private final Context mContext;
+
+ /** The cached value of the setting. */
+ @VisibleForTesting
+ String mFoldLockSetting;
+
+ public FoldLockSettingsObserver(Handler handler, Context context) {
+ super(handler);
+ mContext = context;
+ }
+
+ /** Registers the observer and updates the cache for the first time. */
+ public void register() {
+ final ContentResolver r = mContext.getContentResolver();
+ r.registerContentObserver(
+ Settings.System.getUriFor(Settings.System.FOLD_LOCK_BEHAVIOR),
+ false, this, UserHandle.USER_ALL);
+ requestAndCacheFoldLockSetting();
+ }
+
+ /** Unregisters the observer. */
+ public void unregister() {
+ mContext.getContentResolver().unregisterContentObserver(this);
+ }
+
+ /** Runs when settings changes. */
+ @Override
+ public void onChange(boolean selfChange) {
+ requestAndCacheFoldLockSetting();
+ }
+
+ /**
+ * Requests and caches the current FOLD_LOCK_BEHAVIOR setting, which should be one of three
+ * values: SETTING_VALUE_STAY_AWAKE_ON_FOLD, SETTING_VALUE_SELECTIVE_STAY_AWAKE,
+ * SETTING_VALUE_SLEEP_ON_FOLD. If null (not set), returns the system default.
+ */
+ @VisibleForTesting
+ void requestAndCacheFoldLockSetting() {
+ String currentSetting = request();
+
+ if (currentSetting == null || !SETTING_VALUES.contains(currentSetting)) {
+ currentSetting = SETTING_VALUE_DEFAULT;
+ }
+
+ setCurrentFoldSetting(currentSetting);
+ }
+
+ /**
+ * Makes a binder call to request the current FOLD_LOCK_BEHAVIOR setting.
+ */
+ @VisibleForTesting
+ @Nullable
+ String request() {
+ return Settings.System.getStringForUser(mContext.getContentResolver(),
+ Settings.System.FOLD_LOCK_BEHAVIOR, UserHandle.USER_CURRENT);
+ }
+
+ /** Caches the fold-lock behavior received from Settings. */
+ @VisibleForTesting
+ void setCurrentFoldSetting(String newSetting) {
+ mFoldLockSetting = newSetting;
+ }
+
+ /** Used by external requesters: checks if the current setting is "stay awake on fold". */
+ public boolean isStayAwakeOnFold() {
+ return mFoldLockSetting.equals(SETTING_VALUE_STAY_AWAKE_ON_FOLD);
+ }
+
+ /** Used by external requesters: checks if the current setting is "swipe up to continue". */
+ public boolean isSelectiveStayAwake() {
+ return mFoldLockSetting.equals(SETTING_VALUE_SELECTIVE_STAY_AWAKE);
+ }
+
+ /** Used by external requesters: checks if the current setting is "sleep on fold". */
+ public boolean isSleepOnFold() {
+ return mFoldLockSetting.equals(SETTING_VALUE_SLEEP_ON_FOLD);
+ }
+}
diff --git a/core/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/symbols.xml b/core/res/res/values/symbols.xml
index fcafdae..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" />
diff --git a/core/tests/coretests/src/com/android/internal/policy/FoldLockSettingsObserverTest.java b/core/tests/coretests/src/com/android/internal/policy/FoldLockSettingsObserverTest.java
new file mode 100644
index 0000000..537dd69
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/policy/FoldLockSettingsObserverTest.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.policy;
+
+import static com.android.internal.policy.FoldLockSettingsObserver.SETTING_VALUE_DEFAULT;
+import static com.android.internal.policy.FoldLockSettingsObserver.SETTING_VALUE_SLEEP_ON_FOLD;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.os.Handler;
+import android.os.UserHandle;
+import android.provider.Settings;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Tests for {@link FoldLockSettingsObserver}.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class FoldLockSettingsObserverTest {
+ @Mock
+ private Context mContext;
+ @Mock
+ private Handler mHandler;
+ @Mock
+ private ContentResolver mContentResolver;
+
+ private FoldLockSettingsObserver mFoldLockSettingsObserver;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+
+ mFoldLockSettingsObserver =
+ spy(new FoldLockSettingsObserver(mHandler, mContext));
+
+ doReturn(mContentResolver).when(mContext).getContentResolver();
+ doReturn(SETTING_VALUE_DEFAULT).when(mFoldLockSettingsObserver).request();
+
+ mFoldLockSettingsObserver.register();
+ }
+
+ @Test
+ public void shouldRegister() {
+ doReturn(mContentResolver).when(mContext).getContentResolver();
+
+ mFoldLockSettingsObserver.register();
+
+ verify(mContentResolver).registerContentObserver(
+ Settings.System.getUriFor(Settings.System.FOLD_LOCK_BEHAVIOR),
+ false /*notifyForDescendants */,
+ mFoldLockSettingsObserver,
+ UserHandle.USER_ALL
+ );
+ }
+
+ @Test
+ public void shouldUnregister() {
+ mFoldLockSettingsObserver.unregister();
+
+ verify(mContentResolver).unregisterContentObserver(mFoldLockSettingsObserver);
+ }
+
+ @Test
+ public void shouldCacheNewValue() {
+ // Reset the mock's behavior and call count to zero.
+ reset(mFoldLockSettingsObserver);
+ doReturn(SETTING_VALUE_SLEEP_ON_FOLD).when(mFoldLockSettingsObserver).request();
+
+ // Setting is DEFAULT at first.
+ assertEquals(SETTING_VALUE_DEFAULT, mFoldLockSettingsObserver.mFoldLockSetting);
+
+ // Cache new setting.
+ mFoldLockSettingsObserver.requestAndCacheFoldLockSetting();
+
+ // Check that setter was called once and change went through properly.
+ verify(mFoldLockSettingsObserver).setCurrentFoldSetting(anyString());
+ assertTrue(mFoldLockSettingsObserver.isSleepOnFold());
+ }
+}
diff --git a/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/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index e713af6..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,
@@ -366,13 +369,14 @@
Optional<WindowDecorViewModel> windowDecorViewModel,
Optional<DesktopTasksController> desktopTasksController,
MultiInstanceHelper multiInstanceHelper,
- @ShellMainThread ShellExecutor mainExecutor) {
+ @ShellMainThread ShellExecutor mainExecutor,
+ @ShellMainThread Handler mainHandler) {
return new SplitScreenController(context, shellInit, shellCommandHandler, shellController,
shellTaskOrganizer, syncQueue, rootTaskDisplayAreaOrganizer, displayController,
displayImeController, displayInsetsController, dragAndDropController, transitions,
transactionPool, iconProvider, recentTasks, launchAdjacentController,
windowDecorViewModel, desktopTasksController, null /* stageCoordinator */,
- multiInstanceHelper, mainExecutor);
+ multiInstanceHelper, mainExecutor, mainHandler);
}
//
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
index 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 31f797a..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)
@@ -1079,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 =
@@ -1089,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()) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java
index 8df287d..06c57bd 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java
@@ -113,6 +113,9 @@
/** Called when device waking up finished. */
void onFinishedWakingUp();
+ /** Called when device starts going to sleep (screen off). */
+ void onStartedGoingToSleep();
+
/** Called when requested to go to fullscreen from the current active split app. */
void goToFullscreenFromSplit();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
index e659151..b857556 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
@@ -50,6 +50,7 @@
import android.content.pm.ShortcutInfo;
import android.graphics.Rect;
import android.os.Bundle;
+import android.os.Handler;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.ArrayMap;
@@ -180,6 +181,7 @@
private final LauncherApps mLauncherApps;
private final RootTaskDisplayAreaOrganizer mRootTDAOrganizer;
private final ShellExecutor mMainExecutor;
+ private final Handler mMainHandler;
private final SplitScreenImpl mImpl = new SplitScreenImpl();
private final DisplayController mDisplayController;
private final DisplayImeController mDisplayImeController;
@@ -227,7 +229,8 @@
Optional<DesktopTasksController> desktopTasksController,
@Nullable StageCoordinator stageCoordinator,
MultiInstanceHelper multiInstanceHelper,
- ShellExecutor mainExecutor) {
+ ShellExecutor mainExecutor,
+ Handler mainHandler) {
mShellCommandHandler = shellCommandHandler;
mShellController = shellController;
mTaskOrganizer = shellTaskOrganizer;
@@ -236,6 +239,7 @@
mLauncherApps = context.getSystemService(LauncherApps.class);
mRootTDAOrganizer = rootTDAOrganizer;
mMainExecutor = mainExecutor;
+ mMainHandler = mainHandler;
mDisplayController = displayController;
mDisplayImeController = displayImeController;
mDisplayInsetsController = displayInsetsController;
@@ -292,7 +296,7 @@
return new StageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue,
mTaskOrganizer, mDisplayController, mDisplayImeController,
mDisplayInsetsController, mTransitions, mTransactionPool, mIconProvider,
- mMainExecutor, mRecentTasksOptional, mLaunchAdjacentController,
+ mMainExecutor, mMainHandler, mRecentTasksOptional, mLaunchAdjacentController,
mWindowDecorViewModel);
}
@@ -448,13 +452,17 @@
@Override
public void onKeyguardVisibilityChanged(boolean visible, boolean occluded,
boolean animatingDismiss) {
- mStageCoordinator.onKeyguardVisibilityChanged(visible);
+ mStageCoordinator.onKeyguardStateChanged(visible, occluded);
}
public void onFinishedWakingUp() {
mStageCoordinator.onFinishedWakingUp();
}
+ public void onStartedGoingToSleep() {
+ mStageCoordinator.onStartedGoingToSleep();
+ }
+
public void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) {
mStageCoordinator.exitSplitScreenOnHide(exitSplitScreenOnHide);
}
@@ -1201,6 +1209,11 @@
}
@Override
+ public void onStartedGoingToSleep() {
+ mMainExecutor.execute(SplitScreenController.this::onStartedGoingToSleep);
+ }
+
+ @Override
public void goToFullscreenFromSplit() {
mMainExecutor.execute(SplitScreenController.this::goToFullscreenFromSplit);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index dbeee3b..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
@@ -92,6 +92,7 @@
import android.hardware.devicestate.DeviceStateManager;
import android.os.Bundle;
import android.os.Debug;
+import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.ServiceManager;
@@ -119,6 +120,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.InstanceId;
+import com.android.internal.policy.FoldLockSettingsObserver;
import com.android.internal.protolog.ProtoLog;
import com.android.internal.util.ArrayUtils;
import com.android.launcher3.icons.IconProvider;
@@ -191,7 +193,7 @@
private SplitLayout mSplitLayout;
private ValueAnimator mDividerFadeInAnimator;
private boolean mDividerVisible;
- private boolean mKeyguardShowing;
+ private boolean mKeyguardActive;
private boolean mShowDecorImmediately;
private final SyncTransactionQueue mSyncQueue;
private final ShellTaskOrganizer mTaskOrganizer;
@@ -205,6 +207,7 @@
private SplitScreenTransitions mSplitTransitions;
private final SplitscreenEventLogger mLogger;
private final ShellExecutor mMainExecutor;
+ private final Handler mMainHandler;
// Cache live tile tasks while entering recents, evict them from stages in finish transaction
// if user is opening another task(s).
private final ArrayList<Integer> mPausingTasks = new ArrayList<>();
@@ -233,7 +236,10 @@
private boolean mIsExiting;
private boolean mIsRootTranslucent;
@VisibleForTesting
- int mTopStageAfterFoldDismiss;
+ @StageType int mLastActiveStage;
+ private boolean mBreakOnNextWake;
+ /** Used to get the Settings value for "Continue using apps on fold". */
+ private FoldLockSettingsObserver mFoldLockSettingsObserver;
private DefaultMixedHandler mMixedHandler;
private final Toast mSplitUnsupportedToast;
@@ -313,9 +319,8 @@
ShellTaskOrganizer taskOrganizer, DisplayController displayController,
DisplayImeController displayImeController,
DisplayInsetsController displayInsetsController, Transitions transitions,
- TransactionPool transactionPool,
- IconProvider iconProvider, ShellExecutor mainExecutor,
- Optional<RecentTasksController> recentTasks,
+ TransactionPool transactionPool, IconProvider iconProvider, ShellExecutor mainExecutor,
+ Handler mainHandler, Optional<RecentTasksController> recentTasks,
LaunchAdjacentController launchAdjacentController,
Optional<WindowDecorViewModel> windowDecorViewModel) {
mContext = context;
@@ -324,6 +329,7 @@
mTaskOrganizer = taskOrganizer;
mLogger = new SplitscreenEventLogger();
mMainExecutor = mainExecutor;
+ mMainHandler = mainHandler;
mRecentTasks = recentTasks;
mLaunchAdjacentController = launchAdjacentController;
mWindowDecorViewModel = windowDecorViewModel;
@@ -366,6 +372,9 @@
// With shell transition, we should update recents tile each callback so set this to true by
// default.
mShouldUpdateRecents = ENABLE_SHELL_TRANSITIONS;
+ mFoldLockSettingsObserver =
+ new FoldLockSettingsObserver(mainHandler, context);
+ mFoldLockSettingsObserver.register();
}
@VisibleForTesting
@@ -373,9 +382,8 @@
ShellTaskOrganizer taskOrganizer, MainStage mainStage, SideStage sideStage,
DisplayController displayController, DisplayImeController displayImeController,
DisplayInsetsController displayInsetsController, SplitLayout splitLayout,
- Transitions transitions, TransactionPool transactionPool,
- ShellExecutor mainExecutor,
- Optional<RecentTasksController> recentTasks,
+ Transitions transitions, TransactionPool transactionPool, ShellExecutor mainExecutor,
+ Handler mainHandler, Optional<RecentTasksController> recentTasks,
LaunchAdjacentController launchAdjacentController,
Optional<WindowDecorViewModel> windowDecorViewModel) {
mContext = context;
@@ -393,6 +401,7 @@
this::onTransitionAnimationComplete, this);
mLogger = new SplitscreenEventLogger();
mMainExecutor = mainExecutor;
+ mMainHandler = mainHandler;
mRecentTasks = recentTasks;
mLaunchAdjacentController = launchAdjacentController;
mWindowDecorViewModel = windowDecorViewModel;
@@ -400,6 +409,9 @@
transitions.addHandler(this);
mSplitUnsupportedToast = Toast.makeText(mContext,
R.string.dock_non_resizeble_failed_to_dock_text, Toast.LENGTH_SHORT);
+ mFoldLockSettingsObserver =
+ new FoldLockSettingsObserver(context.getMainThreadHandler(), context);
+ mFoldLockSettingsObserver.register();
}
public void setMixedHandler(DefaultMixedHandler mixedHandler) {
@@ -1504,51 +1516,80 @@
}
}
- void onKeyguardVisibilityChanged(boolean showing) {
- mKeyguardShowing = showing;
+ /**
+ * Runs when keyguard state changes. The booleans here are a bit complicated, so for reference:
+ * @param active {@code true} if we are in a state where the keyguard *should* be shown
+ * -- still true when keyguard is "there" but is behind an app, or
+ * screen is off.
+ * @param occludingTaskRunning {@code true} when there is a running task that has
+ * FLAG_SHOW_WHEN_LOCKED -- also true when the task is
+ * just running on its own and keyguard is not active
+ * at all.
+ */
+ void onKeyguardStateChanged(boolean active, boolean occludingTaskRunning) {
+ mKeyguardActive = active;
if (!mMainStage.isActive()) {
return;
}
- ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onKeyguardVisibilityChanged: showing=%b", showing);
- setDividerVisibility(!mKeyguardShowing, null);
+ ProtoLog.d(WM_SHELL_SPLIT_SCREEN,
+ "onKeyguardVisibilityChanged: active=%b occludingTaskRunning=%b",
+ active, occludingTaskRunning);
+ setDividerVisibility(!mKeyguardActive, null);
+
+ if (active && occludingTaskRunning) {
+ dismissSplitKeepingLastActiveStage(EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP);
+ }
}
void onFinishedWakingUp() {
ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onFinishedWakingUp");
- if (!mMainStage.isActive()) {
+ if (mBreakOnNextWake) {
+ dismissSplitKeepingLastActiveStage(EXIT_REASON_DEVICE_FOLDED);
+ }
+ }
+
+ void onStartedGoingToSleep() {
+ recordLastActiveStage();
+ }
+
+ /**
+ * Records the user's last focused stage -- main stage or side stage. Used to determine which
+ * stage of a split pair should be kept, in cases where system focus has moved elsewhere.
+ */
+ void recordLastActiveStage() {
+ if (!isSplitActive() || !isSplitScreenVisible()) {
+ mLastActiveStage = STAGE_TYPE_UNDEFINED;
+ } else if (mMainStage.isFocused()) {
+ mLastActiveStage = STAGE_TYPE_MAIN;
+ } else if (mSideStage.isFocused()) {
+ mLastActiveStage = STAGE_TYPE_SIDE;
+ }
+ }
+
+ /**
+ * Dismisses split, keeping the app that the user focused last in split screen. If the user was
+ * not in split screen, {@link #mLastActiveStage} should be set to STAGE_TYPE_UNDEFINED, and we
+ * will do a no-op.
+ */
+ void dismissSplitKeepingLastActiveStage(@ExitReason int reason) {
+ if (!mMainStage.isActive() || mLastActiveStage == STAGE_TYPE_UNDEFINED) {
+ // no-op
return;
}
- // Check if there's only one stage visible while keyguard occluded.
- final boolean mainStageVisible = mMainStage.mRootTaskInfo.isVisible;
- final boolean oneStageVisible =
- mMainStage.mRootTaskInfo.isVisible != mSideStage.mRootTaskInfo.isVisible;
- if (oneStageVisible && !ENABLE_SHELL_TRANSITIONS) {
- // Dismiss split because there's show-when-locked activity showing on top of keyguard.
- // Also make sure the task contains show-when-locked activity remains on top after split
- // dismissed.
- final StageTaskListener toTop = mainStageVisible ? mMainStage : mSideStage;
- exitSplitScreen(toTop, EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP);
+ if (ENABLE_SHELL_TRANSITIONS) {
+ // Need manually clear here due to this transition might be aborted due to keyguard
+ // on top and lead to no visible change.
+ clearSplitPairedInRecents(reason);
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ prepareExitSplitScreen(mLastActiveStage, wct);
+ mSplitTransitions.startDismissTransition(wct, this, mLastActiveStage, reason);
+ setSplitsVisible(false);
+ } else {
+ exitSplitScreen(mLastActiveStage == STAGE_TYPE_MAIN ? mMainStage : mSideStage, reason);
}
- // Dismiss split if the flag record any side of stages.
- if (mTopStageAfterFoldDismiss != STAGE_TYPE_UNDEFINED) {
- if (ENABLE_SHELL_TRANSITIONS) {
- // Need manually clear here due to this transition might be aborted due to keyguard
- // on top and lead to no visible change.
- clearSplitPairedInRecents(EXIT_REASON_DEVICE_FOLDED);
- final WindowContainerTransaction wct = new WindowContainerTransaction();
- prepareExitSplitScreen(mTopStageAfterFoldDismiss, wct);
- mSplitTransitions.startDismissTransition(wct, this,
- mTopStageAfterFoldDismiss, EXIT_REASON_DEVICE_FOLDED);
- setSplitsVisible(false);
- } else {
- exitSplitScreen(
- mTopStageAfterFoldDismiss == STAGE_TYPE_MAIN ? mMainStage : mSideStage,
- EXIT_REASON_DEVICE_FOLDED);
- }
- mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED;
- }
+ mBreakOnNextWake = false;
}
void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) {
@@ -2223,11 +2264,11 @@
ProtoLog.d(WM_SHELL_SPLIT_SCREEN,
"setDividerVisibility: visible=%b keyguardShowing=%b dividerAnimating=%b caller=%s",
- visible, mKeyguardShowing, mIsDividerRemoteAnimating, Debug.getCaller());
+ visible, mKeyguardActive, mIsDividerRemoteAnimating, Debug.getCaller());
// Defer showing divider bar after keyguard dismissed, so it won't interfere with keyguard
// dismissing animation.
- if (visible && mKeyguardShowing) {
+ if (visible && mKeyguardActive) {
ProtoLog.d(WM_SHELL_SPLIT_SCREEN,
" Defer showing divider bar due to keyguard showing.");
return;
@@ -2597,21 +2638,24 @@
@VisibleForTesting
void onFoldedStateChanged(boolean folded) {
ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onFoldedStateChanged: folded=%b", folded);
- mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED;
- if (!folded) return;
- if (!isSplitActive() || !isSplitScreenVisible()) return;
-
- // To avoid split dismiss when user fold the device and unfold to use later, we only
- // record the flag here and try to dismiss on wakeUp callback to ensure split dismiss
- // when user interact on phone folded.
- if (mMainStage.isFocused()) {
- mTopStageAfterFoldDismiss = STAGE_TYPE_MAIN;
- } else if (mSideStage.isFocused()) {
- mTopStageAfterFoldDismiss = STAGE_TYPE_SIDE;
+ if (folded) {
+ recordLastActiveStage();
+ // If user folds and has the setting "Continue using apps on fold = NEVER", we assume
+ // they don't want to continue using split on the outer screen (i.e. we break split if
+ // they wake the device in its folded state).
+ mBreakOnNextWake = willSleepOnFold();
+ } else {
+ mBreakOnNextWake = false;
}
}
+ /** Returns true if the phone will sleep when it folds. */
+ @VisibleForTesting
+ boolean willSleepOnFold() {
+ return mFoldLockSettingsObserver != null && mFoldLockSettingsObserver.isSleepOnFold();
+ }
+
private Rect getSideStageBounds() {
return mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT
? mSplitLayout.getBounds1() : mSplitLayout.getBounds2();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitScreenController.java
index e330f3a..b65e978 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitScreenController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvSplitScreenController.java
@@ -33,7 +33,6 @@
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.common.SystemWindows;
import com.android.wm.shell.common.TransactionPool;
-import com.android.wm.shell.draganddrop.DragAndDropController;
import com.android.wm.shell.recents.RecentTasksController;
import com.android.wm.shell.splitscreen.SplitScreenController;
import com.android.wm.shell.splitscreen.StageCoordinator;
@@ -88,7 +87,8 @@
syncQueue, rootTDAOrganizer, displayController, displayImeController,
displayInsetsController, null, transitions, transactionPool,
iconProvider, recentTasks, launchAdjacentController, Optional.empty(),
- Optional.empty(), null /* stageCoordinator */, multiInstanceHelper, mainExecutor);
+ Optional.empty(), null /* stageCoordinator */, multiInstanceHelper, mainExecutor,
+ mainHandler);
mTaskOrganizer = shellTaskOrganizer;
mSyncQueue = syncQueue;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvStageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvStageCoordinator.java
index 7947691..81ca48f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvStageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvStageCoordinator.java
@@ -56,7 +56,7 @@
SystemWindows systemWindows) {
super(context, displayId, syncQueue, taskOrganizer, displayController, displayImeController,
displayInsetsController, transitions, transactionPool, iconProvider,
- mainExecutor, recentTasks, launchAdjacentController, Optional.empty());
+ mainExecutor, mainHandler, recentTasks, launchAdjacentController, Optional.empty());
mTvSplitMenuController = new TvSplitMenuController(context, this,
systemWindows, mainHandler);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/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 5397625..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;
@@ -43,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;
@@ -98,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;
@@ -132,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;
@@ -183,6 +184,7 @@
ShellExecutor shellExecutor,
Handler mainHandler,
Choreographer mainChoreographer,
+ @ShellBackgroundThread ShellExecutor bgExecutor,
ShellInit shellInit,
ShellCommandHandler shellCommandHandler,
IWindowManager windowManager,
@@ -201,6 +203,7 @@
shellExecutor,
mainHandler,
mainChoreographer,
+ bgExecutor,
shellInit,
shellCommandHandler,
windowManager,
@@ -225,6 +228,7 @@
ShellExecutor shellExecutor,
Handler mainHandler,
Choreographer mainChoreographer,
+ @ShellBackgroundThread ShellExecutor bgExecutor,
ShellInit shellInit,
ShellCommandHandler shellCommandHandler,
IWindowManager windowManager,
@@ -245,6 +249,7 @@
mMainExecutor = shellExecutor;
mMainHandler = mainHandler;
mMainChoreographer = mainChoreographer;
+ mBgExecutor = bgExecutor;
mActivityTaskManager = mContext.getSystemService(ActivityTaskManager.class);
mTaskOrganizer = taskOrganizer;
mShellController = shellController;
@@ -424,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 {
@@ -1095,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 b51b700..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
@@ -69,7 +69,9 @@
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
@@ -105,7 +107,7 @@
private val globalMenuPosition: Point = Point()
init {
- updateHandleMenuPillPositions()
+ updateHandleMenuPillPositions(captionX)
}
fun show() {
@@ -262,11 +264,11 @@
/**
* 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
@@ -287,7 +289,8 @@
handleMenuPosition.set(menuX.toFloat(), menuY.toFloat())
}
- private fun updateGlobalMenuPosition(taskBounds: Rect) {
+ private fun updateGlobalMenuPosition(taskBounds: Rect, captionX: Int) {
+ val nonFreeformX = captionX + (captionWidth / 2) - (menuWidth / 2)
when {
taskInfo.isFreeform -> {
globalMenuPosition.set(
@@ -297,7 +300,7 @@
}
taskInfo.isFullscreen -> {
globalMenuPosition.set(
- /* x = */ taskBounds.width() / 2 - (menuWidth / 2),
+ /* x = */ nonFreeformX,
/* y = */ marginMenuTop
)
}
@@ -311,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
)
}
@@ -332,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)
}
}
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/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 6002c21..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)!!
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
index 5b95b15..1c5d5e9 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
@@ -50,7 +50,7 @@
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.os.Bundle;
-import android.os.IBinder;
+import android.os.Handler;
import android.window.IWindowContainerToken;
import android.window.WindowContainerToken;
@@ -104,6 +104,7 @@
@Mock SyncTransactionQueue mSyncQueue;
@Mock RootTaskDisplayAreaOrganizer mRootTDAOrganizer;
@Mock ShellExecutor mMainExecutor;
+ @Mock Handler mMainHandler;
@Mock DisplayController mDisplayController;
@Mock DisplayImeController mDisplayImeController;
@Mock DisplayInsetsController mDisplayInsetsController;
@@ -134,7 +135,7 @@
mDisplayInsetsController, mDragAndDropController, mTransitions, mTransactionPool,
mIconProvider, Optional.of(mRecentTasks), mLaunchAdjacentController,
Optional.of(mWindowDecorViewModel), Optional.of(mDesktopTasksController),
- mStageCoordinator, mMultiInstanceHelper, mMainExecutor));
+ mStageCoordinator, mMultiInstanceHelper, mMainExecutor, mMainHandler));
}
@Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java
index a3009a5..29d3fb4 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java
@@ -22,6 +22,7 @@
import android.app.ActivityManager;
import android.content.Context;
import android.graphics.Rect;
+import android.os.Handler;
import android.view.SurfaceControl;
import android.view.SurfaceSession;
@@ -77,13 +78,13 @@
DisplayController displayController, DisplayImeController imeController,
DisplayInsetsController insetsController, SplitLayout splitLayout,
Transitions transitions, TransactionPool transactionPool,
- ShellExecutor mainExecutor,
+ ShellExecutor mainExecutor, Handler mainHandler,
Optional<RecentTasksController> recentTasks,
LaunchAdjacentController launchAdjacentController,
Optional<WindowDecorViewModel> windowDecorViewModel) {
super(context, displayId, syncQueue, taskOrganizer, mainStage,
sideStage, displayController, imeController, insetsController, splitLayout,
- transitions, transactionPool, mainExecutor, recentTasks,
+ transitions, transactionPool, mainExecutor, mainHandler, recentTasks,
launchAdjacentController, windowDecorViewModel);
// Prepare root task for testing.
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
index 34b2eeb..37ef788 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
@@ -49,6 +49,7 @@
import android.annotation.NonNull;
import android.app.ActivityManager;
+import android.os.Handler;
import android.os.IBinder;
import android.view.SurfaceControl;
import android.view.SurfaceSession;
@@ -107,6 +108,7 @@
@Mock private IconProvider mIconProvider;
@Mock private WindowDecorViewModel mWindowDecorViewModel;
@Mock private ShellExecutor mMainExecutor;
+ @Mock private Handler mMainHandler;
@Mock private LaunchAdjacentController mLaunchAdjacentController;
@Mock private DefaultMixedHandler mMixedHandler;
@Mock private SplitScreen.SplitInvocationListener mInvocationListener;
@@ -140,7 +142,7 @@
mStageCoordinator = new SplitTestUtils.TestStageCoordinator(mContext, DEFAULT_DISPLAY,
mSyncQueue, mTaskOrganizer, mMainStage, mSideStage, mDisplayController,
mDisplayImeController, mDisplayInsetsController, mSplitLayout, mTransitions,
- mTransactionPool, mMainExecutor, Optional.empty(),
+ mTransactionPool, mMainExecutor, mMainHandler, Optional.empty(),
mLaunchAdjacentController, Optional.empty());
mStageCoordinator.setMixedHandler(mMixedHandler);
mSplitScreenTransitions = mStageCoordinator.getSplitTransitions();
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
index e7b4c50..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
@@ -138,7 +138,8 @@
mStageCoordinator = spy(new StageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue,
mTaskOrganizer, mMainStage, mSideStage, mDisplayController, mDisplayImeController,
mDisplayInsetsController, mSplitLayout, mTransitions, mTransactionPool,
- mMainExecutor, Optional.empty(), mLaunchAdjacentController, Optional.empty()));
+ mMainExecutor, mMainHandler, Optional.empty(), mLaunchAdjacentController,
+ Optional.empty()));
mDividerLeash = new SurfaceControl.Builder(mSurfaceSession).setName("fakeDivider").build();
when(mSplitLayout.getBounds1()).thenReturn(mBounds1);
@@ -358,10 +359,11 @@
mMainStage.mRootTaskInfo = new TestRunningTaskInfoBuilder().setVisible(true).build();
when(mStageCoordinator.isSplitActive()).thenReturn(true);
when(mStageCoordinator.isSplitScreenVisible()).thenReturn(true);
+ when(mStageCoordinator.willSleepOnFold()).thenReturn(true);
mStageCoordinator.onFoldedStateChanged(true);
- assertEquals(mStageCoordinator.mTopStageAfterFoldDismiss, STAGE_TYPE_MAIN);
+ assertEquals(mStageCoordinator.mLastActiveStage, STAGE_TYPE_MAIN);
mStageCoordinator.onFinishedWakingUp();
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
index 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/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/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/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/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/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/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
index a120bdc..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()
}
@@ -1699,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/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/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/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/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/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/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/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 1ba274f..0e06117 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
@@ -37,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
@@ -55,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 }
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/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/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
index 51447cc..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
@@ -187,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(
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/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/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/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/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 99327d1..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
@@ -47,6 +47,7 @@
hideNotifsForOtherUsersCoordinator: HideNotifsForOtherUsersCoordinator,
keyguardCoordinator: KeyguardCoordinator,
unseenKeyguardCoordinator: OriginalUnseenKeyguardCoordinator,
+ lockScreenMinimalismCoordinator: LockScreenMinimalismCoordinator,
rankingCoordinator: RankingCoordinator,
colorizedFgsCoordinator: ColorizedFgsCoordinator,
deviceProvisionedCoordinator: DeviceProvisionedCoordinator,
@@ -87,7 +88,11 @@
mCoordinators.add(hideLocallyDismissedNotifsCoordinator)
mCoordinators.add(hideNotifsForOtherUsersCoordinator)
mCoordinators.add(keyguardCoordinator)
- mCoordinators.add(unseenKeyguardCoordinator)
+ if (NotificationMinimalismPrototype.isEnabled) {
+ mCoordinators.add(lockScreenMinimalismCoordinator)
+ } else {
+ mCoordinators.add(unseenKeyguardCoordinator)
+ }
mCoordinators.add(rankingCoordinator)
mCoordinators.add(colorizedFgsCoordinator)
mCoordinators.add(deviceProvisionedCoordinator)
@@ -120,12 +125,12 @@
}
// Manually add Ordered Sections
- if (NotificationMinimalismPrototype.V2.isEnabled) {
- mOrderedSections.add(unseenKeyguardCoordinator.topOngoingSectioner) // Top Ongoing
+ if (NotificationMinimalismPrototype.isEnabled) {
+ mOrderedSections.add(lockScreenMinimalismCoordinator.topOngoingSectioner) // Top Ongoing
}
mOrderedSections.add(headsUpCoordinator.sectioner) // HeadsUp
- if (NotificationMinimalismPrototype.V2.isEnabled) {
- mOrderedSections.add(unseenKeyguardCoordinator.topUnseenSectioner) // Top Unseen
+ if (NotificationMinimalismPrototype.isEnabled) {
+ mOrderedSections.add(lockScreenMinimalismCoordinator.topUnseenSectioner) // Top Unseen
}
mOrderedSections.add(colorizedFgsCoordinator.sectioner) // ForegroundService
if (PriorityPeopleSection.isEnabled) {
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
index 5dd1663..5b25b11 100644
--- 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
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * 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.
@@ -17,7 +17,6 @@
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
@@ -30,21 +29,14 @@
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.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.statusbar.policy.HeadsUpManager
import com.android.systemui.statusbar.policy.headsUpEvents
import com.android.systemui.util.asIndenting
@@ -73,9 +65,12 @@
import kotlinx.coroutines.yield
/**
- * Filters low priority and privacy-sensitive notifications from the lockscreen, and hides section
- * headers on the lockscreen. If enabled, it will also track and hide seen notifications on the
- * lockscreen.
+ * 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")
@@ -100,10 +95,7 @@
private var unseenFilterEnabled = false
override fun attach(pipeline: NotifPipeline) {
- if (NotificationMinimalismPrototype.V2.isEnabled) {
- pipeline.addPromoter(unseenNotifPromoter)
- pipeline.addOnBeforeTransformGroupsListener(::pickOutTopUnseenNotifs)
- }
+ NotificationMinimalismPrototype.assertInLegacyMode()
pipeline.addFinalizeFilter(unseenNotifFilter)
pipeline.addCollectionListener(collectionListener)
scope.launch { trackUnseenFilterSettingChanges() }
@@ -112,6 +104,7 @@
private suspend fun trackSeenNotifications() {
// Whether or not keyguard is visible (or occluded).
+ @Suppress("DEPRECATION")
val isKeyguardPresentFlow: Flow<Boolean> =
keyguardTransitionInteractor
.transitionValue(
@@ -265,11 +258,9 @@
}
private fun unseenFeatureEnabled(): Flow<Boolean> {
- if (
- NotificationMinimalismPrototype.V1.isEnabled ||
- NotificationMinimalismPrototype.V2.isEnabled
- ) {
- return flowOf(true)
+ 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
@@ -340,110 +331,18 @@
}
}
- 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
- 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
val unseenNotifFilter =
- object : NotifFilter("$TAG-unseen") {
+ object : NotifFilter(TAG) {
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
+ !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
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/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/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/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/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/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt
index 4c97854..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)
@@ -213,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
@@ -224,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) {
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 645a361..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,9 +37,10 @@
@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"
@@ -53,6 +54,9 @@
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
@@ -100,7 +104,7 @@
return getKey(headsUpEntryShowing)
}
- fun isEnabled() : Boolean {
+ fun isEnabled(): Boolean {
return NotificationThrottleHun.isEnabled && enableAtRuntime
}
@@ -110,7 +114,7 @@
log { "Runnable is NULL, stop update." }
return
}
- if (!NotificationThrottleHun.isEnabled) {
+ if (!isEnabled()) {
runnable.run()
return
}
@@ -170,7 +174,7 @@
log { "Runnable is NULL, stop delete." }
return
}
- if (!NotificationThrottleHun.isEnabled) {
+ if (!isEnabled()) {
runnable.run()
return
}
@@ -356,12 +360,14 @@
}
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)
+ 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() {
@@ -382,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/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/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
index ec9b5cf..a714351 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
@@ -263,6 +263,11 @@
public void onFinishedWakingUp() {
splitScreen.onFinishedWakingUp();
}
+
+ @Override
+ public void onStartedGoingToSleep() {
+ splitScreen.onStartedGoingToSleep();
+ }
});
mCommandQueue.addCallback(new CommandQueue.Callbacks() {
@Override
diff --git a/packages/SystemUI/tests/src/com/android/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/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/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/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 8a6b68f..a5c4bcd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -675,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,
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 52af907..64eadb7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt
@@ -36,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
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/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/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/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/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/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index ecbbd46..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;
@@ -597,7 +596,7 @@
}
break;
}
- case STYLUS_HANDWRITING_ENABLED: {
+ case Settings.Secure.STYLUS_HANDWRITING_ENABLED: {
InputMethodManager.invalidateLocalStylusHandwritingAvailabilityCaches();
InputMethodManager
.invalidateLocalConnectionlessStylusHandwritingAvailabilityCaches();
@@ -1099,14 +1098,9 @@
final InputMethodSettings newSettings = queryInputMethodServicesInternal(mContext,
userId, AdditionalSubtypeMapRepository.get(userId), DirectBootAwareness.AUTO);
InputMethodSettingsRepository.put(userId, newSettings);
- if (!mConcurrentMultiUserModeEnabled) {
- // We need to rebuild IMEs.
- postInputMethodSettingUpdatedLocked(false /* resetDefaultEnabledIme */, userId);
- updateInputMethodsFromSettingsLocked(true /* enabledChanged */, userId);
- } else {
- // TODO(b/352758479): Stop relying on initializeVisibleBackgroundUserLocked()
- initializeVisibleBackgroundUserLocked(userId);
- }
+ // We need to rebuild IMEs.
+ postInputMethodSettingUpdatedLocked(false /* resetDefaultEnabledIme */, userId);
+ updateInputMethodsFromSettingsLocked(true /* enabledChanged */, userId);
}
}
@@ -1617,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;
@@ -4787,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 {
@@ -6274,7 +6267,6 @@
boolean isCritical) {
IInputMethodInvoker method;
ClientState client;
- ClientState focusedWindowClient;
final Printer p = new PrintWriterPrinter(pw);
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodMenuControllerNew.java b/services/core/java/com/android/server/inputmethod/InputMethodMenuControllerNew.java
index cbb1807..045414b 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodMenuControllerNew.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodMenuControllerNew.java
@@ -17,17 +17,22 @@
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;
@@ -80,6 +85,7 @@
* @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.
@@ -120,7 +126,7 @@
.requireViewById(com.android.internal.R.id.button1);
languageSettingsButton.setVisibility(View.VISIBLE);
languageSettingsButton.setOnClickListener(v -> {
- v.getContext().startActivity(languageSettingsIntent);
+ v.getContext().startActivityAsUser(languageSettingsIntent, UserHandle.of(userId));
hide(displayId, userId);
});
}
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/wm/AppCompatAspectRatioOverrides.java b/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java
index d9f11b1..05d4c82 100644
--- a/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java
+++ b/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java
@@ -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) {
@@ -278,7 +286,7 @@
return getSplitScreenAspectRatio();
}
- private float getDefaultMinAspectRatio() {
+ float getDefaultMinAspectRatio() {
if (mActivityRecord.getDisplayArea() == null
|| !mAppCompatConfiguration
.getIsDisplayAspectRatioEnabledForFixedOrientationLetterbox()) {
diff --git a/services/core/java/com/android/server/wm/DesktopModeBoundsCalculator.java b/services/core/java/com/android/server/wm/DesktopModeBoundsCalculator.java
index f9f5058..3ecdff6 100644
--- a/services/core/java/com/android/server/wm/DesktopModeBoundsCalculator.java
+++ b/services/core/java/com/android/server/wm/DesktopModeBoundsCalculator.java
@@ -16,18 +16,33 @@
package com.android.server.wm;
+import static android.content.pm.ActivityInfo.isFixedOrientationLandscape;
+import static android.content.pm.ActivityInfo.isFixedOrientationPortrait;
+import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
+import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+
+import static com.android.server.wm.AppCompatConfiguration.DEFAULT_LETTERBOX_ASPECT_RATIO_FOR_MULTI_WINDOW;
+import static com.android.server.wm.AppCompatConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO;
+import static com.android.server.wm.AppCompatUtils.computeAspectRatio;
import static com.android.server.wm.LaunchParamsUtil.applyLayoutGravity;
import static com.android.server.wm.LaunchParamsUtil.calculateLayoutBounds;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityOptions;
+import android.app.AppCompatTaskInfo;
+import android.app.TaskInfo;
import android.content.pm.ActivityInfo;
+import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.SystemProperties;
import android.util.Size;
import android.view.Gravity;
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.window.flags.Flags;
+
import java.util.function.Consumer;
/**
@@ -38,6 +53,8 @@
public static final float DESKTOP_MODE_INITIAL_BOUNDS_SCALE = SystemProperties
.getInt("persist.wm.debug.desktop_mode_initial_bounds_scale", 75) / 100f;
+ public static final int DESKTOP_MODE_LANDSCAPE_APP_PADDING = SystemProperties
+ .getInt("persist.wm.debug.desktop_mode_landscape_app_padding", 25);
/**
* Updates launch bounds for an activity with respect to its activity options, window layout,
@@ -48,12 +65,8 @@
@NonNull Rect outBounds, @NonNull Consumer<String> logger) {
// Use stable frame instead of raw frame to avoid launching freeform windows on top of
// stable insets, which usually are system widgets such as sysbar & navbar.
- final TaskDisplayArea displayArea = task.getDisplayArea();
- final Rect screenBounds = displayArea.getBounds();
final Rect stableBounds = new Rect();
- displayArea.getStableRect(stableBounds);
- final int desiredWidth = (int) (stableBounds.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
- final int desiredHeight = (int) (stableBounds.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+ task.getDisplayArea().getStableRect(stableBounds);
if (options != null && options.getLaunchBounds() != null) {
outBounds.set(options.getLaunchBounds());
@@ -63,37 +76,282 @@
final int horizontalGravity = layout.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
if (layout.hasSpecifiedSize()) {
calculateLayoutBounds(stableBounds, layout, outBounds,
- new Size(desiredWidth, desiredHeight));
+ calculateIdealSize(stableBounds, DESKTOP_MODE_INITIAL_BOUNDS_SCALE));
applyLayoutGravity(verticalGravity, horizontalGravity, outBounds,
stableBounds);
logger.accept("layout specifies sizes, inheriting size and applying gravity");
} else if (verticalGravity > 0 || horizontalGravity > 0) {
- calculateAndCentreInitialBounds(outBounds, screenBounds);
+ outBounds.set(calculateInitialBounds(task, activity, stableBounds));
applyLayoutGravity(verticalGravity, horizontalGravity, outBounds,
stableBounds);
logger.accept("layout specifies gravity, applying desired bounds and gravity");
}
} else {
- calculateAndCentreInitialBounds(outBounds, screenBounds);
+ outBounds.set(calculateInitialBounds(task, activity, stableBounds));
logger.accept("layout not specified, applying desired bounds");
}
}
/**
- * Calculates the initial height and width of a task in desktop mode and centers it within the
- * window bounds.
+ * Calculates the initial bounds required for an application to fill a scale of the display
+ * bounds without any letterboxing. This is done by taking into account the applications
+ * fullscreen size, aspect ratio, orientation and resizability to calculate an area this is
+ * compatible with the applications previous configuration.
*/
- private static void calculateAndCentreInitialBounds(@NonNull Rect outBounds,
+ 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/319819547): Account for app constraints so apps do not become letterboxed
- // The desired dimensions that a fully resizable window should take when initially entering
- // desktop mode. Calculated as a percentage of the available display area as defined by the
- // DESKTOP_MODE_INITIAL_BOUNDS_SCALE.
- final int desiredWidth = (int) (screenBounds.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
- final int desiredHeight = (int) (screenBounds.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
- outBounds.right = desiredWidth;
- outBounds.bottom = desiredHeight;
- outBounds.offset(screenBounds.centerX() - outBounds.centerX(),
- screenBounds.centerY() - outBounds.centerY());
+ // TODO(b/325240051): Position apps with bottom heavy offset
+ final int heightOffset = (screenBounds.height() - desiredSize.getHeight()) / 2;
+ final int widthOffset = (screenBounds.width() - desiredSize.getWidth()) / 2;
+ final Rect resultBounds = new Rect(0, 0,
+ desiredSize.getWidth(), desiredSize.getHeight());
+ resultBounds.offset(screenBounds.left + widthOffset, screenBounds.top + heightOffset);
+ return resultBounds;
+ }
+
+ private static float getFixedOrientationLetterboxAspectRatio(@NonNull ActivityRecord activity,
+ @NonNull Task task) {
+ return activity.shouldCreateCompatDisplayInsets()
+ ? getDefaultMinAspectRatioForUnresizableApps(activity, task)
+ : activity.mAppCompatController.getAppCompatAspectRatioOverrides()
+ .getDefaultMinAspectRatio();
+ }
+
+ private static float getDefaultMinAspectRatioForUnresizableApps(
+ @NonNull ActivityRecord activity,
+ @NonNull Task task) {
+ final AppCompatAspectRatioOverrides appCompatAspectRatioOverrides =
+ activity.mAppCompatController.getAppCompatAspectRatioOverrides();
+ if (appCompatAspectRatioOverrides.isSplitScreenAspectRatioForUnresizableAppsEnabled()) {
+ // Default letterbox aspect ratio for unresizable apps.
+ return getSplitScreenAspectRatio(activity, task);
+ }
+
+ if (appCompatAspectRatioOverrides.getDefaultMinAspectRatioForUnresizableAppsFromConfig()
+ > MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO) {
+ return appCompatAspectRatioOverrides
+ .getDefaultMinAspectRatioForUnresizableAppsFromConfig();
+ }
+
+ return appCompatAspectRatioOverrides.getDefaultMinAspectRatio();
+ }
+
+ /**
+ * Calculates the aspect ratio of the available display area when an app enters split-screen on
+ * a given device, taking into account any dividers and insets.
+ */
+ private static float getSplitScreenAspectRatio(@NonNull ActivityRecord activity,
+ @NonNull Task task) {
+ final int dividerWindowWidth =
+ activity.mWmService.mContext.getResources().getDimensionPixelSize(
+ R.dimen.docked_stack_divider_thickness);
+ final int dividerInsets =
+ activity.mWmService.mContext.getResources().getDimensionPixelSize(
+ R.dimen.docked_stack_divider_insets);
+ final int dividerSize = dividerWindowWidth - dividerInsets * 2;
+ final Rect bounds = new Rect(0, 0,
+ task.mDisplayContent.getDisplayInfo().appWidth,
+ task.mDisplayContent.getDisplayInfo().appHeight);
+ if (bounds.width() >= bounds.height()) {
+ bounds.inset(/* dx */ dividerSize / 2, /* dy */ 0);
+ bounds.right = bounds.centerX();
+ } else {
+ bounds.inset(/* dx */ 0, /* dy */ dividerSize / 2);
+ bounds.bottom = bounds.centerY();
+ }
+ return computeAspectRatio(bounds);
}
}
diff --git a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
index aacd3c6..548addb 100644
--- a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
+++ b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
@@ -25,7 +25,6 @@
import android.app.ActivityOptions;
import android.content.Context;
import android.content.pm.ActivityInfo;
-import android.os.SystemProperties;
import android.util.Slog;
import com.android.server.wm.LaunchParamsController.LaunchParamsModifier;
@@ -38,19 +37,9 @@
TAG_WITH_CLASS_NAME ? "DesktopModeLaunchParamsModifier" : TAG_ATM;
private static final boolean DEBUG = false;
- public static final float DESKTOP_MODE_INITIAL_BOUNDS_SCALE =
- SystemProperties
- .getInt("persist.wm.debug.desktop_mode_initial_bounds_scale", 75) / 100f;
-
- /**
- * Flag to indicate whether to restrict desktop mode to supported devices.
- */
- private static final boolean ENFORCE_DEVICE_RESTRICTIONS = SystemProperties.getBoolean(
- "persist.wm.debug.desktop_mode_enforce_device_restrictions", true);
-
private StringBuilder mLogBuilder;
- private final Context mContext;
+ @NonNull private final Context mContext;
DesktopModeLaunchParamsModifier(@NonNull Context context) {
mContext = context;
diff --git a/services/core/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/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/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java
index 23a88a1..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);
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 a816aa9..d5d2847 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
@@ -203,6 +203,7 @@
.mockStatic(LockGuard.class, mockStubOnly)
.mockStatic(Watchdog.class, mockStubOnly)
.spyStatic(DesktopModeHelper.class)
+ .spyStatic(DesktopModeBoundsCalculator.class)
.strictness(Strictness.LENIENT)
.startMocking();
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();
}