Merge "[SB][RONs] Remove duplicate StatusBarRonChips class." into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 0ca9789..dd919ca 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -93,6 +93,7 @@
"com.android.media.flags.performance-aconfig-java",
"com.android.media.flags.projection-aconfig-java",
"com.android.net.thread.platform.flags-aconfig-java",
+ "com.android.ranging.flags.ranging-aconfig-java",
"com.android.server.contextualsearch.flags-java",
"com.android.server.flags.services-aconfig-java",
"com.android.text.flags-aconfig-java",
@@ -1549,6 +1550,13 @@
defaults: ["framework-minus-apex-aconfig-java-defaults"],
}
+// Ranging
+java_aconfig_library {
+ name: "com.android.ranging.flags.ranging-aconfig-java",
+ aconfig_declarations: "ranging_aconfig_flags",
+ defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
+
// System Server
aconfig_declarations {
name: "android.systemserver.flags-aconfig",
diff --git a/api/Android.bp b/api/Android.bp
index 533f9f6..3f2316f 100644
--- a/api/Android.bp
+++ b/api/Android.bp
@@ -102,6 +102,11 @@
"framework-crashrecovery",
],
default: [],
+ }) + select(release_flag("RELEASE_RANGING_STACK"), {
+ true: [
+ "framework-ranging",
+ ],
+ default: [],
}),
system_server_classpath: [
"service-art",
diff --git a/api/StubLibraries.bp b/api/StubLibraries.bp
index b3a674f..d1aa23c 100644
--- a/api/StubLibraries.bp
+++ b/api/StubLibraries.bp
@@ -466,6 +466,32 @@
}
java_library {
+ name: "android-non-updatable.stubs.system_server",
+ defaults: ["android-non-updatable_defaults"],
+ static_libs: [
+ "android-non-updatable.stubs.system_server.from-source",
+ ],
+ product_variables: {
+ build_from_text_stub: {
+ static_libs: [
+ "android-non-updatable.stubs.system_server.from-text",
+ ],
+ exclude_static_libs: [
+ "android-non-updatable.stubs.system_server.from-source",
+ ],
+ },
+ },
+}
+
+java_library {
+ name: "android-non-updatable.stubs.exportable.system_server",
+ defaults: ["android-non-updatable_defaults"],
+ static_libs: [
+ "android-non-updatable.stubs.exportable.system_server.from-source",
+ ],
+}
+
+java_library {
name: "android-non-updatable.stubs.from-source",
defaults: [
"android-non-updatable_defaults",
@@ -561,6 +587,30 @@
},
}
+java_library {
+ name: "android-non-updatable.stubs.system_server.from-source",
+ defaults: [
+ "android-non-updatable_defaults",
+ "android-non-updatable_from_source_defaults",
+ ],
+ srcs: [":services-non-updatable-stubs"],
+ libs: non_updatable_api_deps_on_modules,
+}
+
+java_library {
+ name: "android-non-updatable.stubs.exportable.system_server.from-source",
+ defaults: [
+ "android-non-updatable_defaults",
+ "android-non-updatable_from_source_defaults",
+ "android-non-updatable_exportable_from_source_defaults",
+ ],
+ srcs: [":services-non-updatable-stubs{.exportable}"],
+ libs: non_updatable_api_deps_on_modules,
+ dist: {
+ dir: "apistubs/android/system-server",
+ },
+}
+
java_defaults {
name: "android-non-updatable_from_text_defaults",
defaults: ["android-non-updatable-stubs-libs-defaults"],
@@ -662,6 +712,25 @@
libs: ["all-modules-system-stubs"],
}
+java_api_library {
+ name: "android-non-updatable.stubs.system_server.from-text",
+ api_surface: "system_server",
+ api_contributions: [
+ "api-stubs-docs-non-updatable.api.contribution",
+ "system-api-stubs-docs-non-updatable.api.contribution",
+ "module-lib-api-stubs-docs-non-updatable.api.contribution",
+ "services-non-updatable-stubs.api.contribution",
+ ],
+ defaults: [
+ "module-classpath-java-defaults",
+ "android-non-updatable_everything_from_text_defaults",
+ ],
+
+ // Use full Android API not just the non-updatable API as the latter is incomplete
+ // and can result in incorrect behavior.
+ previous_api: ":android.api.combined.system-server.latest",
+}
+
java_defaults {
name: "android_stubs_dists_default",
dist: {
@@ -813,9 +882,9 @@
defaults: [
"android.jar_defaults",
],
- srcs: [":services-non-updatable-stubs"],
installable: false,
static_libs: [
+ "android-non-updatable.stubs.system_server",
"android_module_lib_stubs_current",
],
visibility: ["//frameworks/base/services"],
@@ -827,9 +896,9 @@
"android.jar_defaults",
"android_stubs_dists_default",
],
- srcs: [":services-non-updatable-stubs{.exportable}"],
installable: false,
static_libs: [
+ "android-non-updatable.stubs.exportable.system_server",
"android_module_lib_stubs_current_exportable",
],
dist: {
diff --git a/cmds/uiautomator/library/Android.bp b/cmds/uiautomator/library/Android.bp
index 966bf13..5c5b220 100644
--- a/cmds/uiautomator/library/Android.bp
+++ b/cmds/uiautomator/library/Android.bp
@@ -28,9 +28,9 @@
"testrunner-src/**/*.java",
],
libs: [
- "android.test.runner",
+ "android.test.runner.stubs.system",
"junit",
- "android.test.base",
+ "android.test.base.stubs.system",
"unsupportedappusage",
],
installable: false,
@@ -56,9 +56,9 @@
":uiautomator-stubs",
],
libs: [
- "android.test.runner",
+ "android.test.runner.stubs",
"junit",
- "android.test.base",
+ "android.test.base.stubs",
],
sdk_version: "current",
installable: false,
diff --git a/core/api/current.txt b/core/api/current.txt
index 5d134c6..542543d 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -32613,6 +32613,13 @@
method public boolean isCharging();
field public static final String ACTION_CHARGING = "android.os.action.CHARGING";
field public static final String ACTION_DISCHARGING = "android.os.action.DISCHARGING";
+ field @FlaggedApi("android.os.battery_part_status_api") public static final int BATTERY_CAPACITY_LEVEL_CRITICAL = 1; // 0x1
+ field @FlaggedApi("android.os.battery_part_status_api") public static final int BATTERY_CAPACITY_LEVEL_FULL = 5; // 0x5
+ field @FlaggedApi("android.os.battery_part_status_api") public static final int BATTERY_CAPACITY_LEVEL_HIGH = 4; // 0x4
+ field @FlaggedApi("android.os.battery_part_status_api") public static final int BATTERY_CAPACITY_LEVEL_LOW = 2; // 0x2
+ field @FlaggedApi("android.os.battery_part_status_api") public static final int BATTERY_CAPACITY_LEVEL_NORMAL = 3; // 0x3
+ field @FlaggedApi("android.os.battery_part_status_api") public static final int BATTERY_CAPACITY_LEVEL_UNKNOWN = 0; // 0x0
+ field @FlaggedApi("android.os.battery_part_status_api") public static final int BATTERY_CAPACITY_LEVEL_UNSUPPORTED = -1; // 0xffffffff
field public static final int BATTERY_HEALTH_COLD = 7; // 0x7
field public static final int BATTERY_HEALTH_DEAD = 4; // 0x4
field public static final int BATTERY_HEALTH_GOOD = 2; // 0x2
@@ -32637,6 +32644,7 @@
field public static final int BATTERY_STATUS_NOT_CHARGING = 4; // 0x4
field public static final int BATTERY_STATUS_UNKNOWN = 1; // 0x1
field public static final String EXTRA_BATTERY_LOW = "battery_low";
+ field @FlaggedApi("android.os.battery_part_status_api") public static final String EXTRA_CAPACITY_LEVEL = "android.os.extra.CAPACITY_LEVEL";
field public static final String EXTRA_CHARGING_STATUS = "android.os.extra.CHARGING_STATUS";
field public static final String EXTRA_CYCLE_COUNT = "android.os.extra.CYCLE_COUNT";
field public static final String EXTRA_HEALTH = "health";
diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt
index df45862..8447a7f 100644
--- a/core/api/module-lib-current.txt
+++ b/core/api/module-lib-current.txt
@@ -387,6 +387,12 @@
field public static final int DEVICE_INITIAL_SDK_INT;
}
+ public class Handler {
+ method @FlaggedApi("android.os.mainline_vcn_platform_api") public final boolean hasMessagesOrCallbacks();
+ method @FlaggedApi("android.os.mainline_vcn_platform_api") public final void removeCallbacksAndEqualMessages(@Nullable Object);
+ method @FlaggedApi("android.os.mainline_vcn_platform_api") public final void removeEqualMessages(int, @Nullable Object);
+ }
+
public class IpcDataCache<Query, Result> {
ctor public IpcDataCache(int, @NonNull String, @NonNull String, @NonNull String, @NonNull android.os.IpcDataCache.QueryHandler<Query,Result>);
method public void disableForCurrentProcess();
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 7a8e829..b2a49e1 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -3835,6 +3835,7 @@
field @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public static final String ON_DEVICE_INTELLIGENCE_SERVICE = "on_device_intelligence";
field public static final String PERMISSION_CONTROLLER_SERVICE = "permission_controller";
field public static final String PERMISSION_SERVICE = "permission";
+ field @FlaggedApi("com.android.ranging.flags.ranging_stack_enabled") public static final String RANGING_SERVICE = "ranging";
field public static final String REBOOT_READINESS_SERVICE = "reboot_readiness";
field public static final String ROLLBACK_SERVICE = "rollback";
field public static final String SAFETY_CENTER_SERVICE = "safety_center";
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index caf6992..72a68f8 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -1797,18 +1797,20 @@
public class InputSettings {
method @FlaggedApi("com.android.hardware.input.keyboard_a11y_bounce_keys_flag") public static int getAccessibilityBounceKeysThreshold(@NonNull android.content.Context);
- method @FlaggedApi("com.android.hardware.input.keyboard_repeat_keys") public static int getAccessibilityRepeatKeysDelay(@NonNull android.content.Context);
- method @FlaggedApi("com.android.hardware.input.keyboard_repeat_keys") public static int getAccessibilityRepeatKeysTimeout(@NonNull android.content.Context);
method @FlaggedApi("com.android.hardware.input.keyboard_a11y_slow_keys_flag") public static int getAccessibilitySlowKeysThreshold(@NonNull android.content.Context);
+ method @FlaggedApi("com.android.input.flags.keyboard_repeat_keys") public static int getRepeatKeysDelay(@NonNull android.content.Context);
+ method @FlaggedApi("com.android.input.flags.keyboard_repeat_keys") public static int getRepeatKeysTimeout(@NonNull android.content.Context);
method @FlaggedApi("com.android.hardware.input.keyboard_a11y_mouse_keys") public static boolean isAccessibilityMouseKeysEnabled(@NonNull android.content.Context);
method @FlaggedApi("com.android.hardware.input.keyboard_a11y_sticky_keys_flag") public static boolean isAccessibilityStickyKeysEnabled(@NonNull android.content.Context);
+ method @FlaggedApi("com.android.input.flags.keyboard_repeat_keys") public static boolean isRepeatKeysEnabled(@NonNull android.content.Context);
method @FlaggedApi("com.android.hardware.input.keyboard_a11y_bounce_keys_flag") @RequiresPermission(android.Manifest.permission.WRITE_SETTINGS) public static void setAccessibilityBounceKeysThreshold(@NonNull android.content.Context, int);
method @FlaggedApi("com.android.hardware.input.keyboard_a11y_mouse_keys") @RequiresPermission(android.Manifest.permission.WRITE_SETTINGS) public static void setAccessibilityMouseKeysEnabled(@NonNull android.content.Context, boolean);
- method @FlaggedApi("com.android.hardware.input.keyboard_repeat_keys") @RequiresPermission(android.Manifest.permission.WRITE_SETTINGS) public static void setAccessibilityRepeatKeysDelay(@NonNull android.content.Context, int);
- method @FlaggedApi("com.android.hardware.input.keyboard_repeat_keys") @RequiresPermission(android.Manifest.permission.WRITE_SETTINGS) public static void setAccessibilityRepeatKeysTimeout(@NonNull android.content.Context, int);
method @FlaggedApi("com.android.hardware.input.keyboard_a11y_slow_keys_flag") @RequiresPermission(android.Manifest.permission.WRITE_SETTINGS) public static void setAccessibilitySlowKeysThreshold(@NonNull android.content.Context, int);
method @FlaggedApi("com.android.hardware.input.keyboard_a11y_sticky_keys_flag") @RequiresPermission(android.Manifest.permission.WRITE_SETTINGS) public static void setAccessibilityStickyKeysEnabled(@NonNull android.content.Context, boolean);
method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public static void setMaximumObscuringOpacityForTouch(@NonNull android.content.Context, @FloatRange(from=0, to=1) float);
+ method @FlaggedApi("com.android.input.flags.keyboard_repeat_keys") @RequiresPermission(android.Manifest.permission.WRITE_SETTINGS) public static void setRepeatKeysDelay(@NonNull android.content.Context, int);
+ method @FlaggedApi("com.android.input.flags.keyboard_repeat_keys") @RequiresPermission(android.Manifest.permission.WRITE_SETTINGS) public static void setRepeatKeysEnabled(@NonNull android.content.Context, boolean);
+ method @FlaggedApi("com.android.input.flags.keyboard_repeat_keys") @RequiresPermission(android.Manifest.permission.WRITE_SETTINGS) public static void setRepeatKeysTimeout(@NonNull android.content.Context, int);
field public static final int DEFAULT_POINTER_SPEED = 0; // 0x0
}
diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java
index dbf9afd..ed6b851 100644
--- a/core/java/android/app/ApplicationPackageManager.java
+++ b/core/java/android/app/ApplicationPackageManager.java
@@ -16,6 +16,7 @@
package android.app;
+import static android.app.PropertyInvalidatedCache.createSystemCacheKey;
import static android.app.admin.DevicePolicyResources.Drawables.Style.SOLID_COLORED;
import static android.app.admin.DevicePolicyResources.Drawables.Style.SOLID_NOT_COLORED;
import static android.app.admin.DevicePolicyResources.Drawables.WORK_PROFILE_ICON;
@@ -817,7 +818,7 @@
private final static PropertyInvalidatedCache<HasSystemFeatureQuery, Boolean>
mHasSystemFeatureCache =
new PropertyInvalidatedCache<HasSystemFeatureQuery, Boolean>(
- 256, "cache_key.has_system_feature") {
+ 256, createSystemCacheKey("has_system_feature")) {
@Override
public Boolean recompute(HasSystemFeatureQuery query) {
try {
@@ -1127,7 +1128,7 @@
}
private static final String CACHE_KEY_PACKAGES_FOR_UID_PROPERTY =
- "cache_key.get_packages_for_uid";
+ createSystemCacheKey("get_packages_for_uid");
private static final PropertyInvalidatedCache<Integer, GetPackagesForUidResult>
mGetPackagesForUidCache =
new PropertyInvalidatedCache<Integer, GetPackagesForUidResult>(
diff --git a/core/java/android/app/PropertyInvalidatedCache.java b/core/java/android/app/PropertyInvalidatedCache.java
index 0c786cb..0e761fc 100644
--- a/core/java/android/app/PropertyInvalidatedCache.java
+++ b/core/java/android/app/PropertyInvalidatedCache.java
@@ -19,6 +19,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.TestApi;
+import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
@@ -283,6 +284,12 @@
*/
/**
+ * The well-known key prefix.
+ * @hide
+ */
+ private static final String CACHE_KEY_PREFIX = "cache_key";
+
+ /**
* The module used for unit tests and cts tests. It is expected that no process in
* the system has permissions to write properties with this module.
* @hide
@@ -366,7 +373,44 @@
}
}
- return "cache_key." + module + "." + new String(suffix);
+ return CACHE_KEY_PREFIX + "." + module + "." + new String(suffix);
+ }
+
+ /**
+ * All legal keys start with one of the following strings.
+ */
+ private static final String[] sValidKeyPrefix = {
+ CACHE_KEY_PREFIX + "." + MODULE_SYSTEM + ".",
+ CACHE_KEY_PREFIX + "." + MODULE_BLUETOOTH + ".",
+ CACHE_KEY_PREFIX + "." + MODULE_TELEPHONY + ".",
+ CACHE_KEY_PREFIX + "." + MODULE_TEST + ".",
+ };
+
+ /**
+ * Verify that the property name conforms to the standard. Log a warning if this is not true.
+ * Note that this is done once in the cache constructor; it does not have to be very fast.
+ */
+ private void validateCacheKey(String name) {
+ if (Build.IS_USER) {
+ // Do not bother checking keys in user builds. The keys will have been tested in
+ // eng/userdebug builds already.
+ return;
+ }
+ for (int i = 0; i < sValidKeyPrefix.length; i++) {
+ if (name.startsWith(sValidKeyPrefix[i])) return;
+ }
+ Log.w(TAG, "invalid cache name: " + name);
+ }
+
+ /**
+ * Create a cache key for the system module. The parameter is the API name. This reduces
+ * some of the boilerplate in system caches. It is not needed in other modules because other
+ * modules must use the {@link IpcDataCache} interfaces.
+ * @hide
+ */
+ @NonNull
+ public static String createSystemCacheKey(@NonNull String api) {
+ return createPropertyName(MODULE_SYSTEM, api);
}
/**
@@ -561,6 +605,7 @@
public PropertyInvalidatedCache(int maxEntries, @NonNull String propertyName,
@NonNull String cacheName) {
mPropertyName = propertyName;
+ validateCacheKey(mPropertyName);
mCacheName = cacheName;
mMaxEntries = maxEntries;
mComputer = new DefaultComputer<>(this);
@@ -584,6 +629,7 @@
public PropertyInvalidatedCache(int maxEntries, @NonNull String module, @NonNull String api,
@NonNull String cacheName, @NonNull QueryHandler<Query, Result> computer) {
mPropertyName = createPropertyName(module, api);
+ validateCacheKey(mPropertyName);
mCacheName = cacheName;
mMaxEntries = maxEntries;
mComputer = computer;
diff --git a/core/java/android/app/appfunctions/AppFunctionService.java b/core/java/android/app/appfunctions/AppFunctionService.java
index c27141a..0d981ea 100644
--- a/core/java/android/app/appfunctions/AppFunctionService.java
+++ b/core/java/android/app/appfunctions/AppFunctionService.java
@@ -26,6 +26,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.Service;
+import android.content.Context;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
@@ -60,29 +61,53 @@
@NonNull
public static final String SERVICE_INTERFACE = "android.app.appfunctions.AppFunctionService";
- private final Binder mBinder =
- new IAppFunctionService.Stub() {
- @Override
- public void executeAppFunction(
- @NonNull ExecuteAppFunctionRequest request,
- @NonNull IExecuteAppFunctionCallback callback) {
- if (AppFunctionService.this.checkCallingPermission(BIND_APP_FUNCTION_SERVICE)
- == PERMISSION_DENIED) {
- throw new SecurityException("Can only be called by the system server.");
- }
- SafeOneTimeExecuteAppFunctionCallback safeCallback =
- new SafeOneTimeExecuteAppFunctionCallback(callback);
- try {
- AppFunctionService.this.onExecuteFunction(request, safeCallback::onResult);
- } catch (Exception ex) {
- // Apps should handle exceptions. But if they don't, report the error on
- // behalf of them.
- safeCallback.onResult(
- ExecuteAppFunctionResponse.newFailure(
- getResultCode(ex), ex.getMessage(), /* extras= */ null));
- }
+ /**
+ * Functional interface to represent the execution logic of an app function.
+ *
+ * @hide
+ */
+ @FunctionalInterface
+ public interface OnExecuteFunction {
+ /**
+ * Performs the semantic of executing the function specified by the provided request and
+ * return the response through the provided callback.
+ */
+ void perform(
+ @NonNull ExecuteAppFunctionRequest request,
+ @NonNull Consumer<ExecuteAppFunctionResponse> callback);
+ }
+
+ /** @hide */
+ @NonNull
+ public static Binder createBinder(
+ @NonNull Context context, @NonNull OnExecuteFunction onExecuteFunction) {
+ return new IAppFunctionService.Stub() {
+ @Override
+ public void executeAppFunction(
+ @NonNull ExecuteAppFunctionRequest request,
+ @NonNull IExecuteAppFunctionCallback callback) {
+ if (context.checkCallingPermission(BIND_APP_FUNCTION_SERVICE)
+ == PERMISSION_DENIED) {
+ throw new SecurityException("Can only be called by the system server.");
}
- };
+ SafeOneTimeExecuteAppFunctionCallback safeCallback =
+ new SafeOneTimeExecuteAppFunctionCallback(callback);
+ try {
+ onExecuteFunction.perform(request, safeCallback::onResult);
+ } catch (Exception ex) {
+ // Apps should handle exceptions. But if they don't, report the error on
+ // behalf of them.
+ safeCallback.onResult(
+ ExecuteAppFunctionResponse.newFailure(
+ getResultCode(ex), ex.getMessage(), /* extras= */ null));
+ }
+ }
+ };
+ }
+
+ private final Binder mBinder = createBinder(
+ AppFunctionService.this,
+ AppFunctionService.this::onExecuteFunction);
@NonNull
@Override
diff --git a/core/java/android/app/compat/ChangeIdStateCache.java b/core/java/android/app/compat/ChangeIdStateCache.java
index 7948cec..db663f8 100644
--- a/core/java/android/app/compat/ChangeIdStateCache.java
+++ b/core/java/android/app/compat/ChangeIdStateCache.java
@@ -16,6 +16,8 @@
package android.app.compat;
+import static android.app.PropertyInvalidatedCache.createSystemCacheKey;
+
import android.annotation.NonNull;
import android.app.PropertyInvalidatedCache;
import android.content.Context;
@@ -31,7 +33,7 @@
*/
public final class ChangeIdStateCache
extends PropertyInvalidatedCache<ChangeIdStateQuery, Boolean> {
- private static final String CACHE_KEY = "cache_key.is_compat_change_enabled";
+ private static final String CACHE_KEY = createSystemCacheKey("is_compat_change_enabled");
private static final int MAX_ENTRIES = 2048;
private static boolean sDisabled = false;
private volatile IPlatformCompat mPlatformCompat;
diff --git a/core/java/android/app/notification.aconfig b/core/java/android/app/notification.aconfig
index 9891e89..9b06adf 100644
--- a/core/java/android/app/notification.aconfig
+++ b/core/java/android/app/notification.aconfig
@@ -236,4 +236,12 @@
namespace: "systemui"
description: "Guards new android.app.richongoingnotification api"
bug: "337261753"
+}
+
+flag {
+ name: "ui_rich_ongoing"
+ is_exported: true
+ namespace: "systemui"
+ description: "Guards new android.app.richongoingnotification promotion and new uis"
+ bug: "337261753"
}
\ No newline at end of file
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 12c5d07..91f7a8b 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -4324,6 +4324,7 @@
SECURITY_STATE_SERVICE,
//@hide: ECM_ENHANCED_CONFIRMATION_SERVICE,
CONTACT_KEYS_SERVICE,
+ RANGING_SERVICE,
})
@Retention(RetentionPolicy.SOURCE)
@@ -6402,6 +6403,17 @@
/**
* Use with {@link #getSystemService(String)} to retrieve a
+ * {@link android.ranging.RangingManager}.
+ *
+ * @see #getSystemService(String)
+ * @hide
+ */
+ @FlaggedApi(com.android.ranging.flags.Flags.FLAG_RANGING_STACK_ENABLED)
+ @SystemApi
+ public static final String RANGING_SERVICE = "ranging";
+
+ /**
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.app.DreamManager} for controlling Dream states.
*
* @see #getSystemService(String)
diff --git a/core/java/android/content/pm/TEST_MAPPING b/core/java/android/content/pm/TEST_MAPPING
index ffadd1e..2cdae21 100644
--- a/core/java/android/content/pm/TEST_MAPPING
+++ b/core/java/android/content/pm/TEST_MAPPING
@@ -206,6 +206,17 @@
]
},
{
+ "name": "CtsPackageInstallerCUJDeviceAdminTestCases",
+ "options":[
+ {
+ "exclude-annotation":"androidx.test.filters.FlakyTest"
+ },
+ {
+ "exclude-annotation":"org.junit.Ignore"
+ }
+ ]
+ },
+ {
"name": "CtsPackageInstallerCUJInstallationTestCases",
"options":[
{
@@ -217,6 +228,17 @@
]
},
{
+ "name": "CtsPackageInstallerCUJMultiUsersTestCases",
+ "options":[
+ {
+ "exclude-annotation":"androidx.test.filters.FlakyTest"
+ },
+ {
+ "exclude-annotation":"org.junit.Ignore"
+ }
+ ]
+ },
+ {
"name": "CtsPackageInstallerCUJUninstallationTestCases",
"options":[
{
diff --git a/core/java/android/hardware/display/DisplayManagerGlobal.java b/core/java/android/hardware/display/DisplayManagerGlobal.java
index 9612a53..7185719 100644
--- a/core/java/android/hardware/display/DisplayManagerGlobal.java
+++ b/core/java/android/hardware/display/DisplayManagerGlobal.java
@@ -1445,7 +1445,7 @@
* system's display configuration.
*/
public static final String CACHE_KEY_DISPLAY_INFO_PROPERTY =
- "cache_key.display_info";
+ PropertyInvalidatedCache.createSystemCacheKey("display_info");
/**
* Invalidates the contents of the display info cache for all applications. Can only
diff --git a/core/java/android/hardware/input/InputSettings.java b/core/java/android/hardware/input/InputSettings.java
index 8592ded..177ee6f 100644
--- a/core/java/android/hardware/input/InputSettings.java
+++ b/core/java/android/hardware/input/InputSettings.java
@@ -20,15 +20,15 @@
import static com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_MOUSE_KEYS;
import static com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_SLOW_KEYS_FLAG;
import static com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_STICKY_KEYS_FLAG;
-import static com.android.hardware.input.Flags.FLAG_KEYBOARD_REPEAT_KEYS;
import static com.android.hardware.input.Flags.keyboardA11yBounceKeysFlag;
import static com.android.hardware.input.Flags.keyboardA11ySlowKeysFlag;
import static com.android.hardware.input.Flags.keyboardA11yStickyKeysFlag;
import static com.android.hardware.input.Flags.keyboardA11yMouseKeys;
-import static com.android.hardware.input.Flags.keyboardRepeatKeys;
import static com.android.hardware.input.Flags.touchpadTapDragging;
import static com.android.hardware.input.Flags.touchpadVisualizer;
import static com.android.input.flags.Flags.enableInputFilterRustImpl;
+import static com.android.input.flags.Flags.FLAG_KEYBOARD_REPEAT_KEYS;
+import static com.android.input.flags.Flags.keyboardRepeatKeys;
import android.Manifest;
import android.annotation.FlaggedApi;
@@ -800,7 +800,7 @@
*
* <p>
* ‘Repeat keys’ is a feature which allows users to generate key repeats when a particular
- * key on the physical keyboard is held down. This accessibility feature allows the user
+ * key on the physical keyboard is held down. This feature allows the user
* to configure the timeout before the key repeats begin as well as the delay
* between successive key repeats.
* </p>
@@ -812,7 +812,31 @@
}
/**
- * Get Accessibility repeat keys timeout duration in milliseconds.
+ * Whether "Repeat keys" feature is enabled.
+ * Repeat keys is ON by default.
+ * The repeat keys timeout and delay would have the default values in the default ON case.
+ *
+ * <p>
+ * 'Repeat keys’ is a feature which allows users to generate key repeats when a particular
+ * key on the physical keyboard is held down. This feature allows the user
+ * to configure the timeout before the key repeats begin as well as the delay
+ * between successive key repeats.
+ * </p>
+ *
+ * @hide
+ */
+ @TestApi
+ @FlaggedApi(FLAG_KEYBOARD_REPEAT_KEYS)
+ public static boolean isRepeatKeysEnabled(@NonNull Context context) {
+ if (!isRepeatKeysFeatureFlagEnabled()) {
+ return true;
+ }
+ return Settings.Secure.getIntForUser(context.getContentResolver(),
+ Settings.Secure.KEY_REPEAT_ENABLED, 1, UserHandle.USER_CURRENT) != 0;
+ }
+
+ /**
+ * Get repeat keys timeout duration in milliseconds.
* The default key repeat timeout is {@link ViewConfiguration#DEFAULT_KEY_REPEAT_TIMEOUT_MS}.
*
* @param context The application context
@@ -823,7 +847,7 @@
*
* <p>
* ‘Repeat keys’ is a feature which allows users to generate key repeats when a particular
- * key on the physical keyboard is held down. This accessibility feature allows the user
+ * key on the physical keyboard is held down. This feature allows the user
* to configure the timeout before the key repeats begin as well as the delay
* between successive key repeats.
* </p>
@@ -832,14 +856,17 @@
*/
@TestApi
@FlaggedApi(FLAG_KEYBOARD_REPEAT_KEYS)
- public static int getAccessibilityRepeatKeysTimeout(@NonNull Context context) {
+ public static int getRepeatKeysTimeout(@NonNull Context context) {
+ if (!isRepeatKeysFeatureFlagEnabled()) {
+ return ViewConfiguration.getKeyRepeatTimeout();
+ }
return Settings.Secure.getIntForUser(context.getContentResolver(),
Settings.Secure.KEY_REPEAT_TIMEOUT_MS, ViewConfiguration.getKeyRepeatTimeout(),
UserHandle.USER_CURRENT);
}
/**
- * Get Accessibility repeat keys delay rate in milliseconds.
+ * Get repeat keys delay rate in milliseconds.
* The default key repeat delay is {@link ViewConfiguration#DEFAULT_KEY_REPEAT_DELAY_MS}.
*
* @param context The application context
@@ -850,7 +877,7 @@
*
* <p>
* ‘Repeat keys’ is a feature which allows users to generate key repeats when a particular
- * key on the physical keyboard is held down. This accessibility feature allows the user
+ * key on the physical keyboard is held down. This feature allows the user
* to configure the timeout before the key repeats begin as well as the delay
* between successive key repeats.
* </p>
@@ -859,14 +886,41 @@
*/
@TestApi
@FlaggedApi(FLAG_KEYBOARD_REPEAT_KEYS)
- public static int getAccessibilityRepeatKeysDelay(@NonNull Context context) {
+ public static int getRepeatKeysDelay(@NonNull Context context) {
+ if (!isRepeatKeysFeatureFlagEnabled()) {
+ return ViewConfiguration.getKeyRepeatDelay();
+ }
return Settings.Secure.getIntForUser(context.getContentResolver(),
Settings.Secure.KEY_REPEAT_DELAY_MS, ViewConfiguration.getKeyRepeatDelay(),
UserHandle.USER_CURRENT);
}
/**
- * Set Accessibility repeat keys timeout duration in milliseconds.
+ * Set repeat keys feature enabled/disabled.
+ *
+ * <p>
+ * 'Repeat keys’ is a feature which allows users to generate key repeats when a particular
+ * key on the physical keyboard is held down. This feature allows the user
+ * to configure the timeout before the key repeats begin as well as the delay
+ * between successive key repeats.
+ * </p>
+ *
+ * @hide
+ */
+ @TestApi
+ @FlaggedApi(FLAG_KEYBOARD_REPEAT_KEYS)
+ @RequiresPermission(Manifest.permission.WRITE_SETTINGS)
+ public static void setRepeatKeysEnabled(@NonNull Context context,
+ boolean enabled) {
+ if (!isRepeatKeysFeatureFlagEnabled()) {
+ return;
+ }
+ Settings.Secure.putIntForUser(context.getContentResolver(),
+ Settings.Secure.KEY_REPEAT_ENABLED, enabled ? 1 : 0, UserHandle.USER_CURRENT);
+ }
+
+ /**
+ * Set repeat keys timeout duration in milliseconds.
*
* @param timeoutTimeMillis time duration for which a key should be pressed after which the
* pressed key will be repeated. The timeout must be between
@@ -875,7 +929,7 @@
*
* <p>
* ‘Repeat keys’ is a feature which allows users to generate key repeats when a particular
- * key on the physical keyboard is held down. This accessibility feature allows the user
+ * key on the physical keyboard is held down. This feature allows the user
* to configure the timeout before the key repeats begin as well as the delay
* between successive key repeats.
* </p>
@@ -885,8 +939,12 @@
@TestApi
@FlaggedApi(FLAG_KEYBOARD_REPEAT_KEYS)
@RequiresPermission(Manifest.permission.WRITE_SETTINGS)
- public static void setAccessibilityRepeatKeysTimeout(@NonNull Context context,
+ public static void setRepeatKeysTimeout(@NonNull Context context,
int timeoutTimeMillis) {
+ if (!isRepeatKeysFeatureFlagEnabled()
+ && !isRepeatKeysEnabled(context)) {
+ return;
+ }
if (timeoutTimeMillis < MIN_KEY_REPEAT_TIMEOUT_MILLIS
|| timeoutTimeMillis > MAX_KEY_REPEAT_TIMEOUT_MILLIS) {
throw new IllegalArgumentException(
@@ -900,7 +958,7 @@
}
/**
- * Set Accessibility repeat key delay duration in milliseconds.
+ * Set repeat key delay duration in milliseconds.
*
* @param delayTimeMillis Time duration between successive key repeats when a key is
* pressed down. The delay duration must be between
@@ -908,7 +966,7 @@
* {@link #MAX_KEY_REPEAT_DELAY_MILLIS}
* <p>
* ‘Repeat keys’ is a feature which allows users to generate key repeats when a particular
- * key on the physical keyboard is held down. This accessibility feature allows the user
+ * key on the physical keyboard is held down. This feature allows the user
* to configure the timeout before the key repeats begin as well as the delay
* between successive key repeats.
* </p>
@@ -918,8 +976,12 @@
@TestApi
@FlaggedApi(FLAG_KEYBOARD_REPEAT_KEYS)
@RequiresPermission(Manifest.permission.WRITE_SETTINGS)
- public static void setAccessibilityRepeatKeysDelay(@NonNull Context context,
+ public static void setRepeatKeysDelay(@NonNull Context context,
int delayTimeMillis) {
+ if (!isRepeatKeysFeatureFlagEnabled()
+ && !isRepeatKeysEnabled(context)) {
+ return;
+ }
if (delayTimeMillis < MIN_KEY_REPEAT_DELAY_MILLIS
|| delayTimeMillis > MAX_KEY_REPEAT_DELAY_MILLIS) {
throw new IllegalArgumentException(
diff --git a/core/java/android/net/vcn/VcnGatewayConnectionConfig.java b/core/java/android/net/vcn/VcnGatewayConnectionConfig.java
index 6f11d3a..af93c96 100644
--- a/core/java/android/net/vcn/VcnGatewayConnectionConfig.java
+++ b/core/java/android/net/vcn/VcnGatewayConnectionConfig.java
@@ -35,7 +35,6 @@
import android.util.ArraySet;
import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.util.ArrayUtils;
import com.android.internal.util.Preconditions;
import com.android.server.vcn.util.PersistableBundleUtils;
@@ -434,7 +433,14 @@
@NonNull
public int[] getExposedCapabilities() {
// Sorted set guarantees ordering
- return ArrayUtils.convertToIntArray(new ArrayList<>(mExposedCapabilities));
+ final int[] caps = new int[mExposedCapabilities.size()];
+
+ int i = 0;
+ for (int c : mExposedCapabilities) {
+ caps[i++] = c;
+ }
+
+ return caps;
}
/**
diff --git a/core/java/android/net/vcn/VcnUnderlyingNetworkSpecifier.java b/core/java/android/net/vcn/VcnUnderlyingNetworkSpecifier.java
index a975637..e1d1b3c6 100644
--- a/core/java/android/net/vcn/VcnUnderlyingNetworkSpecifier.java
+++ b/core/java/android/net/vcn/VcnUnderlyingNetworkSpecifier.java
@@ -24,7 +24,6 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.annotations.VisibleForTesting.Visibility;
-import com.android.internal.util.ArrayUtils;
import java.util.Arrays;
import java.util.Objects;
@@ -114,8 +113,13 @@
@Override
public boolean canBeSatisfiedBy(NetworkSpecifier other) {
if (other instanceof TelephonyNetworkSpecifier) {
- return ArrayUtils.contains(
- mSubIds, ((TelephonyNetworkSpecifier) other).getSubscriptionId());
+ final int targetSubId = ((TelephonyNetworkSpecifier) other).getSubscriptionId();
+ for (int subId : mSubIds) {
+ if (targetSubId == subId) {
+ return true;
+ }
+ }
+ return false;
}
// TODO(b/180140053): Allow matching against WifiNetworkAgentSpecifier
diff --git a/core/java/android/os/BatteryManager.java b/core/java/android/os/BatteryManager.java
index f3efd89..8b267bf 100644
--- a/core/java/android/os/BatteryManager.java
+++ b/core/java/android/os/BatteryManager.java
@@ -141,6 +141,7 @@
/**
* Extra for {@link android.content.Intent#ACTION_BATTERY_CHANGED}:
* integer containing the charge counter present in the battery.
+ * It shows the available battery power in µAh
* {@hide}
*/
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
@@ -166,6 +167,76 @@
public static final String EXTRA_CHARGING_STATUS = "android.os.extra.CHARGING_STATUS";
/**
+ * Extra for {@link android.content.Intent#ACTION_BATTERY_CHANGED}:
+ * Int value representing the battery's capacity level. These constants are key indicators of
+ * battery status and system capabilities, guiding power management decisions for both the
+ * system and apps:
+ * {@link #BATTERY_CAPACITY_LEVEL_UNSUPPORTED}: Feature not supported on this device.
+ * {@link #BATTERY_CAPACITY_LEVEL_UNKNOWN}: Battery status is unavailable or uninitialized.
+ * {@link #BATTERY_CAPACITY_LEVEL_CRITICAL}: Battery is critically low and the Android
+ * framework has been notified to schedule a shutdown by this value
+ * {@link #BATTERY_CAPACITY_LEVEL_LOW}: Android framework must limit background jobs to
+ * avoid impacting charging speed
+ * {@link #BATTERY_CAPACITY_LEVEL_NORMAL}: Battery level and charging rates are normal,
+ * battery temperature is within normal range and adapter power is enough to charge the
+ * battery at an acceptable rate. Android framework can run light background tasks without
+ * affecting charging performance severely.
+ * {@link #BATTERY_CAPACITY_LEVEL_HIGH}: Battery level is high, battery temperature is
+ * within normal range and adapter power is enough to charge the battery at an acceptable
+ * rate while running background loads. Android framework can run background tasks without
+ * affecting charging or battery performance.
+ * {@link #BATTERY_CAPACITY_LEVEL_FULL}: The battery is full, battery temperature is
+ * within normal range and adapter power is enough to sustain running background loads.
+ * Android framework can run background tasks without affecting the battery level or
+ * battery performance.
+ */
+
+ @FlaggedApi(FLAG_BATTERY_PART_STATUS_API)
+ public static final String EXTRA_CAPACITY_LEVEL = "android.os.extra.CAPACITY_LEVEL";
+
+ /**
+ * Battery capacity level is unsupported. @see EXTRA_CAPACITY_LEVEL
+ */
+ @FlaggedApi(FLAG_BATTERY_PART_STATUS_API)
+ public static final int BATTERY_CAPACITY_LEVEL_UNSUPPORTED = -1;
+
+ /**
+ * Battery capacity level is unknown. @see EXTRA_CAPACITY_LEVEL
+ */
+ @FlaggedApi(FLAG_BATTERY_PART_STATUS_API)
+ public static final int BATTERY_CAPACITY_LEVEL_UNKNOWN = 0;
+
+ /**
+ * Battery capacity level is critical. @see EXTRA_CAPACITY_LEVEL
+ */
+ @FlaggedApi(FLAG_BATTERY_PART_STATUS_API)
+ public static final int BATTERY_CAPACITY_LEVEL_CRITICAL = 1;
+
+ /**
+ * Battery capacity level is low. @see EXTRA_CAPACITY_LEVEL
+ */
+ @FlaggedApi(FLAG_BATTERY_PART_STATUS_API)
+ public static final int BATTERY_CAPACITY_LEVEL_LOW = 2;
+
+ /**
+ * Battery capacity level is normal. @see EXTRA_CAPACITY_LEVEL
+ */
+ @FlaggedApi(FLAG_BATTERY_PART_STATUS_API)
+ public static final int BATTERY_CAPACITY_LEVEL_NORMAL = 3;
+
+ /**
+ * Battery capacity level is high. @see EXTRA_CAPACITY_LEVEL
+ */
+ @FlaggedApi(FLAG_BATTERY_PART_STATUS_API)
+ public static final int BATTERY_CAPACITY_LEVEL_HIGH = 4;
+
+ /**
+ * Battery capacity level is full. @see EXTRA_CAPACITY_LEVEL
+ */
+ @FlaggedApi(FLAG_BATTERY_PART_STATUS_API)
+ public static final int BATTERY_CAPACITY_LEVEL_FULL = 5;
+
+ /**
* Extra for {@link android.content.Intent#ACTION_BATTERY_LEVEL_CHANGED}:
* Contains list of Bundles representing battery events
* @hide
diff --git a/core/java/android/os/Handler.java b/core/java/android/os/Handler.java
index 80f39bf..d0828c3 100644
--- a/core/java/android/os/Handler.java
+++ b/core/java/android/os/Handler.java
@@ -16,8 +16,10 @@
package android.os;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.SystemApi;
import android.compat.annotation.UnsupportedAppUsage;
import android.util.Log;
import android.util.Printer;
@@ -819,16 +821,25 @@
}
/**
+ * WARNING: This API is dangerous because if the implementation
+ * of equals() is broken, it would delete unrelated events. For example,
+ * if object.equals() always returns true, it'd remove all messages.
+ *
+ * For this reason, never expose this API to non-platform code. i.e.
+ * this shouldn't be exposed to SystemApi.PRIVILEGED_APPS.
+ *
* Remove any pending posts of messages with code 'what' and whose obj is
* 'object' that are in the message queue. If <var>object</var> is null,
* all messages will be removed.
- * <p>
- * Similar to {@link #removeMessages(int, Object)} but uses object equality
+ *
+ * <p>Similar to {@link #removeMessages(int, Object)} but uses object equality
* ({@link Object#equals(Object)}) instead of reference equality (==) in
* determining whether object is the message's obj'.
*
*@hide
*/
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+ @FlaggedApi(android.os.Flags.FLAG_MAINLINE_VCN_PLATFORM_API)
public final void removeEqualMessages(int what, @Nullable Object object) {
mQueue.removeEqualMessages(this, what, disallowNullArgumentIfShared(object));
}
@@ -843,12 +854,25 @@
}
/**
+ * WARNING: This API is dangerous because if the implementation
+ * of equals() is broken, it would delete unrelated events. For example,
+ * if object.equals() always returns true, it'd remove all messages.
+ *
+ * For this reason, never expose this API to non-platform code. i.e.
+ * this shouldn't be exposed to SystemApi.PRIVILEGED_APPS.
+ *
* Remove any pending posts of callbacks and sent messages whose
* <var>obj</var> is <var>token</var>. If <var>token</var> is null,
* all callbacks and messages will be removed.
*
+ * <p>Similar to {@link #removeCallbacksAndMessages(Object)} but uses object
+ * equality ({@link Object#equals(Object)}) instead of reference equality (==) in
+ * determining whether object is the message's obj'.
+ *
*@hide
*/
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+ @FlaggedApi(android.os.Flags.FLAG_MAINLINE_VCN_PLATFORM_API)
public final void removeCallbacksAndEqualMessages(@Nullable Object token) {
mQueue.removeCallbacksAndEqualMessages(this, disallowNullArgumentIfShared(token));
}
@@ -864,6 +888,8 @@
* Return whether there are any messages or callbacks currently scheduled on this handler.
* @hide
*/
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+ @FlaggedApi(android.os.Flags.FLAG_MAINLINE_VCN_PLATFORM_API)
public final boolean hasMessagesOrCallbacks() {
return mQueue.hasMessages(this);
}
diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java
index 026013c..e4c12b6 100644
--- a/core/java/android/os/PowerManager.java
+++ b/core/java/android/os/PowerManager.java
@@ -1144,9 +1144,10 @@
}
private static final String CACHE_KEY_IS_POWER_SAVE_MODE_PROPERTY =
- "cache_key.is_power_save_mode";
+ PropertyInvalidatedCache.createSystemCacheKey("is_power_save_mode");
- private static final String CACHE_KEY_IS_INTERACTIVE_PROPERTY = "cache_key.is_interactive";
+ private static final String CACHE_KEY_IS_INTERACTIVE_PROPERTY =
+ PropertyInvalidatedCache.createSystemCacheKey("is_interactive");
private static final int MAX_CACHE_ENTRIES = 1;
diff --git a/core/java/android/os/flags.aconfig b/core/java/android/os/flags.aconfig
index 738d129..f670601 100644
--- a/core/java/android/os/flags.aconfig
+++ b/core/java/android/os/flags.aconfig
@@ -216,3 +216,11 @@
bug: "346294653"
is_exported: true
}
+
+flag {
+ name: "mainline_vcn_platform_api"
+ namespace: "vcn"
+ description: "Expose platform APIs to mainline VCN"
+ is_exported: true
+ bug: "366598445"
+}
diff --git a/core/java/android/permission/PermissionManager.java b/core/java/android/permission/PermissionManager.java
index 7e51cb0..e98397d 100644
--- a/core/java/android/permission/PermissionManager.java
+++ b/core/java/android/permission/PermissionManager.java
@@ -1796,7 +1796,8 @@
}
/** @hide */
- public static final String CACHE_KEY_PACKAGE_INFO = "cache_key.package_info";
+ public static final String CACHE_KEY_PACKAGE_INFO =
+ PropertyInvalidatedCache.createSystemCacheKey("package_info");
/** @hide */
private static final PropertyInvalidatedCache<PermissionQuery, Integer> sPermissionCache =
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 0ada993..b8a8be1 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -9158,15 +9158,27 @@
public static final String MULTI_PRESS_TIMEOUT = "multi_press_timeout";
/**
+ * Whether to enable key repeats for Physical Keyboard.
+ *
+ * If set to false, continuous key presses on
+ * physical keyboard will not cause the pressed key to repeated.
+ * @hide
+ */
+ @Readable
+ public static final String KEY_REPEAT_ENABLED = "key_repeat_enabled";
+
+ /**
* The duration before a key repeat begins in milliseconds.
* @hide
*/
+ @Readable
public static final String KEY_REPEAT_TIMEOUT_MS = "key_repeat_timeout";
/**
* The duration between successive key repeats in milliseconds.
* @hide
*/
+ @Readable
public static final String KEY_REPEAT_DELAY_MS = "key_repeat_delay";
/**
diff --git a/core/java/android/text/ClientFlags.java b/core/java/android/text/ClientFlags.java
index c2ad508..ca88764 100644
--- a/core/java/android/text/ClientFlags.java
+++ b/core/java/android/text/ClientFlags.java
@@ -16,21 +16,14 @@
package android.text;
-import com.android.text.flags.Flags;
-
/**
* An aconfig feature flags that can be accessible from application process without
* ContentProvider IPCs.
*
* When you add new flags, you have to add flag string to {@link TextFlags#TEXT_ACONFIGS_FLAGS}.
*
+ * TODO(nona): Remove this class.
* @hide
*/
public class ClientFlags {
- /**
- * @see Flags#fixMisalignedContextMenu()
- */
- public static boolean fixMisalignedContextMenu() {
- return TextFlags.isFeatureEnabled(Flags.FLAG_FIX_MISALIGNED_CONTEXT_MENU);
- }
}
diff --git a/core/java/android/text/TextFlags.java b/core/java/android/text/TextFlags.java
index 076721f..f69a333 100644
--- a/core/java/android/text/TextFlags.java
+++ b/core/java/android/text/TextFlags.java
@@ -19,11 +19,10 @@
import android.annotation.NonNull;
import android.app.AppGlobals;
-import com.android.text.flags.Flags;
-
/**
* Flags in the "text" namespace.
*
+ * TODO(nona): Remove this class.
* @hide
*/
public final class TextFlags {
@@ -55,7 +54,6 @@
* List of text flags to be transferred to the application process.
*/
public static final String[] TEXT_ACONFIGS_FLAGS = {
- Flags.FLAG_FIX_MISALIGNED_CONTEXT_MENU,
};
/**
@@ -64,7 +62,6 @@
* The order must be the same to the TEXT_ACONFIG_FLAGS.
*/
public static final boolean[] TEXT_ACONFIG_DEFAULT_VALUE = {
- Flags.fixMisalignedContextMenu(),
};
/**
diff --git a/core/java/android/text/flags/flags.aconfig b/core/java/android/text/flags/flags.aconfig
index 3846972..c83285a 100644
--- a/core/java/android/text/flags/flags.aconfig
+++ b/core/java/android/text/flags/flags.aconfig
@@ -84,16 +84,6 @@
}
flag {
- name: "fix_misaligned_context_menu"
- namespace: "text"
- description: "Fix the context menu misalignment and incosistent icon size."
- bug: "332542108"
- metadata {
- purpose: PURPOSE_BUGFIX
- }
-}
-
-flag {
name: "missing_getter_apis"
namespace: "text"
description: "Fix the lint warning about missing getters."
diff --git a/core/java/android/window/TransitionFilter.java b/core/java/android/window/TransitionFilter.java
index 3cfde87..8bb4c52 100644
--- a/core/java/android/window/TransitionFilter.java
+++ b/core/java/android/window/TransitionFilter.java
@@ -187,6 +187,7 @@
/** If non-null, requires the change to specifically have or not-have a custom animation. */
public Boolean mCustomAnimation = null;
+ public IBinder mTaskFragmentToken = null;
public Requirement() {
}
@@ -204,12 +205,19 @@
// 0: null, 1: false, 2: true
final int customAnimRaw = in.readInt();
mCustomAnimation = customAnimRaw == 0 ? null : Boolean.valueOf(customAnimRaw == 2);
+ mTaskFragmentToken = in.readStrongBinder();
}
/** Go through changes and find if at-least one change matches this filter */
boolean matches(@NonNull TransitionInfo info) {
for (int i = info.getChanges().size() - 1; i >= 0; --i) {
final TransitionInfo.Change change = info.getChanges().get(i);
+
+ if (mTaskFragmentToken != null
+ && !mTaskFragmentToken.equals(change.getTaskFragmentToken())) {
+ continue;
+ }
+
if (mMustBeIndependent && !TransitionInfo.isIndependent(change, info)) {
// Only look at independent animating windows.
continue;
@@ -313,6 +321,7 @@
dest.writeStrongBinder(mLaunchCookie);
int customAnimRaw = mCustomAnimation == null ? 0 : (mCustomAnimation ? 2 : 1);
dest.writeInt(customAnimRaw);
+ dest.writeStrongBinder(mTaskFragmentToken);
}
@NonNull
@@ -357,6 +366,9 @@
if (mCustomAnimation != null) {
out.append(" customAnim=").append(mCustomAnimation.booleanValue());
}
+ if (mTaskFragmentToken != null) {
+ out.append(" taskFragmentToken=").append(mTaskFragmentToken);
+ }
out.append("}");
return out.toString();
}
diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java
index ec79f94..14505f5 100644
--- a/core/java/android/window/TransitionInfo.java
+++ b/core/java/android/window/TransitionInfo.java
@@ -49,6 +49,7 @@
import android.graphics.Point;
import android.graphics.Rect;
import android.hardware.HardwareBuffer;
+import android.os.IBinder;
import android.os.Parcel;
import android.os.Parcelable;
import android.view.Surface;
@@ -681,6 +682,7 @@
private float mSnapshotLuma;
private ComponentName mActivityComponent = null;
private AnimationOptions mAnimationOptions = null;
+ private IBinder mTaskFragmentToken = null;
public Change(@Nullable WindowContainerToken container, @NonNull SurfaceControl leash) {
mContainer = container;
@@ -712,6 +714,7 @@
mSnapshotLuma = in.readFloat();
mActivityComponent = in.readTypedObject(ComponentName.CREATOR);
mAnimationOptions = in.readTypedObject(AnimationOptions.CREATOR);
+ mTaskFragmentToken = in.readStrongBinder();
}
private Change localRemoteCopy() {
@@ -737,6 +740,7 @@
out.mSnapshotLuma = mSnapshotLuma;
out.mActivityComponent = mActivityComponent;
out.mAnimationOptions = mAnimationOptions;
+ out.mTaskFragmentToken = mTaskFragmentToken;
return out;
}
@@ -854,6 +858,14 @@
mAnimationOptions = options;
}
+ /**
+ * Sets the client-defined TaskFragment token. Only set this if the window is a
+ * client-organized TaskFragment.
+ */
+ public void setTaskFragmentToken(@Nullable IBinder token) {
+ mTaskFragmentToken = token;
+ }
+
/** @return the container that is changing. May be null if non-remotable (eg. activity) */
@Nullable
public WindowContainerToken getContainer() {
@@ -1009,6 +1021,15 @@
return mAnimationOptions;
}
+ /**
+ * Returns the client-defined TaskFragment token. {@code null} if this window is not a
+ * client-organized TaskFragment.
+ */
+ @Nullable
+ public IBinder getTaskFragmentToken() {
+ return mTaskFragmentToken;
+ }
+
/** @hide */
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
@@ -1035,6 +1056,7 @@
dest.writeFloat(mSnapshotLuma);
dest.writeTypedObject(mActivityComponent, flags);
dest.writeTypedObject(mAnimationOptions, flags);
+ dest.writeStrongBinder(mTaskFragmentToken);
}
@NonNull
@@ -1110,6 +1132,9 @@
if (mAnimationOptions != null) {
sb.append(" opt=").append(mAnimationOptions);
}
+ if (mTaskFragmentToken != null) {
+ sb.append(" taskFragmentToken=").append(mTaskFragmentToken);
+ }
sb.append('}');
return sb.toString();
}
diff --git a/core/java/android/window/flags/windowing_sdk.aconfig b/core/java/android/window/flags/windowing_sdk.aconfig
index 13648de..9ae3fc1 100644
--- a/core/java/android/window/flags/windowing_sdk.aconfig
+++ b/core/java/android/window/flags/windowing_sdk.aconfig
@@ -122,3 +122,10 @@
description: "Requires apps to opt-in to overlay pass through touches and provide APIs to opt-in"
bug: "358129114"
}
+
+flag {
+ namespace: "windowing_sdk"
+ name: "wlinfo_oncreate"
+ description: "Makes WindowLayoutInfo accessible without racing in the Activity#onCreate()"
+ bug: "337820752"
+}
diff --git a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
index e440dc9..fbc058c 100644
--- a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
+++ b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
@@ -210,6 +210,8 @@
DataSourceParams
.PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_DROP)
.build();
+ // NOTE: Registering that datasource is an async operation, so there may be no data traced
+ // for some messages logged right after the construction of this class.
mDataSource.register(params);
this.mViewerConfigInputStreamProvider = viewerConfigInputStreamProvider;
this.mViewerConfigReader = viewerConfigReader;
@@ -223,17 +225,17 @@
"ServiceManager returned a null ProtoLog Configuration Service");
try {
- var args = new ProtoLogConfigurationService.RegisterClientArgs();
+ var args = new ProtoLogConfigurationServiceImpl.RegisterClientArgs();
if (viewerConfigFilePath != null) {
args.setViewerConfigFile(viewerConfigFilePath);
}
final var groupArgs = Stream.of(groups)
- .map(group -> new ProtoLogConfigurationService.RegisterClientArgs
+ .map(group -> new ProtoLogConfigurationServiceImpl.RegisterClientArgs
.GroupConfig(group.name(), group.isLogToLogcat()))
- .toArray(
- ProtoLogConfigurationService.RegisterClientArgs.GroupConfig[]::new);
+ .toArray(ProtoLogConfigurationServiceImpl
+ .RegisterClientArgs.GroupConfig[]::new);
args.setGroups(groupArgs);
mProtoLogConfigurationService.registerClient(this, args);
diff --git a/core/java/com/android/internal/protolog/ProtoLogConfigurationService.java b/core/java/com/android/internal/protolog/ProtoLogConfigurationService.java
index 7031d69..d65aaae 100644
--- a/core/java/com/android/internal/protolog/ProtoLogConfigurationService.java
+++ b/core/java/com/android/internal/protolog/ProtoLogConfigurationService.java
@@ -16,434 +16,32 @@
package com.android.internal.protolog;
-import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.GROUPS;
-import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.Group.ID;
-import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.Group.NAME;
-import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.Group.TAG;
-import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MESSAGES;
-import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.GROUP_ID;
-import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.LEVEL;
-import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.LOCATION;
-import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.MESSAGE;
-import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.MESSAGE_ID;
-
import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.annotation.SystemService;
-import android.content.Context;
-import android.os.RemoteException;
-import android.os.ResultReceiver;
-import android.os.ShellCallback;
-import android.tracing.perfetto.DataSourceParams;
-import android.tracing.perfetto.InitArguments;
-import android.tracing.perfetto.Producer;
-import android.util.Log;
-import android.util.proto.ProtoInputStream;
-import android.util.proto.ProtoOutputStream;
-import com.android.internal.annotations.VisibleForTesting;
-
-import java.io.FileDescriptor;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-import java.util.TreeMap;
-
-/**
- * The ProtoLog service is responsible for orchestrating centralized actions of the protolog tracing
- * system. Currently this service has the following roles:
- * - Handle shell commands to toggle logging ProtoLog messages for specified groups to logcat.
- * - Handle viewer config dumping (the mapping from message hash to message string) for all protolog
- * clients. This is for two reasons: firstly, because client processes might be frozen so might
- * not response to the request to dump their viewer config when the trace is stopped; secondly,
- * multiple processes might be running the same code with the same viewer config, this centralized
- * service ensures we don't dump the same viewer config multiple times across processes.
- * <p>
- * {@link com.android.internal.protolog.IProtoLogClient ProtoLog clients} register themselves to
- * this service on initialization.
- * <p>
- * This service is intended to run on the system server, such that it never gets frozen.
- */
-@SystemService(Context.PROTOLOG_CONFIGURATION_SERVICE)
-public final class ProtoLogConfigurationService extends IProtoLogConfigurationService.Stub {
- private static final String LOG_TAG = "ProtoLogConfigurationService";
-
- private final ProtoLogDataSource mDataSource;
-
- /**
- * Keeps track of how many of each viewer config file is currently registered.
- * Use to keep track of which viewer config files are actively being used in tracing and might
- * need to be dumped on flush.
- */
- private final Map<String, Integer> mConfigFileCounts = new HashMap<>();
- /**
- * Keeps track of the viewer config file of each client if available.
- */
- private final Map<IProtoLogClient, String> mClientConfigFiles = new HashMap<>();
-
- /**
- * Keeps track of all the protolog groups that have been registered by clients and are still
- * being actively traced.
- */
- private final Set<String> mRegisteredGroups = new HashSet<>();
- /**
- * Keeps track of all the clients that are actively tracing a given protolog group.
- */
- private final Map<String, Set<IProtoLogClient>> mGroupToClients = new HashMap<>();
-
- /**
- * Keeps track of whether or not a given group should be logged to logcat.
- * True when logging to logcat, false otherwise.
- */
- private final Map<String, Boolean> mLogGroupToLogcatStatus = new TreeMap<>();
-
- /**
- * Keeps track of all the tracing instance ids that are actively running for ProtoLog.
- */
- private final Set<Integer> mRunningInstances = new HashSet<>();
-
- private final ViewerConfigFileTracer mViewerConfigFileTracer;
-
- public ProtoLogConfigurationService() {
- this(ProtoLogDataSource::new, ProtoLogConfigurationService::dumpTransitionTraceConfig);
- }
-
- @VisibleForTesting
- public ProtoLogConfigurationService(@NonNull ProtoLogDataSourceBuilder dataSourceBuilder) {
- this(dataSourceBuilder, ProtoLogConfigurationService::dumpTransitionTraceConfig);
- }
-
- @VisibleForTesting
- public ProtoLogConfigurationService(@NonNull ViewerConfigFileTracer tracer) {
- this(ProtoLogDataSource::new, tracer);
- }
-
- @VisibleForTesting
- public ProtoLogConfigurationService(
- @NonNull ProtoLogDataSourceBuilder dataSourceBuilder,
- @NonNull ViewerConfigFileTracer tracer) {
- mDataSource = dataSourceBuilder.build(
- this::onTracingInstanceStart,
- this::onTracingInstanceFlush,
- this::onTracingInstanceStop
- );
-
- // Initialize the Perfetto producer and register the Perfetto ProtoLog datasource to be
- // receive the lifecycle callbacks of the datasource and write the viewer configs if and
- // when required to the datasource.
- Producer.init(InitArguments.DEFAULTS);
- final var params = new DataSourceParams.Builder()
- .setBufferExhaustedPolicy(DataSourceParams.PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_DROP)
- .build();
- mDataSource.register(params);
-
- mViewerConfigFileTracer = tracer;
- }
-
- public static class RegisterClientArgs extends IRegisterClientArgs.Stub {
- /**
- * The viewer config file to be registered for this client ProtoLog process.
- */
- @Nullable
- private String mViewerConfigFile = null;
- /**
- * The list of all groups that this client protolog process supports and might trace.
- */
- @NonNull
- private String[] mGroups = new String[0];
- /**
- * The default logcat status of the ProtoLog client. True is logging to logcat, false
- * otherwise. The indices should match the indices in {@link mGroups}.
- */
- @NonNull
- private boolean[] mLogcatStatus = new boolean[0];
-
- public record GroupConfig(@NonNull String group, boolean logToLogcat) {}
-
- /**
- * Specify groups to register with this client that will be used for protologging in this
- * process.
- * @param groups to register with this client.
- * @return self
- */
- public RegisterClientArgs setGroups(GroupConfig... groups) {
- mGroups = new String[groups.length];
- mLogcatStatus = new boolean[groups.length];
-
- for (int i = 0; i < groups.length; i++) {
- mGroups[i] = groups[i].group;
- mLogcatStatus[i] = groups[i].logToLogcat;
- }
-
- return this;
- }
-
- /**
- * Set the viewer config file that the logs in this process are using.
- * @param viewerConfigFile The file path of the viewer config.
- * @return self
- */
- public RegisterClientArgs setViewerConfigFile(@NonNull String viewerConfigFile) {
- mViewerConfigFile = viewerConfigFile;
-
- return this;
- }
-
- @Override
- @NonNull
- public String[] getGroups() {
- return mGroups;
- }
-
- @Override
- @NonNull
- public boolean[] getGroupsDefaultLogcatStatus() {
- return mLogcatStatus;
- }
-
- @Nullable
- @Override
- public String getViewerConfigFile() {
- return mViewerConfigFile;
- }
- }
-
- @FunctionalInterface
- public interface ViewerConfigFileTracer {
- /**
- * Write the viewer config data to the trace buffer.
- *
- * @param dataSource The target datasource to write the viewer config to.
- * @param viewerConfigFilePath The path of the viewer config file which contains the data we
- * want to write to the trace buffer.
- * @throws FileNotFoundException if the viewerConfigFilePath is invalid.
- */
- void trace(@NonNull ProtoLogDataSource dataSource, @NonNull String viewerConfigFilePath);
- }
-
- @Override
- public void registerClient(@NonNull IProtoLogClient client, @NonNull IRegisterClientArgs args)
- throws RemoteException {
- client.asBinder().linkToDeath(() -> onClientBinderDeath(client), /* flags */ 0);
-
- final String viewerConfigFile = args.getViewerConfigFile();
- if (viewerConfigFile != null) {
- registerViewerConfigFile(client, viewerConfigFile);
- }
-
- registerGroups(client, args.getGroups(), args.getGroupsDefaultLogcatStatus());
- }
-
- @Override
- public void onShellCommand(@Nullable FileDescriptor in, @Nullable FileDescriptor out,
- @Nullable FileDescriptor err, @NonNull String[] args, @Nullable ShellCallback callback,
- @NonNull ResultReceiver resultReceiver) throws RemoteException {
- new ProtoLogCommandHandler(this)
- .exec(this, in, out, err, args, callback, resultReceiver);
- }
-
+public interface ProtoLogConfigurationService extends IProtoLogConfigurationService {
/**
* Get the list of groups clients have registered to the protolog service.
* @return The list of ProtoLog groups registered with this service.
*/
@NonNull
- public String[] getGroups() {
- return mRegisteredGroups.toArray(new String[0]);
- }
-
- /**
- * Enable logging target groups to logcat.
- * @param groups we want to enable logging them to logcat for.
- */
- public void enableProtoLogToLogcat(String... groups) {
- toggleProtoLogToLogcat(true, groups);
- }
-
- /**
- * Disable logging target groups to logcat.
- * @param groups we want to disable from being logged to logcat.
- */
- public void disableProtoLogToLogcat(String... groups) {
- toggleProtoLogToLogcat(false, groups);
- }
+ String[] getGroups();
/**
* Check if a group is logging to logcat
* @param group The group we want to check for
* @return True iff we are logging this group to logcat.
*/
- public boolean isLoggingToLogcat(@NonNull String group) {
- final Boolean isLoggingToLogcat = mLogGroupToLogcatStatus.get(group);
+ boolean isLoggingToLogcat(@NonNull String group);
- if (isLoggingToLogcat == null) {
- throw new RuntimeException(
- "Trying to get logcat logging status of non-registered group " + group);
- }
+ /**
+ * Enable logging target groups to logcat.
+ * @param groups we want to enable logging them to logcat for.
+ */
+ void enableProtoLogToLogcat(@NonNull String... groups);
- return isLoggingToLogcat;
- }
-
- private void registerViewerConfigFile(
- @NonNull IProtoLogClient client, @NonNull String viewerConfigFile) {
- final var count = mConfigFileCounts.getOrDefault(viewerConfigFile, 0);
- mConfigFileCounts.put(viewerConfigFile, count + 1);
- mClientConfigFiles.put(client, viewerConfigFile);
- }
-
- private void registerGroups(@NonNull IProtoLogClient client, @NonNull String[] groups,
- @NonNull boolean[] logcatStatuses) throws RemoteException {
- if (groups.length != logcatStatuses.length) {
- throw new RuntimeException(
- "Expected groups and logcatStatuses to have the same length, "
- + "but groups has length " + groups.length
- + " and logcatStatuses has length " + logcatStatuses.length);
- }
-
- for (int i = 0; i < groups.length; i++) {
- String group = groups[i];
- boolean logcatStatus = logcatStatuses[i];
-
- mRegisteredGroups.add(group);
-
- mGroupToClients.putIfAbsent(group, new HashSet<>());
- mGroupToClients.get(group).add(client);
-
- if (!mLogGroupToLogcatStatus.containsKey(group)) {
- mLogGroupToLogcatStatus.put(group, logcatStatus);
- }
-
- boolean requestedLogToLogcat = mLogGroupToLogcatStatus.get(group);
- if (requestedLogToLogcat != logcatStatus) {
- client.toggleLogcat(requestedLogToLogcat, new String[] { group });
- }
- }
- }
-
- private void toggleProtoLogToLogcat(boolean enabled, @NonNull String[] groups) {
- final var clientToGroups = new HashMap<IProtoLogClient, Set<String>>();
-
- for (String group : groups) {
- final var clients = mGroupToClients.get(group);
-
- if (clients == null) {
- // No clients associated to this group
- Log.w(LOG_TAG, "Attempting to toggle log to logcat for group " + group
- + " with no registered clients.");
- continue;
- }
-
- for (IProtoLogClient client : clients) {
- clientToGroups.putIfAbsent(client, new HashSet<>());
- clientToGroups.get(client).add(group);
- }
- }
-
- for (IProtoLogClient client : clientToGroups.keySet()) {
- try {
- client.toggleLogcat(enabled, clientToGroups.get(client).toArray(new String[0]));
- } catch (RemoteException e) {
- throw new RuntimeException(
- "Failed to toggle logcat status for groups on client", e);
- }
- }
-
- for (String group : groups) {
- mLogGroupToLogcatStatus.put(group, enabled);
- }
- }
-
- private void onTracingInstanceStart(int instanceIdx, ProtoLogDataSource.ProtoLogConfig config) {
- mRunningInstances.add(instanceIdx);
- }
-
- private void onTracingInstanceFlush() {
- for (String fileName : mConfigFileCounts.keySet()) {
- mViewerConfigFileTracer.trace(mDataSource, fileName);
- }
- }
-
- private void onTracingInstanceStop(int instanceIdx, ProtoLogDataSource.ProtoLogConfig config) {
- mRunningInstances.remove(instanceIdx);
- }
-
- private static void dumpTransitionTraceConfig(@NonNull ProtoLogDataSource dataSource,
- @NonNull String viewerConfigFilePath) {
- Utils.dumpViewerConfig(dataSource, () -> {
- try {
- return new ProtoInputStream(new FileInputStream(viewerConfigFilePath));
- } catch (FileNotFoundException e) {
- throw new RuntimeException(
- "Failed to load viewer config file " + viewerConfigFilePath, e);
- }
- });
- }
-
- private void onClientBinderDeath(@NonNull IProtoLogClient client) {
- // Dump the tracing config now if no other client is going to dump the same config file.
- String configFile = mClientConfigFiles.get(client);
- if (configFile != null) {
- final var newCount = mConfigFileCounts.get(configFile) - 1;
- mConfigFileCounts.put(configFile, newCount);
- boolean lastProcessWithViewerConfig = newCount == 0;
- if (lastProcessWithViewerConfig) {
- mViewerConfigFileTracer.trace(mDataSource, configFile);
- }
- }
- }
-
- private static void writeViewerConfigGroup(
- @NonNull ProtoInputStream pis, @NonNull ProtoOutputStream os) throws IOException {
- final long inGroupToken = pis.start(GROUPS);
- final long outGroupToken = os.start(GROUPS);
-
- while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
- switch (pis.getFieldNumber()) {
- case (int) ID -> {
- int id = pis.readInt(ID);
- os.write(ID, id);
- }
- case (int) NAME -> {
- String name = pis.readString(NAME);
- os.write(NAME, name);
- }
- case (int) TAG -> {
- String tag = pis.readString(TAG);
- os.write(TAG, tag);
- }
- default ->
- throw new RuntimeException(
- "Unexpected field id " + pis.getFieldNumber());
- }
- }
-
- pis.end(inGroupToken);
- os.end(outGroupToken);
- }
-
- private static void writeViewerConfigMessage(
- @NonNull ProtoInputStream pis, @NonNull ProtoOutputStream os) throws IOException {
- final long inMessageToken = pis.start(MESSAGES);
- final long outMessagesToken = os.start(MESSAGES);
-
- while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
- switch (pis.getFieldNumber()) {
- case (int) MESSAGE_ID -> os.write(MESSAGE_ID,
- pis.readLong(MESSAGE_ID));
- case (int) MESSAGE -> os.write(MESSAGE, pis.readString(MESSAGE));
- case (int) LEVEL -> os.write(LEVEL, pis.readInt(LEVEL));
- case (int) GROUP_ID -> os.write(GROUP_ID, pis.readInt(GROUP_ID));
- case (int) LOCATION -> os.write(LOCATION, pis.readString(LOCATION));
- default ->
- throw new RuntimeException(
- "Unexpected field id " + pis.getFieldNumber());
- }
- }
-
- pis.end(inMessageToken);
- os.end(outMessagesToken);
- }
+ /**
+ * Disable logging target groups to logcat.
+ * @param groups we want to disable from being logged to logcat.
+ */
+ void disableProtoLogToLogcat(@NonNull String... groups);
}
diff --git a/core/java/com/android/internal/protolog/ProtoLogConfigurationServiceImpl.java b/core/java/com/android/internal/protolog/ProtoLogConfigurationServiceImpl.java
new file mode 100644
index 0000000..e382ac1
--- /dev/null
+++ b/core/java/com/android/internal/protolog/ProtoLogConfigurationServiceImpl.java
@@ -0,0 +1,454 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.protolog;
+
+import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.GROUPS;
+import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.Group.ID;
+import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.Group.NAME;
+import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.Group.TAG;
+import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MESSAGES;
+import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.GROUP_ID;
+import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.LEVEL;
+import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.LOCATION;
+import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.MESSAGE;
+import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.MESSAGE_ID;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemService;
+import android.content.Context;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.os.ShellCallback;
+import android.tracing.perfetto.DataSourceParams;
+import android.tracing.perfetto.InitArguments;
+import android.tracing.perfetto.Producer;
+import android.util.Log;
+import android.util.proto.ProtoInputStream;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+
+/**
+ * The ProtoLog service is responsible for orchestrating centralized actions of the protolog tracing
+ * system. Currently this service has the following roles:
+ * - Handle shell commands to toggle logging ProtoLog messages for specified groups to logcat.
+ * - Handle viewer config dumping (the mapping from message hash to message string) for all protolog
+ * clients. This is for two reasons: firstly, because client processes might be frozen so might
+ * not response to the request to dump their viewer config when the trace is stopped; secondly,
+ * multiple processes might be running the same code with the same viewer config, this centralized
+ * service ensures we don't dump the same viewer config multiple times across processes.
+ * <p>
+ * {@link com.android.internal.protolog.IProtoLogClient ProtoLog clients} register themselves to
+ * this service on initialization.
+ * <p>
+ * This service is intended to run on the system server, such that it never gets frozen.
+ */
+@SystemService(Context.PROTOLOG_CONFIGURATION_SERVICE)
+public class ProtoLogConfigurationServiceImpl extends IProtoLogConfigurationService.Stub
+ implements ProtoLogConfigurationService {
+ private static final String LOG_TAG = "ProtoLogConfigurationService";
+
+ private final ProtoLogDataSource mDataSource;
+
+ /**
+ * Keeps track of how many of each viewer config file is currently registered.
+ * Use to keep track of which viewer config files are actively being used in tracing and might
+ * need to be dumped on flush.
+ */
+ private final Map<String, Integer> mConfigFileCounts = new HashMap<>();
+ /**
+ * Keeps track of the viewer config file of each client if available.
+ */
+ private final Map<IProtoLogClient, String> mClientConfigFiles = new HashMap<>();
+
+ /**
+ * Keeps track of all the protolog groups that have been registered by clients and are still
+ * being actively traced.
+ */
+ private final Set<String> mRegisteredGroups = new HashSet<>();
+ /**
+ * Keeps track of all the clients that are actively tracing a given protolog group.
+ */
+ private final Map<String, Set<IProtoLogClient>> mGroupToClients = new HashMap<>();
+
+ /**
+ * Keeps track of whether or not a given group should be logged to logcat.
+ * True when logging to logcat, false otherwise.
+ */
+ private final Map<String, Boolean> mLogGroupToLogcatStatus = new TreeMap<>();
+
+ /**
+ * Keeps track of all the tracing instance ids that are actively running for ProtoLog.
+ */
+ private final Set<Integer> mRunningInstances = new HashSet<>();
+
+ private final ViewerConfigFileTracer mViewerConfigFileTracer;
+
+ public ProtoLogConfigurationServiceImpl() {
+ this(ProtoLogDataSource::new, ProtoLogConfigurationServiceImpl::dumpTransitionTraceConfig);
+ }
+
+ @VisibleForTesting
+ public ProtoLogConfigurationServiceImpl(@NonNull ProtoLogDataSourceBuilder dataSourceBuilder) {
+ this(dataSourceBuilder, ProtoLogConfigurationServiceImpl::dumpTransitionTraceConfig);
+ }
+
+ @VisibleForTesting
+ public ProtoLogConfigurationServiceImpl(@NonNull ViewerConfigFileTracer tracer) {
+ this(ProtoLogDataSource::new, tracer);
+ }
+
+ @VisibleForTesting
+ public ProtoLogConfigurationServiceImpl(
+ @NonNull ProtoLogDataSourceBuilder dataSourceBuilder,
+ @NonNull ViewerConfigFileTracer tracer) {
+ mDataSource = dataSourceBuilder.build(
+ this::onTracingInstanceStart,
+ this::onTracingInstanceFlush,
+ this::onTracingInstanceStop
+ );
+
+ // Initialize the Perfetto producer and register the Perfetto ProtoLog datasource to be
+ // receive the lifecycle callbacks of the datasource and write the viewer configs if and
+ // when required to the datasource.
+ Producer.init(InitArguments.DEFAULTS);
+ final var params = new DataSourceParams.Builder()
+ .setBufferExhaustedPolicy(DataSourceParams.PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_DROP)
+ .build();
+ mDataSource.register(params);
+
+ mViewerConfigFileTracer = tracer;
+ }
+
+ public static class RegisterClientArgs extends IRegisterClientArgs.Stub {
+ /**
+ * The viewer config file to be registered for this client ProtoLog process.
+ */
+ @Nullable
+ private String mViewerConfigFile = null;
+ /**
+ * The list of all groups that this client protolog process supports and might trace.
+ */
+ @NonNull
+ private String[] mGroups = new String[0];
+ /**
+ * The default logcat status of the ProtoLog client. True is logging to logcat, false
+ * otherwise. The indices should match the indices in {@link mGroups}.
+ */
+ @NonNull
+ private boolean[] mLogcatStatus = new boolean[0];
+
+ public record GroupConfig(@NonNull String group, boolean logToLogcat) {}
+
+ /**
+ * Specify groups to register with this client that will be used for protologging in this
+ * process.
+ * @param groups to register with this client.
+ * @return self
+ */
+ public RegisterClientArgs setGroups(GroupConfig... groups) {
+ mGroups = new String[groups.length];
+ mLogcatStatus = new boolean[groups.length];
+
+ for (int i = 0; i < groups.length; i++) {
+ mGroups[i] = groups[i].group;
+ mLogcatStatus[i] = groups[i].logToLogcat;
+ }
+
+ return this;
+ }
+
+ /**
+ * Set the viewer config file that the logs in this process are using.
+ * @param viewerConfigFile The file path of the viewer config.
+ * @return self
+ */
+ public RegisterClientArgs setViewerConfigFile(@NonNull String viewerConfigFile) {
+ mViewerConfigFile = viewerConfigFile;
+
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public String[] getGroups() {
+ return mGroups;
+ }
+
+ @Override
+ @NonNull
+ public boolean[] getGroupsDefaultLogcatStatus() {
+ return mLogcatStatus;
+ }
+
+ @Nullable
+ @Override
+ public String getViewerConfigFile() {
+ return mViewerConfigFile;
+ }
+ }
+
+ @FunctionalInterface
+ public interface ViewerConfigFileTracer {
+ /**
+ * Write the viewer config data to the trace buffer.
+ *
+ * @param dataSource The target datasource to write the viewer config to.
+ * @param viewerConfigFilePath The path of the viewer config file which contains the data we
+ * want to write to the trace buffer.
+ * @throws FileNotFoundException if the viewerConfigFilePath is invalid.
+ */
+ void trace(@NonNull ProtoLogDataSource dataSource, @NonNull String viewerConfigFilePath);
+ }
+
+ @Override
+ public void registerClient(@NonNull IProtoLogClient client, @NonNull IRegisterClientArgs args)
+ throws RemoteException {
+ client.asBinder().linkToDeath(() -> onClientBinderDeath(client), /* flags */ 0);
+
+ final String viewerConfigFile = args.getViewerConfigFile();
+ if (viewerConfigFile != null) {
+ registerViewerConfigFile(client, viewerConfigFile);
+ }
+
+ registerGroups(client, args.getGroups(), args.getGroupsDefaultLogcatStatus());
+ }
+
+ @Override
+ public void onShellCommand(@Nullable FileDescriptor in, @Nullable FileDescriptor out,
+ @Nullable FileDescriptor err, @NonNull String[] args, @Nullable ShellCallback callback,
+ @NonNull ResultReceiver resultReceiver) throws RemoteException {
+ new ProtoLogCommandHandler(this)
+ .exec(this, in, out, err, args, callback, resultReceiver);
+ }
+
+ /**
+ * Get the list of groups clients have registered to the protolog service.
+ * @return The list of ProtoLog groups registered with this service.
+ */
+ @Override
+ @NonNull
+ public String[] getGroups() {
+ return mRegisteredGroups.toArray(new String[0]);
+ }
+
+ /**
+ * Enable logging target groups to logcat.
+ * @param groups we want to enable logging them to logcat for.
+ */
+ @Override
+ public void enableProtoLogToLogcat(@NonNull String... groups) {
+ toggleProtoLogToLogcat(true, groups);
+ }
+
+ /**
+ * Disable logging target groups to logcat.
+ * @param groups we want to disable from being logged to logcat.
+ */
+ @Override
+ public void disableProtoLogToLogcat(@NonNull String... groups) {
+ toggleProtoLogToLogcat(false, groups);
+ }
+
+ /**
+ * Check if a group is logging to logcat
+ * @param group The group we want to check for
+ * @return True iff we are logging this group to logcat.
+ */
+ @Override
+ public boolean isLoggingToLogcat(@NonNull String group) {
+ final Boolean isLoggingToLogcat = mLogGroupToLogcatStatus.get(group);
+
+ if (isLoggingToLogcat == null) {
+ throw new RuntimeException(
+ "Trying to get logcat logging status of non-registered group " + group);
+ }
+
+ return isLoggingToLogcat;
+ }
+
+ private void registerViewerConfigFile(
+ @NonNull IProtoLogClient client, @NonNull String viewerConfigFile) {
+ final var count = mConfigFileCounts.getOrDefault(viewerConfigFile, 0);
+ mConfigFileCounts.put(viewerConfigFile, count + 1);
+ mClientConfigFiles.put(client, viewerConfigFile);
+ }
+
+ private void registerGroups(@NonNull IProtoLogClient client, @NonNull String[] groups,
+ @NonNull boolean[] logcatStatuses) throws RemoteException {
+ if (groups.length != logcatStatuses.length) {
+ throw new RuntimeException(
+ "Expected groups and logcatStatuses to have the same length, "
+ + "but groups has length " + groups.length
+ + " and logcatStatuses has length " + logcatStatuses.length);
+ }
+
+ for (int i = 0; i < groups.length; i++) {
+ String group = groups[i];
+ boolean logcatStatus = logcatStatuses[i];
+
+ mRegisteredGroups.add(group);
+
+ mGroupToClients.putIfAbsent(group, new HashSet<>());
+ mGroupToClients.get(group).add(client);
+
+ if (!mLogGroupToLogcatStatus.containsKey(group)) {
+ mLogGroupToLogcatStatus.put(group, logcatStatus);
+ }
+
+ boolean requestedLogToLogcat = mLogGroupToLogcatStatus.get(group);
+ if (requestedLogToLogcat != logcatStatus) {
+ client.toggleLogcat(requestedLogToLogcat, new String[] { group });
+ }
+ }
+ }
+
+ private void toggleProtoLogToLogcat(boolean enabled, @NonNull String[] groups) {
+ final var clientToGroups = new HashMap<IProtoLogClient, Set<String>>();
+
+ for (String group : groups) {
+ final var clients = mGroupToClients.get(group);
+
+ if (clients == null) {
+ // No clients associated to this group
+ Log.w(LOG_TAG, "Attempting to toggle log to logcat for group " + group
+ + " with no registered clients.");
+ continue;
+ }
+
+ for (IProtoLogClient client : clients) {
+ clientToGroups.putIfAbsent(client, new HashSet<>());
+ clientToGroups.get(client).add(group);
+ }
+ }
+
+ for (IProtoLogClient client : clientToGroups.keySet()) {
+ try {
+ client.toggleLogcat(enabled, clientToGroups.get(client).toArray(new String[0]));
+ } catch (RemoteException e) {
+ throw new RuntimeException(
+ "Failed to toggle logcat status for groups on client", e);
+ }
+ }
+
+ for (String group : groups) {
+ mLogGroupToLogcatStatus.put(group, enabled);
+ }
+ }
+
+ private void onTracingInstanceStart(int instanceIdx, ProtoLogDataSource.ProtoLogConfig config) {
+ mRunningInstances.add(instanceIdx);
+ }
+
+ private void onTracingInstanceFlush() {
+ for (String fileName : mConfigFileCounts.keySet()) {
+ mViewerConfigFileTracer.trace(mDataSource, fileName);
+ }
+ }
+
+ private void onTracingInstanceStop(int instanceIdx, ProtoLogDataSource.ProtoLogConfig config) {
+ mRunningInstances.remove(instanceIdx);
+ }
+
+ private static void dumpTransitionTraceConfig(@NonNull ProtoLogDataSource dataSource,
+ @NonNull String viewerConfigFilePath) {
+ Utils.dumpViewerConfig(dataSource, () -> {
+ try {
+ return new ProtoInputStream(new FileInputStream(viewerConfigFilePath));
+ } catch (FileNotFoundException e) {
+ throw new RuntimeException(
+ "Failed to load viewer config file " + viewerConfigFilePath, e);
+ }
+ });
+ }
+
+ private void onClientBinderDeath(@NonNull IProtoLogClient client) {
+ // Dump the tracing config now if no other client is going to dump the same config file.
+ String configFile = mClientConfigFiles.get(client);
+ if (configFile != null) {
+ final var newCount = mConfigFileCounts.get(configFile) - 1;
+ mConfigFileCounts.put(configFile, newCount);
+ boolean lastProcessWithViewerConfig = newCount == 0;
+ if (lastProcessWithViewerConfig) {
+ mViewerConfigFileTracer.trace(mDataSource, configFile);
+ }
+ }
+ }
+
+ private static void writeViewerConfigGroup(
+ @NonNull ProtoInputStream pis, @NonNull ProtoOutputStream os) throws IOException {
+ final long inGroupToken = pis.start(GROUPS);
+ final long outGroupToken = os.start(GROUPS);
+
+ while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+ switch (pis.getFieldNumber()) {
+ case (int) ID -> {
+ int id = pis.readInt(ID);
+ os.write(ID, id);
+ }
+ case (int) NAME -> {
+ String name = pis.readString(NAME);
+ os.write(NAME, name);
+ }
+ case (int) TAG -> {
+ String tag = pis.readString(TAG);
+ os.write(TAG, tag);
+ }
+ default ->
+ throw new RuntimeException(
+ "Unexpected field id " + pis.getFieldNumber());
+ }
+ }
+
+ pis.end(inGroupToken);
+ os.end(outGroupToken);
+ }
+
+ private static void writeViewerConfigMessage(
+ @NonNull ProtoInputStream pis, @NonNull ProtoOutputStream os) throws IOException {
+ final long inMessageToken = pis.start(MESSAGES);
+ final long outMessagesToken = os.start(MESSAGES);
+
+ while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+ switch (pis.getFieldNumber()) {
+ case (int) MESSAGE_ID -> os.write(MESSAGE_ID,
+ pis.readLong(MESSAGE_ID));
+ case (int) MESSAGE -> os.write(MESSAGE, pis.readString(MESSAGE));
+ case (int) LEVEL -> os.write(LEVEL, pis.readInt(LEVEL));
+ case (int) GROUP_ID -> os.write(GROUP_ID, pis.readInt(GROUP_ID));
+ case (int) LOCATION -> os.write(LOCATION, pis.readString(LOCATION));
+ default ->
+ throw new RuntimeException(
+ "Unexpected field id " + pis.getFieldNumber());
+ }
+ }
+
+ pis.end(inMessageToken);
+ os.end(outMessagesToken);
+ }
+}
diff --git a/core/java/com/android/internal/view/menu/ListMenuItemView.java b/core/java/com/android/internal/view/menu/ListMenuItemView.java
index b73cacb..bdb33c4 100644
--- a/core/java/com/android/internal/view/menu/ListMenuItemView.java
+++ b/core/java/com/android/internal/view/menu/ListMenuItemView.java
@@ -33,8 +33,6 @@
import android.widget.RadioButton;
import android.widget.TextView;
-import com.android.text.flags.Flags;
-
/**
* The item view for each item in the ListView-based MenuViews.
*/
@@ -283,10 +281,7 @@
private void insertIconView() {
LayoutInflater inflater = getInflater();
- mIconView = (ImageView) inflater.inflate(
- !Flags.fixMisalignedContextMenu()
- ? com.android.internal.R.layout.list_menu_item_fixed_size_icon :
- com.android.internal.R.layout.list_menu_item_icon,
+ mIconView = (ImageView) inflater.inflate(com.android.internal.R.layout.list_menu_item_icon,
this, false);
addContentView(mIconView, 0);
}
diff --git a/core/java/com/android/internal/view/menu/StandardMenuPopup.java b/core/java/com/android/internal/view/menu/StandardMenuPopup.java
index c43a8c6..8e2536a 100644
--- a/core/java/com/android/internal/view/menu/StandardMenuPopup.java
+++ b/core/java/com/android/internal/view/menu/StandardMenuPopup.java
@@ -35,8 +35,6 @@
import android.widget.PopupWindow.OnDismissListener;
import android.widget.TextView;
-import com.android.text.flags.Flags;
-
import java.util.Objects;
/**
@@ -122,8 +120,7 @@
mMenu = menu;
mOverflowOnly = overflowOnly;
final LayoutInflater inflater = LayoutInflater.from(context);
- mAdapter = new MenuAdapter(menu, inflater, mOverflowOnly,
- Flags.fixMisalignedContextMenu() ? ITEM_LAYOUT_MATERIAL : ITEM_LAYOUT);
+ mAdapter = new MenuAdapter(menu, inflater, mOverflowOnly, ITEM_LAYOUT_MATERIAL);
mPopupStyleAttr = popupStyleAttr;
mPopupStyleRes = popupStyleRes;
diff --git a/core/proto/android/providers/settings/secure.proto b/core/proto/android/providers/settings/secure.proto
index cb7c226..606e829 100644
--- a/core/proto/android/providers/settings/secure.proto
+++ b/core/proto/android/providers/settings/secure.proto
@@ -401,6 +401,7 @@
optional SettingProto long_press_timeout = 35 [ (android.privacy).dest = DEST_AUTOMATIC ];
optional SettingProto key_press_timeout_ms = 96 [ (android.privacy).dest = DEST_AUTOMATIC ];
optional SettingProto key_press_delay_ms = 97 [ (android.privacy).dest = DEST_AUTOMATIC ];
+ optional SettingProto key_repeat_enabled = 102 [ (android.privacy).dest = DEST_AUTOMATIC ];
message ManagedProfile {
option (android.msg_privacy).dest = DEST_EXPLICIT;
@@ -735,5 +736,5 @@
// Please insert fields in alphabetical order and group them into messages
// if possible (to avoid reaching the method limit).
- // Next tag = 102;
+ // Next tag = 103;
}
diff --git a/core/tests/coretests/src/android/tracing/OWNERS b/core/tests/coretests/src/android/tracing/OWNERS
deleted file mode 100644
index 86a7e88..0000000
--- a/core/tests/coretests/src/android/tracing/OWNERS
+++ /dev/null
@@ -1 +0,0 @@
-include platform/development:/tools/winscope/OWNERS
\ No newline at end of file
diff --git a/core/tests/coretests/src/android/tracing/TEST_MAPPING b/core/tests/coretests/src/android/tracing/TEST_MAPPING
deleted file mode 100644
index 4b7adf9..0000000
--- a/core/tests/coretests/src/android/tracing/TEST_MAPPING
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "postsubmit": [
- {
- "name": "FrameworksCoreTests_android_tracing",
- "file_patterns": [".*\\.java"]
- }
- ]
-}
diff --git a/graphics/java/android/graphics/Bitmap.java b/graphics/java/android/graphics/Bitmap.java
index 6d31578..e07471c 100644
--- a/graphics/java/android/graphics/Bitmap.java
+++ b/graphics/java/android/graphics/Bitmap.java
@@ -128,6 +128,22 @@
private static final WeakHashMap<Bitmap, Void> sAllBitmaps = new WeakHashMap<>();
/**
+ * @hide
+ */
+ private static NativeAllocationRegistry getRegistry(boolean malloc, long size) {
+ final long free = nativeGetNativeFinalizer();
+ if (com.android.libcore.Flags.nativeMetrics()) {
+ Class cls = Bitmap.class;
+ return malloc ? NativeAllocationRegistry.createMalloced(cls, free, size)
+ : NativeAllocationRegistry.createNonmalloced(cls, free, size);
+ } else {
+ ClassLoader loader = Bitmap.class.getClassLoader();
+ return malloc ? NativeAllocationRegistry.createMalloced(loader, free, size)
+ : NativeAllocationRegistry.createNonmalloced(loader, free, size);
+ }
+ }
+
+ /**
* Private constructor that must receive an already allocated native bitmap
* int (pointer).
*/
@@ -151,7 +167,6 @@
mWidth = width;
mHeight = height;
mRequestPremultiplied = requestPremultiplied;
-
mNinePatchChunk = ninePatchChunk;
mNinePatchInsets = ninePatchInsets;
if (density >= 0) {
@@ -159,17 +174,9 @@
}
mNativePtr = nativeBitmap;
-
final int allocationByteCount = getAllocationByteCount();
- NativeAllocationRegistry registry;
- if (fromMalloc) {
- registry = NativeAllocationRegistry.createMalloced(
- Bitmap.class.getClassLoader(), nativeGetNativeFinalizer(), allocationByteCount);
- } else {
- registry = NativeAllocationRegistry.createNonmalloced(
- Bitmap.class.getClassLoader(), nativeGetNativeFinalizer(), allocationByteCount);
- }
- registry.registerNativeAllocation(this, nativeBitmap);
+ getRegistry(fromMalloc, allocationByteCount).registerNativeAllocation(this, mNativePtr);
+
synchronized (Bitmap.class) {
sAllBitmaps.put(this, null);
}
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
index dd86a1a..83619ef 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
@@ -90,9 +90,6 @@
/** The maximum override density allowed for tasks inside the desktop. */
private static final int DESKTOP_DENSITY_MAX = 1000;
- /** The number of [WindowDecorViewHost] instances to warm up on system start. */
- private static final int WINDOW_DECOR_PRE_WARM_SIZE = 2;
-
/**
* Sysprop declaring whether to enters desktop mode by default when the windowing mode of the
* display's root TaskDisplayArea is set to WINDOWING_MODE_FREEFORM.
@@ -115,14 +112,6 @@
private static final String MAX_TASK_LIMIT_SYS_PROP = "persist.wm.debug.desktop_max_task_limit";
/**
- * Sysprop declaring the number of [WindowDecorViewHost] instances to warm up on system start.
- *
- * <p>If it is not defined, then [WINDOW_DECOR_PRE_WARM_SIZE] is used.
- */
- private static final String WINDOW_DECOR_PRE_WARM_SIZE_SYS_PROP =
- "persist.wm.debug.desktop_window_decor_pre_warm_size";
-
- /**
* Return {@code true} if veiled resizing is active. If false, fluid resizing is used.
*/
public static boolean isVeiledResizeEnabled() {
@@ -162,12 +151,6 @@
context.getResources().getInteger(R.integer.config_maxDesktopWindowingActiveTasks));
}
- /** The number of [WindowDecorViewHost] instances to warm up on system start. */
- public static int getWindowDecorPreWarmSize() {
- return SystemProperties.getInt(WINDOW_DECOR_PRE_WARM_SIZE_SYS_PROP,
- WINDOW_DECOR_PRE_WARM_SIZE);
- }
-
/**
* Return {@code true} if the current device supports desktop mode.
*/
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index c545d73..af4a0c5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -1241,8 +1241,9 @@
mBubbleData.dismissBubbleWithKey(
bubbleKey, Bubbles.DISMISS_USER_GESTURE_FROM_LAUNCHER, timestamp);
}
- if (selectedBubbleKey != null && !selectedBubbleKey.equals(bubbleKey)) {
- // We did not remove the selected bubble. Expand it again
+ if (mBubbleData.hasBubbles()) {
+ // We still have bubbles, if we dragged an individual bubble to dismiss we were expanded
+ // so re-expand to whatever is selected.
showExpandedViewForBubbleBar();
}
}
@@ -2007,7 +2008,7 @@
@Override
public void selectionChanged(BubbleViewProvider selectedBubble) {
// Only need to update the layer view if we're currently expanded for selection changes.
- if (mLayerView != null && isStackExpanded()) {
+ if (mLayerView != null && mLayerView.isExpanded()) {
mLayerView.showExpandedView(selectedBubble);
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
index 1c9c195..1367b7e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
@@ -186,6 +186,10 @@
if (expandedView == null) {
return;
}
+ if (mExpandedBubble != null && mIsExpanded && b.getKey().equals(mExpandedBubble.getKey())) {
+ // Already showing this bubble, skip animating
+ return;
+ }
if (mExpandedBubble != null && !b.getKey().equals(mExpandedBubble.getKey())) {
removeView(mExpandedView);
mExpandedView = null;
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 b47adb4..584f272 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
@@ -75,6 +75,7 @@
import com.android.wm.shell.desktopmode.ReturnToDragStartAnimator;
import com.android.wm.shell.desktopmode.SpringDragToDesktopTransitionHandler;
import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler;
+import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository;
import com.android.wm.shell.desktopmode.education.AppHandleEducationController;
import com.android.wm.shell.desktopmode.education.AppHandleEducationFilter;
import com.android.wm.shell.desktopmode.education.data.AppHandleEducationDatastoreRepository;
@@ -116,8 +117,6 @@
import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel;
import com.android.wm.shell.windowdecor.WindowDecorViewModel;
import com.android.wm.shell.windowdecor.viewhost.DefaultWindowDecorViewHostSupplier;
-import com.android.wm.shell.windowdecor.viewhost.PooledWindowDecorViewHostSupplier;
-import com.android.wm.shell.windowdecor.viewhost.ReusableWindowDecorViewHost;
import com.android.wm.shell.windowdecor.viewhost.WindowDecorViewHostSupplier;
import dagger.Binds;
@@ -143,7 +142,7 @@
includes = {
WMShellBaseModule.class,
PipModule.class,
- ShellBackAnimationModule.class,
+ ShellBackAnimationModule.class
})
public abstract class WMShellModule {
@@ -249,6 +248,7 @@
AssistContentRequester assistContentRequester,
MultiInstanceHelper multiInstanceHelper,
Optional<DesktopTasksLimiter> desktopTasksLimiter,
+ WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository,
Optional<DesktopActivityOrientationChangeHandler> desktopActivityOrientationHandler,
WindowDecorViewHostSupplier windowDecorViewHostSupplier) {
if (DesktopModeStatus.canEnterDesktopMode(context)) {
@@ -274,6 +274,7 @@
assistContentRequester,
multiInstanceHelper,
desktopTasksLimiter,
+ windowDecorCaptionHandleRepository,
desktopActivityOrientationHandler,
windowDecorViewHostSupplier);
}
@@ -384,19 +385,8 @@
@WMSingleton
@Provides
static WindowDecorViewHostSupplier provideWindowDecorViewHostSupplier(
- @NonNull Context context,
- @ShellMainThread @NonNull CoroutineScope mainScope,
- @NonNull ShellInit shellInit) {
- if (DesktopModeStatus.canEnterDesktopMode(context)
- && Flags.enableDesktopWindowingScvhCache()) {
- final int maxPoolSize = DesktopModeStatus.getMaxTaskLimit(context);
- final int preWarmSize = DesktopModeStatus.getWindowDecorPreWarmSize();
- return new PooledWindowDecorViewHostSupplier(
- context, mainScope, shellInit,
- ReusableWindowDecorViewHost.DefaultFactory.INSTANCE, maxPoolSize, preWarmSize);
- } else {
- return new DefaultWindowDecorViewHostSupplier(mainScope);
- }
+ @ShellMainThread @NonNull CoroutineScope mainScope) {
+ return new DefaultWindowDecorViewHostSupplier(mainScope);
}
//
@@ -793,6 +783,12 @@
@WMSingleton
@Provides
+ static WindowDecorCaptionHandleRepository provideAppHandleRepository() {
+ return new WindowDecorCaptionHandleRepository();
+ }
+
+ @WMSingleton
+ @Provides
static AppHandleEducationController provideAppHandleEducationController(
AppHandleEducationFilter appHandleEducationFilter,
ShellTaskOrganizer shellTaskOrganizer,
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 853284a..b8ebbcd 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
@@ -305,13 +305,18 @@
private fun getSplitFocusedTask(task1: RunningTaskInfo, task2: RunningTaskInfo) =
if (task1.taskId == task2.parentTaskId) task2 else task1
- private fun isFreeformDisplay(displayId: Int): Boolean {
+ private fun forceEnterDesktop(displayId: Int): Boolean {
+ if (!DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)) {
+ return false
+ }
+
val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(displayId)
requireNotNull(tdaInfo) {
"This method can only be called with the ID of a display having non-null DisplayArea."
}
val tdaWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode
- return tdaWindowingMode == WINDOWING_MODE_FREEFORM
+ val isFreeformDisplay = tdaWindowingMode == WINDOWING_MODE_FREEFORM
+ return isFreeformDisplay
}
/** Moves task to desktop mode if task is running, else launches it in desktop mode. */
@@ -1191,10 +1196,11 @@
val wct = WindowContainerTransaction()
if (!isDesktopModeShowing(task.displayId)) {
logD("Bring desktop tasks to front on transition=taskId=%d", task.taskId)
- // We are outside of desktop mode and already existing desktop task is being launched.
- // We should make this task go to fullscreen instead of freeform. Note that this means
- // any re-launch of a freeform window outside of desktop will be in fullscreen.
- if (taskRepository.isActiveTask(task.taskId)) {
+ if (taskRepository.isActiveTask(task.taskId) && !forceEnterDesktop(task.displayId)) {
+ // We are outside of desktop mode and already existing desktop task is being
+ // launched. We should make this task go to fullscreen instead of freeform. Note
+ // that this means any re-launch of a freeform window outside of desktop will be in
+ // fullscreen as long as default-desktop flag is disabled.
addMoveToFullscreenChanges(wct, task)
return wct
}
@@ -1231,9 +1237,7 @@
transition: IBinder
): WindowContainerTransaction? {
logV("handleFullscreenTaskLaunch")
- val forceEnterDesktop = DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context) &&
- isFreeformDisplay(task.displayId)
- if (isDesktopModeShowing(task.displayId) || forceEnterDesktop) {
+ if (isDesktopModeShowing(task.displayId) || forceEnterDesktop(task.displayId)) {
logD("Switch fullscreen task to freeform on transition: taskId=%d", task.taskId)
return WindowContainerTransaction().also { wct ->
addMoveToDesktopChanges(wct, task)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepository.kt
new file mode 100644
index 0000000..7ae5370
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepository.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.desktopmode
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.graphics.Rect
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+
+/** Repository to observe caption state. */
+class WindowDecorCaptionHandleRepository {
+ private val _captionStateFlow = MutableStateFlow<CaptionState>(CaptionState.NoCaption)
+ /** Observer for app handle state changes. */
+ val captionStateFlow: StateFlow<CaptionState> = _captionStateFlow
+
+ /** Notifies [captionStateFlow] if there is a change to caption state. */
+ fun notifyCaptionChanged(captionState: CaptionState) {
+ _captionStateFlow.value = captionState
+ }
+}
+
+/**
+ * Represents the current status of the caption.
+ *
+ * It can be one of three options:
+ * * [AppHandle]: Indicating that there is at least one visible app handle on the screen.
+ * * [AppHeader]: Indicating that there is at least one visible app chip on the screen.
+ * * [NoCaption]: Signifying that no caption handle is currently visible on the device.
+ */
+sealed class CaptionState {
+ data class AppHandle(
+ val runningTaskInfo: RunningTaskInfo,
+ val isHandleMenuExpanded: Boolean,
+ val globalAppHandleBounds: Rect
+ ) : CaptionState()
+
+ data class AppHeader(
+ val runningTaskInfo: RunningTaskInfo,
+ val isHeaderMenuExpanded: Boolean,
+ val globalAppChipBounds: Rect
+ ) : CaptionState()
+
+ data object NoCaption : CaptionState()
+}
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 5a905cf..e8eb10c 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
@@ -2456,6 +2456,7 @@
final StageChangeRecord record = new StageChangeRecord();
final int transitType = info.getType();
TransitionInfo.Change pipChange = null;
+ int closingSplitTaskId = -1;
for (int iC = 0; iC < info.getChanges().size(); ++iC) {
final TransitionInfo.Change change = info.getChanges().get(iC);
if (change.getMode() == TRANSIT_CHANGE
@@ -2516,21 +2517,31 @@
+ " with " + taskInfo.taskId + " before startAnimation().");
}
}
+ if (isClosingType(change.getMode()) &&
+ getStageOfTask(change.getTaskInfo().taskId) != STAGE_TYPE_UNDEFINED) {
+ // If either one of the 2 stages is closing we're assuming we'll break split
+ closingSplitTaskId = change.getTaskInfo().taskId;
+ }
}
if (pipChange != null) {
TransitionInfo.Change pipReplacingChange = getPipReplacingChange(info, pipChange,
mMainStage.mRootTaskInfo.taskId, mSideStage.mRootTaskInfo.taskId,
getSplitItemStage(pipChange.getLastParent()));
- if (pipReplacingChange != null) {
+ boolean keepSplitWithPip = pipReplacingChange != null && closingSplitTaskId == -1;
+ if (keepSplitWithPip) {
// Set an enter transition for when startAnimation gets called again
mSplitTransitions.setEnterTransition(transition, /*remoteTransition*/ null,
TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE, /*resizeAnim*/ false);
+ } else {
+ int finalClosingTaskId = closingSplitTaskId;
+ mRecentTasks.ifPresent(recentTasks ->
+ recentTasks.removeSplitPair(finalClosingTaskId));
+ logExit(EXIT_REASON_FULLSCREEN_REQUEST);
}
mMixedHandler.animatePendingEnterPipFromSplit(transition, info,
- startTransaction, finishTransaction, finishCallback,
- pipReplacingChange != null);
+ startTransaction, finishTransaction, finishCallback, keepSplitWithPip);
notifySplitAnimationFinished();
return true;
}
@@ -2821,8 +2832,12 @@
}
callbackWct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token, false);
mWindowDecorViewModel.ifPresent(viewModel -> {
- viewModel.onTaskInfoChanged(finalMainChild.getTaskInfo());
- viewModel.onTaskInfoChanged(finalSideChild.getTaskInfo());
+ if (finalMainChild != null) {
+ viewModel.onTaskInfoChanged(finalMainChild.getTaskInfo());
+ }
+ if (finalSideChild != null) {
+ viewModel.onTaskInfoChanged(finalSideChild.getTaskInfo());
+ }
});
mPausingTasks.clear();
});
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 02c818f..7ea0bd6 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
@@ -109,6 +109,7 @@
import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition;
import com.android.wm.shell.desktopmode.DesktopTasksLimiter;
import com.android.wm.shell.desktopmode.DesktopWallpaperActivity;
+import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository;
import com.android.wm.shell.freeform.FreeformTaskTransitionStarter;
import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
import com.android.wm.shell.shared.annotations.ShellMainThread;
@@ -164,7 +165,9 @@
private final InputManager mInputManager;
private final InteractionJankMonitor mInteractionJankMonitor;
private final MultiInstanceHelper mMultiInstanceHelper;
+ private final WindowDecorCaptionHandleRepository mWindowDecorCaptionHandleRepository;
private final Optional<DesktopTasksLimiter> mDesktopTasksLimiter;
+ private final AppHeaderViewHolder.Factory mAppHeaderViewHolderFactory;
private final WindowDecorViewHostSupplier mWindowDecorViewHostSupplier;
private boolean mTransitionDragActive;
@@ -234,6 +237,7 @@
AssistContentRequester assistContentRequester,
MultiInstanceHelper multiInstanceHelper,
Optional<DesktopTasksLimiter> desktopTasksLimiter,
+ WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository,
Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler,
WindowDecorViewHostSupplier windowDecorViewHostSupplier) {
this(
@@ -259,10 +263,12 @@
new DesktopModeWindowDecoration.Factory(),
new InputMonitorFactory(),
SurfaceControl.Transaction::new,
+ new AppHeaderViewHolder.Factory(),
rootTaskDisplayAreaOrganizer,
new SparseArray<>(),
interactionJankMonitor,
desktopTasksLimiter,
+ windowDecorCaptionHandleRepository,
activityOrientationChangeHandler,
new TaskPositionerFactory());
}
@@ -291,10 +297,12 @@
DesktopModeWindowDecoration.Factory desktopModeWindowDecorFactory,
InputMonitorFactory inputMonitorFactory,
Supplier<SurfaceControl.Transaction> transactionFactory,
+ AppHeaderViewHolder.Factory appHeaderViewHolderFactory,
RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
SparseArray<DesktopModeWindowDecoration> windowDecorByTaskId,
InteractionJankMonitor interactionJankMonitor,
Optional<DesktopTasksLimiter> desktopTasksLimiter,
+ WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository,
Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler,
TaskPositionerFactory taskPositionerFactory) {
mContext = context;
@@ -317,6 +325,7 @@
mDesktopModeWindowDecorFactory = desktopModeWindowDecorFactory;
mInputMonitorFactory = inputMonitorFactory;
mTransactionFactory = transactionFactory;
+ mAppHeaderViewHolderFactory = appHeaderViewHolderFactory;
mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer;
mGenericLinksParser = genericLinksParser;
mInputManager = mContext.getSystemService(InputManager.class);
@@ -325,6 +334,7 @@
com.android.internal.R.string.config_systemUi);
mInteractionJankMonitor = interactionJankMonitor;
mDesktopTasksLimiter = desktopTasksLimiter;
+ mWindowDecorCaptionHandleRepository = windowDecorCaptionHandleRepository;
mActivityOrientationChangeHandler = activityOrientationChangeHandler;
mAssistContentRequester = assistContentRequester;
mOnDisplayChangingListener = (displayId, fromRotation, toRotation, displayAreaInfo, t) -> {
@@ -1191,8 +1201,10 @@
: SPLIT_POSITION_TOP_OR_LEFT;
final RunningTaskInfo oppositeTaskInfo =
mSplitScreenController.getTaskInfo(oppositePosition);
- mWindowDecorByTaskId.get(oppositeTaskInfo.taskId)
- .disposeStatusBarInputLayer();
+ if (oppositeTaskInfo != null) {
+ mWindowDecorByTaskId.get(oppositeTaskInfo.taskId)
+ .disposeStatusBarInputLayer();
+ }
}
}
mMoveToDesktopAnimator = null;
@@ -1375,10 +1387,12 @@
mBgExecutor,
mMainChoreographer,
mSyncQueue,
+ mAppHeaderViewHolderFactory,
mRootTaskDisplayAreaOrganizer,
mGenericLinksParser,
mAssistContentRequester,
mMultiInstanceHelper,
+ mWindowDecorCaptionHandleRepository,
mWindowDecorViewHostSupplier);
mWindowDecorByTaskId.put(taskInfo.taskId, windowDecoration);
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 16036be..10e4a39 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
@@ -28,6 +28,7 @@
import static android.window.flags.DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION_ALWAYS;
import static com.android.launcher3.icons.BaseIconFactory.MODE_DEFAULT;
+import static com.android.wm.shell.shared.desktopmode.DesktopModeStatus.canEnterDesktopMode;
import static com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getFineResizeCornerSize;
@@ -85,6 +86,8 @@
import com.android.wm.shell.common.MultiInstanceHelper;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.desktopmode.CaptionState;
+import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository;
import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
import com.android.wm.shell.shared.desktopmode.DesktopModeFlags;
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
@@ -165,6 +168,7 @@
private ExclusionRegionListener mExclusionRegionListener;
+ private final AppHeaderViewHolder.Factory mAppHeaderViewHolderFactory;
private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer;
private final MaximizeMenuFactory mMaximizeMenuFactory;
private final HandleMenuFactory mHandleMenuFactory;
@@ -181,6 +185,7 @@
private final Runnable mCloseMaximizeWindowRunnable = this::closeMaximizeMenu;
private final Runnable mCapturedLinkExpiredRunnable = this::onCapturedLinkExpired;
private final MultiInstanceHelper mMultiInstanceHelper;
+ private final WindowDecorCaptionHandleRepository mWindowDecorCaptionHandleRepository;
DesktopModeWindowDecoration(
Context context,
@@ -194,20 +199,24 @@
@ShellBackgroundThread ShellExecutor bgExecutor,
Choreographer choreographer,
SyncTransactionQueue syncQueue,
+ AppHeaderViewHolder.Factory appHeaderViewHolderFactory,
RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
AppToWebGenericLinksParser genericLinksParser,
AssistContentRequester assistContentRequester,
MultiInstanceHelper multiInstanceHelper,
+ WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository,
WindowDecorViewHostSupplier windowDecorViewHostSupplier) {
this (context, userContext, displayController, splitScreenController, taskOrganizer,
taskInfo, taskSurface, handler, bgExecutor, choreographer, syncQueue,
- rootTaskDisplayAreaOrganizer, genericLinksParser, assistContentRequester,
+ appHeaderViewHolderFactory, rootTaskDisplayAreaOrganizer, genericLinksParser,
+ assistContentRequester,
SurfaceControl.Builder::new, SurfaceControl.Transaction::new,
WindowContainerTransaction::new, SurfaceControl::new, new WindowManagerWrapper(
context.getSystemService(WindowManager.class)),
new SurfaceControlViewHostFactory() {}, windowDecorViewHostSupplier,
DefaultMaximizeMenuFactory.INSTANCE,
- DefaultHandleMenuFactory.INSTANCE, multiInstanceHelper);
+ DefaultHandleMenuFactory.INSTANCE, multiInstanceHelper,
+ windowDecorCaptionHandleRepository);
}
DesktopModeWindowDecoration(
@@ -222,6 +231,7 @@
@ShellBackgroundThread ShellExecutor bgExecutor,
Choreographer choreographer,
SyncTransactionQueue syncQueue,
+ AppHeaderViewHolder.Factory appHeaderViewHolderFactory,
RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
AppToWebGenericLinksParser genericLinksParser,
AssistContentRequester assistContentRequester,
@@ -234,7 +244,8 @@
WindowDecorViewHostSupplier windowDecorViewHostSupplier,
MaximizeMenuFactory maximizeMenuFactory,
HandleMenuFactory handleMenuFactory,
- MultiInstanceHelper multiInstanceHelper) {
+ MultiInstanceHelper multiInstanceHelper,
+ WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository) {
super(context, userContext, displayController, taskOrganizer, taskInfo, taskSurface,
surfaceControlBuilderSupplier, surfaceControlTransactionSupplier,
windowContainerTransactionSupplier, surfaceControlSupplier,
@@ -244,6 +255,7 @@
mBgExecutor = bgExecutor;
mChoreographer = choreographer;
mSyncQueue = syncQueue;
+ mAppHeaderViewHolderFactory = appHeaderViewHolderFactory;
mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer;
mGenericLinksParser = genericLinksParser;
mAssistContentRequester = assistContentRequester;
@@ -251,6 +263,7 @@
mHandleMenuFactory = handleMenuFactory;
mMultiInstanceHelper = multiInstanceHelper;
mWindowManagerWrapper = windowManagerWrapper;
+ mWindowDecorCaptionHandleRepository = windowDecorCaptionHandleRepository;
}
/**
@@ -383,6 +396,9 @@
if (mResult.mRootView == null) {
// This means something blocks the window decor from showing, e.g. the task is hidden.
// Nothing is set up in this case including the decoration surface.
+ if (canEnterDesktopMode(mContext) && Flags.enableDesktopWindowingAppHandleEducation()) {
+ notifyNoCaptionHandle();
+ }
disposeStatusBarInputLayer();
Trace.endSection(); // DesktopModeWindowDecoration#relayout
return;
@@ -398,6 +414,9 @@
position.set(determineHandlePosition());
}
Trace.beginSection("DesktopModeWindowDecoration#relayout-bindData");
+ if (canEnterDesktopMode(mContext) && Flags.enableDesktopWindowingAppHandleEducation()) {
+ notifyCaptionStateChanged();
+ }
mWindowDecorViewHolder.bindData(mTaskInfo,
position,
mResult.mCaptionWidth,
@@ -407,6 +426,7 @@
if (!mTaskInfo.isFocused) {
closeHandleMenu();
+ closeManageWindowsMenu();
closeMaximizeMenu();
}
updateDragResizeListener(oldDecorationSurface);
@@ -507,6 +527,67 @@
return taskInfo.isFreeform() && taskInfo.isResizeable;
}
+ private void notifyCaptionStateChanged() {
+ // TODO: b/366159408 - Ensure bounds sent with notification account for RTL mode.
+ if (!canEnterDesktopMode(mContext) || !Flags.enableDesktopWindowingAppHandleEducation()) {
+ return;
+ }
+ if (!isCaptionVisible()) {
+ notifyNoCaptionHandle();
+ } else if (isAppHandle(mWindowDecorViewHolder)) {
+ // App handle is visible since `mWindowDecorViewHolder` is of type
+ // [AppHandleViewHolder].
+ final CaptionState captionState = new CaptionState.AppHandle(mTaskInfo,
+ isHandleMenuActive(), getCurrentAppHandleBounds());
+ mWindowDecorCaptionHandleRepository.notifyCaptionChanged(captionState);
+ } else {
+ // App header is visible since `mWindowDecorViewHolder` is of type
+ // [AppHeaderViewHolder].
+ ((AppHeaderViewHolder) mWindowDecorViewHolder).runOnAppChipGlobalLayout(
+ () -> {
+ notifyAppChipStateChanged();
+ return Unit.INSTANCE;
+ });
+ }
+ }
+
+ private void notifyNoCaptionHandle() {
+ if (!canEnterDesktopMode(mContext) || !Flags.enableDesktopWindowingAppHandleEducation()) {
+ return;
+ }
+ mWindowDecorCaptionHandleRepository.notifyCaptionChanged(
+ CaptionState.NoCaption.INSTANCE);
+ }
+
+ private Rect getCurrentAppHandleBounds() {
+ return new Rect(
+ mResult.mCaptionX,
+ /* top= */0,
+ mResult.mCaptionX + mResult.mCaptionWidth,
+ mResult.mCaptionHeight);
+ }
+
+ private void notifyAppChipStateChanged() {
+ final Rect appChipPositionInWindow =
+ ((AppHeaderViewHolder) mWindowDecorViewHolder).getAppChipLocationInWindow();
+ final Rect taskBounds = mTaskInfo.configuration.windowConfiguration.getBounds();
+ final Rect appChipGlobalPosition = new Rect(
+ taskBounds.left + appChipPositionInWindow.left,
+ taskBounds.top + appChipPositionInWindow.top,
+ taskBounds.left + appChipPositionInWindow.right,
+ taskBounds.top + appChipPositionInWindow.bottom);
+ final CaptionState captionState = new CaptionState.AppHeader(
+ mTaskInfo,
+ isHandleMenuActive(),
+ appChipGlobalPosition);
+
+ mWindowDecorCaptionHandleRepository.notifyCaptionChanged(captionState);
+ }
+
+ private static boolean isDragResizable(ActivityManager.RunningTaskInfo taskInfo) {
+ return taskInfo.isFreeform() && taskInfo.isResizeable;
+ }
+
private void updateMaximizeMenu(SurfaceControl.Transaction startT) {
if (!isDragResizable(mTaskInfo, mContext) || !isMaximizeMenuActive()) {
return;
@@ -556,7 +637,7 @@
} else if (mRelayoutParams.mLayoutResId
== R.layout.desktop_mode_app_header) {
loadAppInfoIfNeeded();
- return new AppHeaderViewHolder(
+ return mAppHeaderViewHolderFactory.create(
mResult.mRootView,
mOnCaptionTouchListener,
mOnCaptionButtonClickListener,
@@ -994,7 +1075,7 @@
mAppIconBitmap,
mAppName,
mSplitScreenController,
- DesktopModeStatus.canEnterDesktopMode(mContext),
+ canEnterDesktopMode(mContext),
supportsMultiInstance,
shouldShowManageWindowsButton,
getBrowserLink(),
@@ -1027,6 +1108,9 @@
return Unit.INSTANCE;
}
);
+ if (canEnterDesktopMode(mContext) && Flags.enableDesktopWindowingAppHandleEducation()) {
+ notifyCaptionStateChanged();
+ }
mMinimumInstancesFound = false;
}
@@ -1067,7 +1151,10 @@
}
void closeManageWindowsMenu() {
- mManageWindowsMenu.close();
+ if (mManageWindowsMenu != null) {
+ mManageWindowsMenu.close();
+ }
+ mManageWindowsMenu = null;
}
private void updateGenericLink() {
@@ -1089,11 +1176,15 @@
mWindowDecorViewHolder.onHandleMenuClosed();
mHandleMenu.close();
mHandleMenu = null;
+ if (canEnterDesktopMode(mContext) && Flags.enableDesktopWindowingAppHandleEducation()) {
+ notifyCaptionStateChanged();
+ }
}
@Override
void releaseViews(WindowContainerTransaction wct) {
closeHandleMenu();
+ closeManageWindowsMenu();
closeMaximizeMenu();
super.releaseViews(wct);
}
@@ -1257,9 +1348,14 @@
public void close() {
closeDragResizeListener();
closeHandleMenu();
+ closeManageWindowsMenu();
mExclusionRegionListener.onExclusionRegionDismissed(mTaskInfo.taskId);
disposeResizeVeil();
disposeStatusBarInputLayer();
+ if (canEnterDesktopMode(mContext) && Flags.enableDesktopWindowingAppHandleEducation()) {
+ notifyNoCaptionHandle();
+ }
+
super.close();
}
@@ -1367,10 +1463,12 @@
@ShellBackgroundThread ShellExecutor bgExecutor,
Choreographer choreographer,
SyncTransactionQueue syncQueue,
+ AppHeaderViewHolder.Factory appHeaderViewHolderFactory,
RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
AppToWebGenericLinksParser genericLinksParser,
AssistContentRequester assistContentRequester,
MultiInstanceHelper multiInstanceHelper,
+ WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository,
WindowDecorViewHostSupplier windowDecorViewHostSupplier) {
return new DesktopModeWindowDecoration(
context,
@@ -1384,10 +1482,12 @@
bgExecutor,
choreographer,
syncQueue,
+ appHeaderViewHolderFactory,
rootTaskDisplayAreaOrganizer,
genericLinksParser,
assistContentRequester,
multiInstanceHelper,
+ windowDecorCaptionHandleRepository,
windowDecorViewHostSupplier);
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
index 6f3f411..6eb5cca 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
@@ -26,6 +26,8 @@
import android.graphics.Rect;
import android.os.Handler;
import android.os.IBinder;
+import android.os.Looper;
+import android.view.Choreographer;
import android.view.Surface;
import android.view.SurfaceControl;
import android.window.TransitionInfo;
@@ -124,6 +126,11 @@
@Override
public Rect onDragPositioningMove(float x, float y) {
+ if (Looper.myLooper() != mHandler.getLooper()) {
+ // This method must run on the shell main thread to use the correct Choreographer
+ // instance below.
+ throw new IllegalStateException("This method must run on the shell main thread.");
+ }
PointF delta = DragPositioningCallbackUtility.calculateDelta(x, y, mRepositionStartPoint);
if (isResizing() && DragPositioningCallbackUtility.changeBounds(mCtrlType,
mRepositionTaskBounds, mTaskBoundsAtDragStart, mStableBounds, delta,
@@ -141,6 +148,7 @@
final SurfaceControl.Transaction t = mTransactionSupplier.get();
DragPositioningCallbackUtility.setPositionOnDrag(mDesktopWindowDecoration,
mRepositionTaskBounds, mTaskBoundsAtDragStart, mRepositionStartPoint, t, x, y);
+ t.setFrameTimeline(Choreographer.getInstance().getVsyncId());
t.apply();
}
return new Rect(mRepositionTaskBounds);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
index 033d695..4a8cabc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
@@ -22,12 +22,14 @@
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.Point
+import android.graphics.Rect
import android.graphics.drawable.LayerDrawable
import android.graphics.drawable.RippleDrawable
import android.graphics.drawable.ShapeDrawable
import android.graphics.drawable.shapes.RoundRectShape
import android.view.View
import android.view.View.OnLongClickListener
+import android.view.ViewTreeObserver.OnGlobalLayoutListener
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.TextView
@@ -62,7 +64,7 @@
* finer controls such as a close window button and an "app info" section to pull up additional
* controls.
*/
-internal class AppHeaderViewHolder(
+class AppHeaderViewHolder(
rootView: View,
onCaptionTouchListener: View.OnTouchListener,
onCaptionButtonClickListener: View.OnClickListener,
@@ -279,6 +281,34 @@
maximizeButtonView.startHoverAnimation()
}
+ fun runOnAppChipGlobalLayout(runnable: () -> Unit) {
+ if (openMenuButton.isAttachedToWindow) {
+ // App chip is already inflated.
+ runnable()
+ return
+ }
+ // Wait for app chip to be inflated before notifying repository.
+ openMenuButton.viewTreeObserver.addOnGlobalLayoutListener(object :
+ OnGlobalLayoutListener {
+ override fun onGlobalLayout() {
+ runnable()
+ openMenuButton.viewTreeObserver.removeOnGlobalLayoutListener(this)
+ }
+ })
+ }
+
+ fun getAppChipLocationInWindow(): Rect {
+ val appChipBoundsInWindow = IntArray(2)
+ openMenuButton.getLocationInWindow(appChipBoundsInWindow)
+
+ return Rect(
+ /* left = */ appChipBoundsInWindow[0],
+ /* top = */ appChipBoundsInWindow[1],
+ /* right = */ appChipBoundsInWindow[0] + openMenuButton.width,
+ /* bottom = */ appChipBoundsInWindow[1] + openMenuButton.height
+ )
+ }
+
private fun getHeaderStyle(header: Header): HeaderStyle {
return HeaderStyle(
background = getHeaderBackground(header),
@@ -529,4 +559,26 @@
private const val LIGHT_THEME_UNFOCUSED_OPACITY = 166 // 65%
private const val FOCUSED_OPACITY = 255
}
+
+ class Factory {
+ fun create(
+ rootView: View,
+ onCaptionTouchListener: View.OnTouchListener,
+ onCaptionButtonClickListener: View.OnClickListener,
+ onLongClickListener: OnLongClickListener,
+ onCaptionGenericMotionListener: View.OnGenericMotionListener,
+ appName: CharSequence,
+ appIconBitmap: Bitmap,
+ onMaximizeHoverAnimationFinishedListener: () -> Unit,
+ ): AppHeaderViewHolder = AppHeaderViewHolder(
+ rootView,
+ onCaptionTouchListener,
+ onCaptionButtonClickListener,
+ onLongClickListener,
+ onCaptionGenericMotionListener,
+ appName,
+ appIconBitmap,
+ onMaximizeHoverAnimationFinishedListener,
+ )
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/WindowDecorationViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/WindowDecorationViewHolder.kt
index 2341b09..5ea55b3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/WindowDecorationViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/WindowDecorationViewHolder.kt
@@ -24,7 +24,7 @@
* Encapsulates the root [View] of a window decoration and its children to facilitate looking up
* children (via findViewById) and updating to the latest data from [RunningTaskInfo].
*/
-internal abstract class WindowDecorationViewHolder(rootView: View) {
+abstract class WindowDecorationViewHolder(rootView: View) {
val context: Context = rootView.context
/**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/DefaultWindowDecorViewHost.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/DefaultWindowDecorViewHost.kt
index 5156e47..139e679 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/DefaultWindowDecorViewHost.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/DefaultWindowDecorViewHost.kt
@@ -19,33 +19,51 @@
import android.content.res.Configuration
import android.view.Display
import android.view.SurfaceControl
+import android.view.SurfaceControlViewHost
import android.view.View
import android.view.WindowManager
+import android.view.WindowlessWindowManager
import androidx.tracing.Trace
import com.android.internal.annotations.VisibleForTesting
import com.android.wm.shell.shared.annotations.ShellMainThread
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
+typealias SurfaceControlViewHostFactory =
+ (Context, Display, WindowlessWindowManager, String) -> SurfaceControlViewHost
/**
- * A default implementation of [WindowDecorViewHost] backed by a [SurfaceControlViewHostAdapter].
+ * A default implementation of [WindowDecorViewHost] backed by a [SurfaceControlViewHost].
*
- * It supports asynchronously updating the view hierarchy using [updateViewAsync], in which
+ * It does not support swapping the root view added to the VRI of the [SurfaceControlViewHost], and
+ * any attempts to do will throw, which means that once a [View] is added using [updateView] or
+ * [updateViewAsync], only its properties and binding may be changed, its children views may be
+ * added, removed or changed and its [WindowManager.LayoutParams] may be changed.
+ * It also supports asynchronously updating the view hierarchy using [updateViewAsync], in which
* case the update work will be posted on the [ShellMainThread] with no delay.
*/
class DefaultWindowDecorViewHost(
- context: Context,
+ private val context: Context,
@ShellMainThread private val mainScope: CoroutineScope,
- display: Display,
- @VisibleForTesting val viewHostAdapter: SurfaceControlViewHostAdapter =
- SurfaceControlViewHostAdapter(context, display)
+ private val display: Display,
+ private val surfaceControlViewHostFactory: SurfaceControlViewHostFactory = { c, d, wwm, s ->
+ SurfaceControlViewHost(c, d, wwm, s)
+ }
) : WindowDecorViewHost {
+ private val rootSurface: SurfaceControl = SurfaceControl.Builder()
+ .setName("DefaultWindowDecorViewHost surface")
+ .setContainerLayer()
+ .setCallsite("DefaultWindowDecorViewHost#init")
+ .build()
+
+ private var wwm: WindowlessWindowManager? = null
+ @VisibleForTesting
+ var viewHost: SurfaceControlViewHost? = null
private var currentUpdateJob: Job? = null
override val surfaceControl: SurfaceControl
- get() = viewHostAdapter.rootSurface
+ get() = rootSurface
override fun updateView(
view: View,
@@ -74,7 +92,8 @@
override fun release(t: SurfaceControl.Transaction) {
clearCurrentUpdateJob()
- viewHostAdapter.release(t)
+ viewHost?.release()
+ t.remove(rootSurface)
}
private fun updateViewHost(
@@ -83,15 +102,45 @@
configuration: Configuration,
onDrawTransaction: SurfaceControl.Transaction?
) {
- viewHostAdapter.prepareViewHost(configuration)
- onDrawTransaction?.let {
- viewHostAdapter.applyTransactionOnDraw(it)
+ Trace.beginSection("DefaultWindowDecorViewHost#updateViewHost")
+ if (wwm == null) {
+ wwm = WindowlessWindowManager(configuration, rootSurface, null)
}
- viewHostAdapter.updateView(view, attrs)
+ requireWindowlessWindowManager().setConfiguration(configuration)
+ if (viewHost == null) {
+ viewHost = surfaceControlViewHostFactory.invoke(
+ context,
+ display,
+ requireWindowlessWindowManager(),
+ "DefaultWindowDecorViewHost#updateViewHost"
+ )
+ }
+ onDrawTransaction?.let {
+ requireViewHost().rootSurfaceControl.applyTransactionOnDraw(it)
+ }
+ if (requireViewHost().view == null) {
+ Trace.beginSection("DefaultWindowDecorViewHost#updateViewHost-setView")
+ requireViewHost().setView(view, attrs)
+ Trace.endSection()
+ } else {
+ check(requireViewHost().view == view) { "Changing view is not allowed" }
+ Trace.beginSection("DefaultWindowDecorViewHost#updateViewHost-relayout")
+ requireViewHost().relayout(attrs)
+ Trace.endSection()
+ }
+ Trace.endSection()
}
private fun clearCurrentUpdateJob() {
currentUpdateJob?.cancel()
currentUpdateJob = null
}
+
+ private fun requireWindowlessWindowManager(): WindowlessWindowManager {
+ return wwm ?: error("Expected non-null windowless window manager")
+ }
+
+ private fun requireViewHost(): SurfaceControlViewHost {
+ return viewHost ?: error("Expected non-null view host")
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/PooledWindowDecorViewHostSupplier.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/PooledWindowDecorViewHostSupplier.kt
deleted file mode 100644
index b04188f..0000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/PooledWindowDecorViewHostSupplier.kt
+++ /dev/null
@@ -1,105 +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.wm.shell.windowdecor.viewhost
-
-import android.content.Context
-import android.os.Trace
-import android.util.Pools
-import android.view.Display
-import android.view.SurfaceControl
-import com.android.wm.shell.shared.annotations.ShellMainThread
-import com.android.wm.shell.sysui.ShellInit
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.launch
-
-/**
- * A [WindowDecorViewHostSupplier] backed by a pool to allow recycling view hosts which may be
- * expensive to recreate for each new/updated window decoration.
- *
- * Callers can obtain [ReusableWindowDecorViewHost] using [acquire], which will return a pooled
- * object if available, or create a new instance and return it if needed. When done using a
- * [ReusableWindowDecorViewHost], it must be released using [release] to allow it to be sent back
- * into the pool and reused later on.
- *
- * This class also supports pre-warming [ReusableWindowDecorViewHost] instances, which will be put
- * into the pool immediately after creation.
- */
-class PooledWindowDecorViewHostSupplier(
- private val context: Context,
- @ShellMainThread private val mainScope: CoroutineScope,
- shellInit: ShellInit,
- private val viewHostFactory: ReusableWindowDecorViewHost.Factory =
- ReusableWindowDecorViewHost.DefaultFactory,
- maxPoolSize: Int,
- private val preWarmSize: Int,
-) : WindowDecorViewHostSupplier<ReusableWindowDecorViewHost> {
-
- private val pool: Pools.Pool<ReusableWindowDecorViewHost> = Pools.SynchronizedPool(maxPoolSize)
- private var nextDecorViewHostId = 0
-
- init {
- require(preWarmSize <= maxPoolSize) { "Pre-warm size should not exceed pool size" }
- shellInit.addInitCallback(this::onShellInit, this)
- }
-
- private fun onShellInit() {
- if (preWarmSize <= 0) {
- return
- }
- preWarmViewHosts(preWarmSize)
- }
-
- private fun preWarmViewHosts(preWarmSize: Int) {
- mainScope.launch {
- // Applying isn't needed, as the surface was never actually shown.
- val t = SurfaceControl.Transaction()
- repeat(preWarmSize) {
- val warmedViewHost = create(context, context.display).apply {
- warmUp()
- }
- // Put the warmed view host in the pool by releasing it.
- release(warmedViewHost, t)
- }
- }
- }
-
- override fun acquire(context: Context, display: Display): ReusableWindowDecorViewHost {
- val reusedDecorViewHost = pool.acquire()
- if (reusedDecorViewHost != null) {
- return reusedDecorViewHost
- }
- Trace.beginSection("WindowDecorViewHostPool#acquire-new")
- val newDecorViewHost = create(context, display)
- Trace.endSection()
- return newDecorViewHost
- }
-
- override fun release(viewHost: ReusableWindowDecorViewHost, t: SurfaceControl.Transaction) {
- val cached = pool.release(viewHost)
- if (!cached) {
- viewHost.release(t)
- }
- }
-
- private fun create(context: Context, display: Display): ReusableWindowDecorViewHost {
- return viewHostFactory.create(
- context,
- mainScope,
- display,
- nextDecorViewHostId++
- )
- }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/ReusableWindowDecorViewHost.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/ReusableWindowDecorViewHost.kt
deleted file mode 100644
index 64536d1..0000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/ReusableWindowDecorViewHost.kt
+++ /dev/null
@@ -1,161 +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.wm.shell.windowdecor.viewhost
-
-import android.content.Context
-import android.content.res.Configuration
-import android.graphics.PixelFormat
-import android.os.Trace
-import android.view.Display
-import android.view.SurfaceControl
-import android.view.SurfaceControlViewHost
-import android.view.View
-import android.view.WindowManager
-import android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
-import android.view.WindowManager.LayoutParams.FLAG_SPLIT_TOUCH
-import android.view.WindowManager.LayoutParams.TYPE_APPLICATION
-import android.widget.FrameLayout
-import com.android.internal.annotations.VisibleForTesting
-import com.android.wm.shell.shared.annotations.ShellMainThread
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.launch
-
-/**
- * An implementation of [WindowDecorViewHost] that supports:
- * 1) Replacing the root [View], meaning [WindowDecorViewHost.updateView] maybe be
- * called with different [View] instances. This is useful when reusing [WindowDecorViewHost]s
- * instances for vastly different view hierarchies, such as Desktop Windowing's App Handles and
- * App Headers.
- * 2) Pre-warming of the underlying [SurfaceControlViewHost]s. Useful because their creation and
- * first root view assignment are expensive, which is undesirable in latency-sensitive code
- * paths like during a shell transition.
- */
-class ReusableWindowDecorViewHost(
- private val context: Context,
- @ShellMainThread private val mainScope: CoroutineScope,
- display: Display,
- val id: Int,
- @VisibleForTesting val viewHostAdapter: SurfaceControlViewHostAdapter =
- SurfaceControlViewHostAdapter(context, display)
-) : WindowDecorViewHost, Warmable {
-
- @VisibleForTesting
- val rootView = FrameLayout(context)
-
- private var currentUpdateJob: Job? = null
-
- override val surfaceControl: SurfaceControl
- get() = viewHostAdapter.rootSurface
-
- override fun warmUp() {
- if (viewHostAdapter.isInitialized()) {
- // Already warmed up.
- return
- }
- Trace.beginSection("$TAG#warmUp")
- viewHostAdapter.prepareViewHost(context.resources.configuration)
- viewHostAdapter.updateView(
- rootView,
- WindowManager.LayoutParams(
- 0 /* width*/,
- 0 /* height */,
- TYPE_APPLICATION,
- FLAG_NOT_FOCUSABLE or FLAG_SPLIT_TOUCH,
- PixelFormat.TRANSPARENT
- ).apply {
- setTitle("View root of $TAG#$id")
- setTrustedOverlay()
- }
- )
- Trace.endSection()
- }
-
- override fun updateView(
- view: View,
- attrs: WindowManager.LayoutParams,
- configuration: Configuration,
- onDrawTransaction: SurfaceControl.Transaction?
- ) {
- clearCurrentUpdateJob()
- updateViewHost(view, attrs, configuration, onDrawTransaction)
- }
-
- override fun updateViewAsync(
- view: View,
- attrs: WindowManager.LayoutParams,
- configuration: Configuration
- ) {
- clearCurrentUpdateJob()
- currentUpdateJob = mainScope.launch {
- updateViewHost(view, attrs, configuration, onDrawTransaction = null)
- }
- }
-
- override fun release(t: SurfaceControl.Transaction) {
- clearCurrentUpdateJob()
- viewHostAdapter.release(t)
- }
-
- private fun updateViewHost(
- view: View,
- attrs: WindowManager.LayoutParams,
- configuration: Configuration,
- onDrawTransaction: SurfaceControl.Transaction?
- ) {
- viewHostAdapter.prepareViewHost(configuration)
- onDrawTransaction?.let {
- viewHostAdapter.applyTransactionOnDraw(it)
- }
- rootView.removeAllViews()
- rootView.addView(view)
- viewHostAdapter.updateView(rootView, attrs)
- }
-
- private fun clearCurrentUpdateJob() {
- currentUpdateJob?.cancel()
- currentUpdateJob = null
- }
-
- interface Factory {
- fun create(
- context: Context,
- @ShellMainThread mainScope: CoroutineScope,
- display: Display,
- id: Int
- ): ReusableWindowDecorViewHost
- }
-
- object DefaultFactory : Factory {
- override fun create(
- context: Context,
- @ShellMainThread mainScope: CoroutineScope,
- display: Display,
- id: Int
- ): ReusableWindowDecorViewHost {
- return ReusableWindowDecorViewHost(
- context,
- mainScope,
- display,
- id
- )
- }
- }
-
- companion object {
- private const val TAG = "ReusableWindowDecorViewHost"
- }
-}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/SurfaceControlViewHostAdapter.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/SurfaceControlViewHostAdapter.kt
deleted file mode 100644
index a54c9ba..0000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/SurfaceControlViewHostAdapter.kt
+++ /dev/null
@@ -1,111 +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.wm.shell.windowdecor.viewhost
-
-import android.content.Context
-import android.content.res.Configuration
-import android.view.AttachedSurfaceControl
-import android.view.Display
-import android.view.SurfaceControl
-import android.view.SurfaceControlViewHost
-import android.view.View
-import android.view.WindowManager
-import android.view.WindowlessWindowManager
-import androidx.tracing.Trace
-import com.android.internal.annotations.VisibleForTesting
-typealias SurfaceControlViewHostFactory =
- (Context, Display, WindowlessWindowManager, String) -> SurfaceControlViewHost
-
-/**
- * Adapter for a [SurfaceControlViewHost] and its backing [SurfaceControl].
- *
- * It does not support swapping the root view added to the VRI of the [SurfaceControlViewHost], and
- * any attempts to do will throw, which means that once a [View] is added using [updateView], only
- * its properties and binding may be changed, its children views may be added, removed or changed
- * and its [WindowManager.LayoutParams] may be changed.
- */
-class SurfaceControlViewHostAdapter(
- private val context: Context,
- private val display: Display,
- private val surfaceControlViewHostFactory: SurfaceControlViewHostFactory = { c, d, wwm, s ->
- SurfaceControlViewHost(c, d, wwm, s)
- }
-) {
- val rootSurface: SurfaceControl = SurfaceControl.Builder()
- .setName("SurfaceControlViewHostAdapter surface")
- .setContainerLayer()
- .setCallsite("SurfaceControlViewHostAdapter#init")
- .build()
-
- private var wwm: WindowlessWindowManager? = null
- @VisibleForTesting
- var viewHost: SurfaceControlViewHost? = null
-
- /** Initialize the [SurfaceControlViewHost] if needed. */
- fun prepareViewHost(configuration: Configuration) {
- if (wwm == null) {
- wwm = WindowlessWindowManager(configuration, rootSurface, null)
- }
- requireWindowlessWindowManager().setConfiguration(configuration)
- if (viewHost == null) {
- viewHost = surfaceControlViewHostFactory.invoke(
- context,
- display,
- requireWindowlessWindowManager(),
- "SurfaceControlViewHostAdapter#prepareViewHost"
- )
- }
- }
-
- /**
- * Request to apply the transaction atomically with the next draw of the view hierarchy.
- * See [AttachedSurfaceControl.applyTransactionOnDraw].
- */
- fun applyTransactionOnDraw(t: SurfaceControl.Transaction) {
- requireViewHost().rootSurfaceControl.applyTransactionOnDraw(t)
- }
-
- /** Update the view hierarchy of the view host. */
- fun updateView(view: View, attrs: WindowManager.LayoutParams) {
- if (requireViewHost().view == null) {
- Trace.beginSection("SurfaceControlViewHostAdapter#updateView-setView")
- requireViewHost().setView(view, attrs)
- Trace.endSection()
- } else {
- check(requireViewHost().view == view) { "Changing view is not allowed" }
- Trace.beginSection("SurfaceControlViewHostAdapter#updateView-relayout")
- requireViewHost().relayout(attrs)
- Trace.endSection()
- }
- }
-
- /** Release the view host and remove the backing surface. */
- fun release(t: SurfaceControl.Transaction) {
- viewHost?.release()
- t.remove(rootSurface)
- }
-
- /** Whether the view host has had a view hierarchy set. */
- fun isInitialized(): Boolean = viewHost?.view != null
-
- private fun requireWindowlessWindowManager(): WindowlessWindowManager {
- return wwm ?: error("Expected non-null windowless window manager")
- }
-
- private fun requireViewHost(): SurfaceControlViewHost {
- return viewHost ?: error("Expected non-null view host")
- }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/Warmable.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/Warmable.kt
deleted file mode 100644
index 0df9bfa..0000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/Warmable.kt
+++ /dev/null
@@ -1,23 +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.wm.shell.windowdecor.viewhost
-
-/**
- * An interface for an object that can be warmed up before it's needed.
- */
-interface Warmable {
- fun warmUp()
-}
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 e610ebd..8f20841 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
@@ -1764,6 +1764,37 @@
}
@Test
+ fun handleRequest_freeformTask_relaunchTask_enforceDesktop_freeformDisplay_noWinModeChange() {
+ assumeTrue(ENABLE_SHELL_TRANSITIONS)
+ whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true)
+ val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
+ tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
+
+ val freeformTask = setUpFreeformTask()
+ markTaskHidden(freeformTask)
+ val wct = controller.handleRequest(Binder(), createTransition(freeformTask))
+
+ assertNotNull(wct, "should handle request")
+ assertFalse(wct.anyWindowingModeChange(freeformTask.token))
+ }
+
+ @Test
+ fun handleRequest_freeformTask_relaunchTask_enforceDesktop_fullscreenDisplay_becomesUndefined() {
+ assumeTrue(ENABLE_SHELL_TRANSITIONS)
+ whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true)
+ val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
+ tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
+
+ val freeformTask = setUpFreeformTask()
+ markTaskHidden(freeformTask)
+ val wct = controller.handleRequest(Binder(), createTransition(freeformTask))
+
+ assertNotNull(wct, "should handle request")
+ assertThat(wct.changes[freeformTask.token.asBinder()]?.windowingMode)
+ .isEqualTo(WINDOWING_MODE_UNDEFINED)
+ }
+
+ @Test
@DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
fun handleRequest_freeformTask_desktopWallpaperDisabled_freeformNotVisible_reorderedToTop() {
assumeTrue(ENABLE_SHELL_TRANSITIONS)
@@ -3493,6 +3524,14 @@
} ?: false
}
+private fun WindowContainerTransaction?.anyWindowingModeChange(
+ token: WindowContainerToken
+): Boolean {
+return this?.changes?.any { change ->
+ change.key == token.asBinder() && change.value.windowingMode >= 0
+} ?: false
+}
+
private fun createTaskInfo(id: Int) =
RecentTaskInfo().apply {
taskId = id
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepositoryTest.kt
new file mode 100644
index 0000000..e3caf2e
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepositoryTest.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.desktopmode
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
+import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
+import android.graphics.Rect
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class WindowDecorCaptionHandleRepositoryTest {
+ private lateinit var captionHandleRepository: WindowDecorCaptionHandleRepository
+
+ @Before
+ fun setUp() {
+ captionHandleRepository = WindowDecorCaptionHandleRepository()
+ }
+
+ @Test
+ fun initialState_noAction_returnsNoCaption() {
+ // Check the initial value of `captionStateFlow`.
+ assertThat(captionHandleRepository.captionStateFlow.value).isEqualTo(CaptionState.NoCaption)
+ }
+
+ @Test
+ fun notifyCaptionChange_toAppHandleVisible_updatesStateWithCorrectData() {
+ val taskInfo = createTaskInfo(WINDOWING_MODE_FULLSCREEN, GMAIL_PACKAGE_NAME)
+ val appHandleCaptionState =
+ CaptionState.AppHandle(
+ taskInfo, false, Rect(/* left= */ 0, /* top= */ 1, /* right= */ 2, /* bottom= */ 3))
+
+ captionHandleRepository.notifyCaptionChanged(appHandleCaptionState)
+
+ assertThat(captionHandleRepository.captionStateFlow.value).isEqualTo(appHandleCaptionState)
+ }
+
+ @Test
+ fun notifyCaptionChange_toAppChipVisible_updatesStateWithCorrectData() {
+ val taskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM, GMAIL_PACKAGE_NAME)
+ val appHeaderCaptionState =
+ CaptionState.AppHeader(
+ taskInfo, true, Rect(/* left= */ 0, /* top= */ 1, /* right= */ 2, /* bottom= */ 3))
+
+ captionHandleRepository.notifyCaptionChanged(appHeaderCaptionState)
+
+ assertThat(captionHandleRepository.captionStateFlow.value).isEqualTo(appHeaderCaptionState)
+ }
+
+ @Test
+ fun notifyCaptionChange_toNoCaption_updatesState() {
+ captionHandleRepository.notifyCaptionChanged(CaptionState.NoCaption)
+
+ assertThat(captionHandleRepository.captionStateFlow.value).isEqualTo(CaptionState.NoCaption)
+ }
+
+ private fun createTaskInfo(
+ deviceWindowingMode: Int = WINDOWING_MODE_UNDEFINED,
+ runningTaskPackageName: String = LAUNCHER_PACKAGE_NAME
+ ): RunningTaskInfo =
+ RunningTaskInfo().apply {
+ configuration.windowConfiguration.apply { windowingMode = deviceWindowingMode }
+ topActivityInfo?.apply { packageName = runningTaskPackageName }
+ }
+
+ private companion object {
+ const val GMAIL_PACKAGE_NAME = "com.google.android.gm"
+ const val LAUNCHER_PACKAGE_NAME = "com.google.android.apps.nexuslauncher"
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
index fec9e3e..aea14b9 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
@@ -332,6 +332,35 @@
}
@Test
+ public void testTransitionFilterTaskFragmentToken() {
+ final IBinder taskFragmentToken = new Binder();
+
+ TransitionFilter filter = new TransitionFilter();
+ filter.mRequirements =
+ new TransitionFilter.Requirement[]{new TransitionFilter.Requirement()};
+ filter.mRequirements[0].mModes = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT};
+ filter.mRequirements[0].mTaskFragmentToken = taskFragmentToken;
+
+ // Transition with the same token should match.
+ final TransitionInfo infoHasTaskFragmentToken = new TransitionInfoBuilder(TRANSIT_OPEN)
+ .addChange(TRANSIT_OPEN, taskFragmentToken).build();
+ assertTrue(filter.matches(infoHasTaskFragmentToken));
+
+ // Transition with a different token should not match.
+ final IBinder differentTaskFragmentToken = new Binder();
+ final TransitionInfo infoDifferentTaskFragmentToken =
+ new TransitionInfoBuilder(TRANSIT_OPEN)
+ .addChange(TRANSIT_OPEN, differentTaskFragmentToken).build();
+ assertFalse(filter.matches(infoDifferentTaskFragmentToken));
+
+ // Transition without a token should not match.
+ final TransitionInfo infoNoTaskFragmentToken = new TransitionInfoBuilder(TRANSIT_OPEN)
+ .addChange(TRANSIT_OPEN, createTaskInfo(
+ 1, WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD)).build();
+ assertFalse(filter.matches(infoNoTaskFragmentToken));
+ }
+
+ @Test
public void testTransitionFilterMultiRequirement() {
// filter that requires at-least one opening and one closing app
TransitionFilter filter = new TransitionFilter();
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/TransitionInfoBuilder.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/TransitionInfoBuilder.java
index b8939e6f..49ae182 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/TransitionInfoBuilder.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/TransitionInfoBuilder.java
@@ -20,8 +20,10 @@
import static org.mockito.Mockito.mock;
+import android.annotation.Nullable;
import android.app.ActivityManager;
import android.content.ComponentName;
+import android.os.IBinder;
import android.view.SurfaceControl;
import android.view.WindowManager;
import android.window.TransitionInfo;
@@ -51,21 +53,24 @@
}
public TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode,
- @TransitionInfo.ChangeFlags int flags, ActivityManager.RunningTaskInfo taskInfo,
- ComponentName activityComponent) {
+ @TransitionInfo.ChangeFlags int flags,
+ @Nullable ActivityManager.RunningTaskInfo taskInfo,
+ @Nullable ComponentName activityComponent, @Nullable IBinder taskFragmentToken) {
final TransitionInfo.Change change = new TransitionInfo.Change(
taskInfo != null ? taskInfo.token : null, createMockSurface(true /* valid */));
change.setMode(mode);
change.setFlags(flags);
change.setTaskInfo(taskInfo);
change.setActivityComponent(activityComponent);
+ change.setTaskFragmentToken(taskFragmentToken);
return addChange(change);
}
/** Add a change to the TransitionInfo */
public TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode,
@TransitionInfo.ChangeFlags int flags, ActivityManager.RunningTaskInfo taskInfo) {
- return addChange(mode, flags, taskInfo, null /* activityComponent */);
+ return addChange(mode, flags, taskInfo, null /* activityComponent */,
+ null /* taskFragmentToken */);
}
public TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode,
@@ -76,13 +81,21 @@
/** Add a change to the TransitionInfo */
public TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode,
ComponentName activityComponent) {
- return addChange(mode, TransitionInfo.FLAG_NONE, null /* taskinfo */, activityComponent);
+ return addChange(mode, TransitionInfo.FLAG_NONE, null /* taskinfo */, activityComponent,
+ null /* taskFragmentToken */);
}
public TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode) {
return addChange(mode, TransitionInfo.FLAG_NONE, null /* taskInfo */);
}
+ /** Add a change with a TaskFragment token to the TransitionInfo */
+ public TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode,
+ @Nullable IBinder taskFragmentToken) {
+ return addChange(mode, TransitionInfo.FLAG_NONE, null /* taskInfo */,
+ null /* activityComponent */, taskFragmentToken);
+ }
+
public TransitionInfoBuilder addChange(TransitionInfo.Change change) {
change.setDisplayId(DISPLAY_ID, DISPLAY_ID);
mInfo.addChange(change);
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 85bc7cc..ee2a41c 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
@@ -88,6 +88,7 @@
import com.android.wm.shell.desktopmode.DesktopTasksController
import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition
import com.android.wm.shell.desktopmode.DesktopTasksLimiter
+import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository
import com.android.wm.shell.freeform.FreeformTaskTransitionStarter
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource
@@ -98,6 +99,7 @@
import com.android.wm.shell.transition.Transitions
import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel.DesktopModeKeyguardChangeListener
import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel.DesktopModeOnInsetsChangedListener
+import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder
import com.android.wm.shell.windowdecor.viewhost.WindowDecorViewHostSupplier
import java.util.Optional
import java.util.function.Consumer
@@ -164,6 +166,7 @@
DesktopModeWindowDecorViewModel.InputMonitorFactory
@Mock private lateinit var mockShellController: ShellController
@Mock private lateinit var mockShellExecutor: ShellExecutor
+ @Mock private lateinit var mockAppHeaderViewHolderFactory: AppHeaderViewHolder.Factory
@Mock private lateinit var mockRootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
@Mock private lateinit var mockShellCommandHandler: ShellCommandHandler
@Mock private lateinit var mockWindowManager: IWindowManager
@@ -182,6 +185,7 @@
@Mock private lateinit var mockTaskPositionerFactory:
DesktopModeWindowDecorViewModel.TaskPositionerFactory
@Mock private lateinit var mockTaskPositioner: TaskPositioner
+ @Mock private lateinit var mockCaptionHandleRepository: WindowDecorCaptionHandleRepository
@Mock private lateinit var mockWindowDecorViewHostSupplier: WindowDecorViewHostSupplier<*>
private lateinit var spyContext: TestableContext
@@ -236,10 +240,12 @@
mockDesktopModeWindowDecorFactory,
mockInputMonitorFactory,
transactionFactory,
+ mockAppHeaderViewHolderFactory,
mockRootTaskDisplayAreaOrganizer,
windowDecorByTaskIdSpy,
mockInteractionJankMonitor,
Optional.of(mockTasksLimiter),
+ mockCaptionHandleRepository,
Optional.of(mockActivityOrientationChangeHandler),
mockTaskPositionerFactory
)
@@ -1211,7 +1217,7 @@
whenever(
mockDesktopModeWindowDecorFactory.create(
any(), any(), any(), any(), any(), eq(task), any(), any(), any(), any(), any(),
- any(), any(), any(), any(), any())
+ any(), 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 dff42da..a1867f3 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
@@ -19,9 +19,11 @@
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
+import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
import static android.view.InsetsSource.FLAG_FORCE_CONSUMING;
import static android.view.InsetsSource.FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR;
+import static android.view.WindowInsets.Type.statusBars;
import static android.view.WindowInsetsController.APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
@@ -38,6 +40,7 @@
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -55,6 +58,7 @@
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.PointF;
+import android.graphics.Rect;
import android.net.Uri;
import android.os.Handler;
import android.os.SystemProperties;
@@ -68,11 +72,13 @@
import android.view.Choreographer;
import android.view.Display;
import android.view.GestureDetector;
+import android.view.InsetsSource;
import android.view.InsetsState;
import android.view.MotionEvent;
import android.view.SurfaceControl;
import android.view.SurfaceControlViewHost;
import android.view.View;
+import android.view.WindowInsets;
import android.view.WindowManager;
import android.window.WindowContainerTransaction;
@@ -94,9 +100,12 @@
import com.android.wm.shell.common.MultiInstanceHelper;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.desktopmode.CaptionState;
+import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository;
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
import com.android.wm.shell.splitscreen.SplitScreenController;
import com.android.wm.shell.windowdecor.WindowDecoration.RelayoutParams;
+import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder;
import com.android.wm.shell.windowdecor.viewhost.WindowDecorViewHost;
import com.android.wm.shell.windowdecor.viewhost.WindowDecorViewHostSupplier;
@@ -153,6 +162,10 @@
@Mock
private SyncTransactionQueue mMockSyncQueue;
@Mock
+ private AppHeaderViewHolder.Factory mMockAppHeaderViewHolderFactory;
+ @Mock
+ private AppHeaderViewHolder mMockAppHeaderViewHolder;
+ @Mock
private RootTaskDisplayAreaOrganizer mMockRootTaskDisplayAreaOrganizer;
@Mock
private Supplier<SurfaceControl.Transaction> mMockTransactionSupplier;
@@ -192,6 +205,8 @@
private HandleMenuFactory mMockHandleMenuFactory;
@Mock
private MultiInstanceHelper mMockMultiInstanceHelper;
+ @Mock
+ private WindowDecorCaptionHandleRepository mMockCaptionHandleRepository;
@Captor
private ArgumentCaptor<Function1<Boolean, Unit>> mOnMaxMenuHoverChangeListener;
@Captor
@@ -245,6 +260,8 @@
when(mMockWindowDecorViewHostSupplier.acquire(any(), eq(defaultDisplay)))
.thenReturn(mMockWindowDecorViewHost);
when(mMockWindowDecorViewHost.getSurfaceControl()).thenReturn(mock(SurfaceControl.class));
+ when(mMockAppHeaderViewHolderFactory.create(any(), any(), any(), any(), any(), any(), any(),
+ any())).thenReturn(mMockAppHeaderViewHolder);
}
@After
@@ -838,6 +855,143 @@
assertFalse(decoration.isHandleMenuActive());
}
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+ public void notifyCaptionStateChanged_flagDisabled_doNoNotify() {
+ when(DesktopModeStatus.canEnterDesktopMode(mContext)).thenReturn(true);
+ final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+ when(mMockDisplayController.getInsetsState(taskInfo.displayId))
+ .thenReturn(createInsetsState(statusBars(), /* visible= */true));
+ final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
+ taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+
+ spyWindowDecor.relayout(taskInfo);
+
+ verify(mMockCaptionHandleRepository, never()).notifyCaptionChanged(any());
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+ public void notifyCaptionStateChanged_inFullscreenMode_notifiesAppHandleVisible() {
+ when(DesktopModeStatus.canEnterDesktopMode(mContext)).thenReturn(true);
+ final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+ when(mMockDisplayController.getInsetsState(taskInfo.displayId))
+ .thenReturn(createInsetsState(statusBars(), /* visible= */true));
+ final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
+ taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+ ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass(
+ CaptionState.class);
+
+ spyWindowDecor.relayout(taskInfo);
+
+ verify(mMockCaptionHandleRepository, atLeastOnce()).notifyCaptionChanged(
+ captionStateArgumentCaptor.capture());
+ assertThat(captionStateArgumentCaptor.getValue()).isInstanceOf(
+ CaptionState.AppHandle.class);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+ @Ignore("TODO(b/367235906): Due to MONITOR_INPUT permission error")
+ public void notifyCaptionStateChanged_inWindowingMode_notifiesAppHeaderVisible() {
+ when(DesktopModeStatus.canEnterDesktopMode(mContext)).thenReturn(true);
+ final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+ when(mMockDisplayController.getInsetsState(taskInfo.displayId))
+ .thenReturn(createInsetsState(statusBars(), /* visible= */true));
+ when(mMockAppHeaderViewHolder.getAppChipLocationInWindow()).thenReturn(
+ new Rect(/* left= */ 0, /* top= */ 1, /* right= */ 2, /* bottom= */ 3));
+ final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
+ taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
+ // Make non-resizable to avoid dealing with input-permissions (MONITOR_INPUT)
+ taskInfo.isResizeable = false;
+ ArgumentCaptor<Function0<Unit>> runnableArgumentCaptor = ArgumentCaptor.forClass(
+ Function0.class);
+ ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass(
+ CaptionState.class);
+
+ spyWindowDecor.relayout(taskInfo);
+ verify(mMockAppHeaderViewHolder, atLeastOnce()).runOnAppChipGlobalLayout(
+ runnableArgumentCaptor.capture());
+ runnableArgumentCaptor.getValue().invoke();
+
+ verify(mMockCaptionHandleRepository, atLeastOnce()).notifyCaptionChanged(
+ captionStateArgumentCaptor.capture());
+ assertThat(captionStateArgumentCaptor.getValue()).isInstanceOf(
+ CaptionState.AppHeader.class);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+ public void notifyCaptionStateChanged_taskNotVisible_notifiesNoCaptionVisible() {
+ when(DesktopModeStatus.canEnterDesktopMode(mContext)).thenReturn(true);
+ final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ false);
+ when(mMockDisplayController.getInsetsState(taskInfo.displayId))
+ .thenReturn(createInsetsState(statusBars(), /* visible= */true));
+ final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
+ taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_UNDEFINED);
+ ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass(
+ CaptionState.class);
+
+ spyWindowDecor.relayout(taskInfo);
+
+ verify(mMockCaptionHandleRepository, atLeastOnce()).notifyCaptionChanged(
+ captionStateArgumentCaptor.capture());
+ assertThat(captionStateArgumentCaptor.getValue()).isInstanceOf(
+ CaptionState.NoCaption.class);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+ public void notifyCaptionStateChanged_captionHandleExpanded_notifiesHandleMenuExpanded() {
+ when(DesktopModeStatus.canEnterDesktopMode(mContext)).thenReturn(true);
+ final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+ when(mMockDisplayController.getInsetsState(taskInfo.displayId))
+ .thenReturn(createInsetsState(statusBars(), /* visible= */true));
+ final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
+ taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+ ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass(
+ CaptionState.class);
+
+ spyWindowDecor.relayout(taskInfo);
+ createHandleMenu(spyWindowDecor);
+
+ verify(mMockCaptionHandleRepository, atLeastOnce()).notifyCaptionChanged(
+ captionStateArgumentCaptor.capture());
+ assertThat(captionStateArgumentCaptor.getValue()).isInstanceOf(
+ CaptionState.AppHandle.class);
+ assertThat(
+ ((CaptionState.AppHandle) captionStateArgumentCaptor.getValue())
+ .isHandleMenuExpanded()).isEqualTo(
+ true);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+ public void notifyCaptionStateChanged_captionHandleClosed_notifiesHandleMenuClosed() {
+ when(DesktopModeStatus.canEnterDesktopMode(mContext)).thenReturn(true);
+ final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+ when(mMockDisplayController.getInsetsState(taskInfo.displayId))
+ .thenReturn(createInsetsState(statusBars(), /* visible= */true));
+ final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
+ taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+ ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass(
+ CaptionState.class);
+
+ spyWindowDecor.relayout(taskInfo);
+ createHandleMenu(spyWindowDecor);
+ spyWindowDecor.closeHandleMenu();
+
+ verify(mMockCaptionHandleRepository, atLeastOnce()).notifyCaptionChanged(
+ captionStateArgumentCaptor.capture());
+ assertThat(captionStateArgumentCaptor.getValue()).isInstanceOf(
+ CaptionState.AppHandle.class);
+ assertThat(
+ ((CaptionState.AppHandle) captionStateArgumentCaptor.getValue())
+ .isHandleMenuExpanded()).isEqualTo(
+ false);
+
+ }
+
private void verifyHandleMenuCreated(@Nullable Uri uri) {
verify(mMockHandleMenuFactory).create(any(), any(), anyInt(), any(), any(),
any(), anyBoolean(), anyBoolean(), anyBoolean(), eq(uri), anyInt(),
@@ -906,12 +1060,13 @@
final DesktopModeWindowDecoration windowDecor = new DesktopModeWindowDecoration(mContext,
mContext, mMockDisplayController, mMockSplitScreenController,
mMockShellTaskOrganizer, taskInfo, mMockSurfaceControl, mMockHandler, mBgExecutor,
- mMockChoreographer, mMockSyncQueue, mMockRootTaskDisplayAreaOrganizer,
+ mMockChoreographer, mMockSyncQueue, mMockAppHeaderViewHolderFactory,
+ mMockRootTaskDisplayAreaOrganizer,
mMockGenericLinksParser, mMockAssistContentRequester, SurfaceControl.Builder::new,
mMockTransactionSupplier, WindowContainerTransaction::new, SurfaceControl::new,
new WindowManagerWrapper(mMockWindowManager), mMockSurfaceControlViewHostFactory,
mMockWindowDecorViewHostSupplier, maximizeMenuFactory, mMockHandleMenuFactory,
- mMockMultiInstanceHelper);
+ mMockMultiInstanceHelper, mMockCaptionHandleRepository);
windowDecor.setCaptionListeners(mMockTouchEventListener, mMockTouchEventListener,
mMockTouchEventListener, mMockTouchEventListener);
windowDecor.setExclusionRegionListener(mMockExclusionRegionListener);
@@ -951,6 +1106,14 @@
!= 0;
}
+ private InsetsState createInsetsState(@WindowInsets.Type.InsetsType int type, boolean visible) {
+ final InsetsState state = new InsetsState();
+ final InsetsSource source = new InsetsSource(/* id= */0, type);
+ source.setVisible(visible);
+ state.addSource(source);
+ return state;
+ }
+
private static class TestTouchEventListener extends GestureDetector.SimpleOnGestureListener
implements View.OnClickListener, View.OnTouchListener, View.OnLongClickListener,
View.OnGenericMotionListener, DragDetector.MotionEventHandler {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
index ab41d9c..1273ee8 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
@@ -23,6 +23,7 @@
import android.graphics.Rect
import android.os.Handler
import android.os.IBinder
+import android.os.Looper
import android.testing.AndroidTestingRunner
import android.view.Display
import android.view.Surface.ROTATION_0
@@ -34,6 +35,7 @@
import android.window.TransitionInfo
import android.window.WindowContainerToken
import androidx.test.filters.SmallTest
+import androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread
import com.android.internal.jank.InteractionJankMonitor
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.ShellTestCase
@@ -108,8 +110,7 @@
private lateinit var mockResources: Resources
@Mock
private lateinit var mockInteractionJankMonitor: InteractionJankMonitor
- @Mock
- private lateinit var mockHandler: Handler
+ private val mainHandler = Handler(Looper.getMainLooper())
private lateinit var taskPositioner: VeiledResizeTaskPositioner
@@ -159,12 +160,12 @@
mockTransactionFactory,
mockTransitions,
mockInteractionJankMonitor,
- mockHandler,
+ mainHandler,
)
}
@Test
- fun testDragResize_noMove_doesNotShowResizeVeil() {
+ fun testDragResize_noMove_doesNotShowResizeVeil() = runOnUiThread {
taskPositioner.onDragPositioningStart(
CTRL_TYPE_TOP or CTRL_TYPE_RIGHT,
STARTING_BOUNDS.left.toFloat(),
@@ -176,6 +177,7 @@
STARTING_BOUNDS.left.toFloat(),
STARTING_BOUNDS.top.toFloat()
)
+
verify(mockTransitions, never()).startTransition(eq(TRANSIT_CHANGE), argThat { wct ->
return@argThat wct.changes.any { (token, change) ->
token == taskBinder &&
@@ -186,7 +188,7 @@
}
@Test
- fun testDragResize_movesTask_doesNotShowResizeVeil() {
+ fun testDragResize_movesTask_doesNotShowResizeVeil() = runOnUiThread {
taskPositioner.onDragPositioningStart(
CTRL_TYPE_UNDEFINED,
STARTING_BOUNDS.left.toFloat(),
@@ -221,7 +223,7 @@
}
@Test
- fun testDragResize_resize_boundsUpdateOnEnd() {
+ fun testDragResize_resize_boundsUpdateOnEnd() = runOnUiThread {
taskPositioner.onDragPositioningStart(
CTRL_TYPE_RIGHT or CTRL_TYPE_TOP,
STARTING_BOUNDS.right.toFloat(),
@@ -262,7 +264,7 @@
}
@Test
- fun testDragResize_noEffectiveMove_skipsTransactionOnEnd() {
+ fun testDragResize_noEffectiveMove_skipsTransactionOnEnd() = runOnUiThread {
taskPositioner.onDragPositioningStart(
CTRL_TYPE_TOP or CTRL_TYPE_RIGHT,
STARTING_BOUNDS.left.toFloat(),
@@ -294,9 +296,8 @@
})
}
-
@Test
- fun testDragResize_drag_setBoundsNotRunIfDragEndsInDisallowedEndArea() {
+ fun testDragResize_drag_setBoundsNotRunIfDragEndsInDisallowedEndArea() = runOnUiThread {
taskPositioner.onDragPositioningStart(
CTRL_TYPE_UNDEFINED, // drag
STARTING_BOUNDS.left.toFloat(),
@@ -321,7 +322,7 @@
}
@Test
- fun testDragResize_resize_resizingTaskReorderedToTopWhenNotFocused() {
+ fun testDragResize_resize_resizingTaskReorderedToTopWhenNotFocused() = runOnUiThread {
mockDesktopWindowDecoration.mTaskInfo.isFocused = false
taskPositioner.onDragPositioningStart(
CTRL_TYPE_RIGHT, // Resize right
@@ -337,7 +338,7 @@
}
@Test
- fun testDragResize_resize_resizingTaskNotReorderedToTopWhenFocused() {
+ fun testDragResize_resize_resizingTaskNotReorderedToTopWhenFocused() = runOnUiThread {
mockDesktopWindowDecoration.mTaskInfo.isFocused = true
taskPositioner.onDragPositioningStart(
CTRL_TYPE_RIGHT, // Resize right
@@ -353,7 +354,7 @@
}
@Test
- fun testDragResize_drag_draggedTaskNotReorderedToTop() {
+ fun testDragResize_drag_draggedTaskNotReorderedToTop() = runOnUiThread {
mockDesktopWindowDecoration.mTaskInfo.isFocused = false
taskPositioner.onDragPositioningStart(
CTRL_TYPE_UNDEFINED, // drag
@@ -370,7 +371,7 @@
}
@Test
- fun testDragResize_drag_updatesStableBoundsOnRotate() {
+ fun testDragResize_drag_updatesStableBoundsOnRotate() = runOnUiThread {
// Test landscape stable bounds
performDrag(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.bottom.toFloat(),
STARTING_BOUNDS.right.toFloat() + 2000, STARTING_BOUNDS.bottom.toFloat() + 2000,
@@ -416,7 +417,7 @@
}
@Test
- fun testIsResizingOrAnimatingResizeSet() {
+ fun testIsResizingOrAnimatingResizeSet() = runOnUiThread {
Assert.assertFalse(taskPositioner.isResizingOrAnimating)
taskPositioner.onDragPositioningStart(
@@ -443,7 +444,7 @@
}
@Test
- fun testIsResizingOrAnimatingResizeResetAfterStartAnimation() {
+ fun testIsResizingOrAnimatingResizeResetAfterStartAnimation() = runOnUiThread {
performDrag(
STARTING_BOUNDS.left.toFloat(), STARTING_BOUNDS.top.toFloat(),
STARTING_BOUNDS.left.toFloat() - 20, STARTING_BOUNDS.top.toFloat() - 20,
@@ -457,7 +458,7 @@
}
@Test
- fun testStartAnimation_useEndRelOffset() {
+ fun testStartAnimation_useEndRelOffset() = runOnUiThread {
val changeMock = mock(TransitionInfo.Change::class.java)
val startTransaction = mock(Transaction::class.java)
val finishTransaction = mock(Transaction::class.java)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/DefaultWindowDecorViewHostTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/DefaultWindowDecorViewHostTest.kt
index 1b0b7d9..1b2ce9e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/DefaultWindowDecorViewHostTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/DefaultWindowDecorViewHostTest.kt
@@ -18,6 +18,7 @@
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.view.SurfaceControl
+import android.view.SurfaceControlViewHost
import android.view.View
import android.view.WindowManager
import androidx.test.filters.SmallTest
@@ -27,6 +28,7 @@
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertThrows
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.mock
@@ -57,8 +59,54 @@
onDrawTransaction = null
)
- assertThat(windowDecorViewHost.viewHostAdapter.isInitialized()).isTrue()
- assertThat(windowDecorViewHost.view()).isEqualTo(view)
+ assertThat(windowDecorViewHost.viewHost).isNotNull()
+ assertThat(windowDecorViewHost.viewHost!!.view).isEqualTo(view)
+ }
+
+ @Test
+ fun updateView_alreadyLaidOut_relayouts() = runTest {
+ val windowDecorViewHost = createDefaultViewHost()
+ val view = View(context)
+ windowDecorViewHost.updateView(
+ view = view,
+ attrs = WindowManager.LayoutParams(100, 100),
+ configuration = context.resources.configuration,
+ onDrawTransaction = null
+ )
+
+ val otherParams = WindowManager.LayoutParams(200, 200)
+ windowDecorViewHost.updateView(
+ view = view,
+ attrs = otherParams,
+ configuration = context.resources.configuration,
+ onDrawTransaction = null
+ )
+
+ assertThat(windowDecorViewHost.viewHost!!.view).isEqualTo(view)
+ assertThat(windowDecorViewHost.viewHost!!.view!!.layoutParams.width)
+ .isEqualTo(otherParams.width)
+ }
+
+ @Test
+ fun updateView_replacingView_throws() = runTest {
+ val windowDecorViewHost = createDefaultViewHost()
+ val view = View(context)
+ windowDecorViewHost.updateView(
+ view = view,
+ attrs = WindowManager.LayoutParams(100, 100),
+ configuration = context.resources.configuration,
+ onDrawTransaction = null
+ )
+
+ val otherView = View(context)
+ assertThrows(Exception::class.java) {
+ windowDecorViewHost.updateView(
+ view = otherView,
+ attrs = WindowManager.LayoutParams(100, 100),
+ configuration = context.resources.configuration,
+ onDrawTransaction = null
+ )
+ }
}
@OptIn(ExperimentalCoroutinesApi::class)
@@ -77,7 +125,7 @@
)
// No view host yet, since the coroutine hasn't run.
- assertThat(windowDecorViewHost.viewHostAdapter.isInitialized()).isFalse()
+ assertThat(windowDecorViewHost.viewHost).isNull()
windowDecorViewHost.updateView(
view = syncView,
@@ -89,13 +137,14 @@
// Would run coroutine if it hadn't been cancelled.
advanceUntilIdle()
- assertThat(windowDecorViewHost.viewHostAdapter.isInitialized()).isTrue()
- assertThat(windowDecorViewHost.view()).isNotNull()
+ assertThat(windowDecorViewHost.viewHost).isNotNull()
+ assertThat(windowDecorViewHost.viewHost!!.view).isNotNull()
// View host view/attrs should match the ones from the sync call, plus, since the
// sync/async were made with different views, if the job hadn't been cancelled there
// would've been an exception thrown as replacing views isn't allowed.
- assertThat(windowDecorViewHost.view()).isEqualTo(syncView)
- assertThat(windowDecorViewHost.view()!!.layoutParams.width).isEqualTo(syncAttrs.width)
+ assertThat(windowDecorViewHost.viewHost!!.view).isEqualTo(syncView)
+ assertThat(windowDecorViewHost.viewHost!!.view!!.layoutParams.width)
+ .isEqualTo(syncAttrs.width)
}
@OptIn(ExperimentalCoroutinesApi::class)
@@ -111,11 +160,11 @@
configuration = context.resources.configuration,
)
- assertThat(windowDecorViewHost.viewHostAdapter.isInitialized()).isFalse()
+ assertThat(windowDecorViewHost.viewHost).isNull()
advanceUntilIdle()
- assertThat(windowDecorViewHost.viewHostAdapter.isInitialized()).isTrue()
+ assertThat(windowDecorViewHost.viewHost).isNotNull()
}
@OptIn(ExperimentalCoroutinesApi::class)
@@ -138,8 +187,9 @@
advanceUntilIdle()
- assertThat(windowDecorViewHost.viewHostAdapter.isInitialized()).isTrue()
- assertThat(windowDecorViewHost.view()).isEqualTo(otherView)
+ assertThat(windowDecorViewHost.viewHost).isNotNull()
+ assertThat(windowDecorViewHost.viewHost!!.view).isNotNull()
+ assertThat(windowDecorViewHost.viewHost!!.view).isEqualTo(otherView)
}
@Test
@@ -157,15 +207,16 @@
val t = mock(SurfaceControl.Transaction::class.java)
windowDecorViewHost.release(t)
- verify(windowDecorViewHost.viewHostAdapter).release(t)
+ verify(windowDecorViewHost.viewHost!!).release()
+ verify(t).remove(windowDecorViewHost.surfaceControl)
}
private fun CoroutineScope.createDefaultViewHost() = DefaultWindowDecorViewHost(
context = context,
mainScope = this,
display = context.display,
- viewHostAdapter = spy(SurfaceControlViewHostAdapter(context, context.display)),
+ surfaceControlViewHostFactory = { c, d, wwm, s ->
+ spy(SurfaceControlViewHost(c, d, wwm, s))
+ }
)
-
- private fun DefaultWindowDecorViewHost.view(): View? = viewHostAdapter.viewHost?.view
}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/PooledWindowDecorViewHostSupplierTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/PooledWindowDecorViewHostSupplierTest.kt
deleted file mode 100644
index a7e4213..0000000
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/PooledWindowDecorViewHostSupplierTest.kt
+++ /dev/null
@@ -1,181 +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.wm.shell.windowdecor.viewhost
-
-import android.testing.AndroidTestingRunner
-import android.view.SurfaceControl
-import androidx.test.filters.SmallTest
-import com.android.wm.shell.ShellTestCase
-import com.android.wm.shell.TestShellExecutor
-import com.android.wm.shell.sysui.ShellInit
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.advanceUntilIdle
-import kotlinx.coroutines.test.runTest
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.MockitoAnnotations
-import org.mockito.kotlin.any
-import org.mockito.kotlin.mock
-import org.mockito.kotlin.never
-import org.mockito.kotlin.verify
-import org.mockito.kotlin.whenever
-
-/**
- * Tests for [PooledWindowDecorViewHostSupplier].
- *
- * Build/Install/Run:
- * atest WMShellUnitTests:PooledWindowDecorViewHostSupplierTest
- */
-@OptIn(ExperimentalCoroutinesApi::class)
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-class PooledWindowDecorViewHostSupplierTest : ShellTestCase() {
-
- private val testExecutor = TestShellExecutor()
- private val testShellInit = ShellInit(testExecutor)
- @Mock
- private lateinit var mockViewHostFactory: ReusableWindowDecorViewHost.Factory
-
- private lateinit var supplier: PooledWindowDecorViewHostSupplier
-
- @Test
- fun setUp() {
- MockitoAnnotations.initMocks(this)
- }
-
- @Test
- fun onInit_warmsAndPoolsViewHosts() = runTest {
- supplier = createSupplier(maxPoolSize = 5, preWarmSize = 2)
- val mockViewHost1 = mock<ReusableWindowDecorViewHost>()
- val mockViewHost2 = mock<ReusableWindowDecorViewHost>()
- whenever(mockViewHostFactory
- .create(context, this, context.display, id = 0))
- .thenReturn(mockViewHost1)
- whenever(mockViewHostFactory
- .create(context, this, context.display, id = 1))
- .thenReturn(mockViewHost2)
-
- testExecutor.flushAll()
- advanceUntilIdle()
-
- // Both were warmed up.
- verify(mockViewHost1).warmUp()
- verify(mockViewHost2).warmUp()
- // Both were released, so re-acquiring them provides the same instance.
- assertThat(mockViewHost2)
- .isEqualTo(supplier.acquire(context, context.display))
- assertThat(mockViewHost1)
- .isEqualTo(supplier.acquire(context, context.display))
- }
-
- @Test(expected = Throwable::class)
- fun onInit_warmUpSizeExceedsPoolSize_throws() = runTest {
- createSupplier(maxPoolSize = 3, preWarmSize = 4)
- }
-
- @Test
- fun acquire_poolHasInstances_reuses() = runTest {
- supplier = createSupplier(maxPoolSize = 5, preWarmSize = 0)
-
- // Prepare the pool with one instance.
- val mockViewHost = mock<ReusableWindowDecorViewHost>()
- supplier.release(mockViewHost, SurfaceControl.Transaction())
-
- assertThat(mockViewHost)
- .isEqualTo(supplier.acquire(context, context.display))
- verify(mockViewHostFactory, never()).create(any(), any(), any(), any())
- }
-
- @Test
- fun acquire_pooledHasZeroInstances_creates() = runTest {
- supplier = createSupplier(maxPoolSize = 5, preWarmSize = 0)
-
- supplier.acquire(context, context.display)
-
- verify(mockViewHostFactory).create(context, this, context.display, id = 0)
- }
-
- @Test
- fun release_poolBelowLimit_caches() = runTest {
- supplier = createSupplier(maxPoolSize = 5, preWarmSize = 0)
-
- val mockViewHost = mock<ReusableWindowDecorViewHost>()
- val mockT = mock<SurfaceControl.Transaction>()
- supplier.release(mockViewHost, mockT)
-
- assertThat(mockViewHost)
- .isEqualTo(supplier.acquire(context, context.display))
- }
-
- @Test
- fun release_poolBelowLimit_doesNotReleaseViewHost() = runTest {
- supplier = createSupplier(maxPoolSize = 5, preWarmSize = 0)
-
- val mockViewHost = mock<ReusableWindowDecorViewHost>()
- val mockT = mock<SurfaceControl.Transaction>()
- supplier.release(mockViewHost, mockT)
-
- verify(mockViewHost, never()).release(mockT)
- }
-
- @Test
- fun release_poolAtLimit_doesNotCache() = runTest {
- supplier = createSupplier(maxPoolSize = 1, preWarmSize = 0)
- val mockT = mock<SurfaceControl.Transaction>()
- val mockViewHost = mock<ReusableWindowDecorViewHost>()
- supplier.release(mockViewHost, mockT) // Maxes pool.
-
- val mockViewHost2 = mock<ReusableWindowDecorViewHost>()
- supplier.release(mockViewHost2, mockT) // Beyond limit.
-
- assertThat(mockViewHost)
- .isEqualTo(supplier.acquire(context, context.display))
- // Second one wasn't cached, so the acquired one should've been a new instance.
- assertThat(mockViewHost2)
- .isNotEqualTo(supplier.acquire(context, context.display))
- }
-
- @Test
- fun release_poolAtLimit_releasesViewHost() = runTest {
- supplier = createSupplier(maxPoolSize = 1, preWarmSize = 0)
- val mockT = mock<SurfaceControl.Transaction>()
- val mockViewHost = mock<ReusableWindowDecorViewHost>()
- supplier.release(mockViewHost, mockT) // Maxes pool.
-
- val mockViewHost2 = mock<ReusableWindowDecorViewHost>()
- supplier.release(mockViewHost2, mockT) // Beyond limit.
-
- // Second one doesn't fit, so it needs to be released.
- verify(mockViewHost2).release(mockT)
- }
-
- private fun CoroutineScope.createSupplier(
- maxPoolSize: Int,
- preWarmSize: Int
- ) = PooledWindowDecorViewHostSupplier(
- context,
- this,
- testShellInit,
- mockViewHostFactory,
- maxPoolSize,
- preWarmSize
- ).also {
- testShellInit.init()
- }
-}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/ReusableWindowDecorViewHostTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/ReusableWindowDecorViewHostTest.kt
deleted file mode 100644
index de2444e..0000000
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/ReusableWindowDecorViewHostTest.kt
+++ /dev/null
@@ -1,182 +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.wm.shell.windowdecor.viewhost
-
-import android.testing.AndroidTestingRunner
-import android.testing.TestableLooper
-import android.view.SurfaceControl
-import android.view.View
-import android.view.WindowManager
-import androidx.test.filters.SmallTest
-import com.android.wm.shell.ShellTestCase
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.advanceUntilIdle
-import kotlinx.coroutines.test.runTest
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mockito.mock
-import org.mockito.kotlin.spy
-import org.mockito.kotlin.verify
-
-/**
- * Tests for [ReusableWindowDecorViewHost].
- *
- * Build/Install/Run:
- * atest WMShellUnitTests:ReusableWindowDecorViewHostTest
- */
-@SmallTest
-@TestableLooper.RunWithLooper
-@RunWith(AndroidTestingRunner::class)
-class ReusableWindowDecorViewHostTest : ShellTestCase() {
-
- @Test
- fun warmUp_addsRootView() = runTest {
- val reusableVH = createReusableViewHost().apply {
- warmUp()
- }
-
- assertThat(reusableVH.viewHostAdapter.isInitialized()).isTrue()
- assertThat(reusableVH.view()).isEqualTo(reusableVH.rootView)
- }
-
- @Test
- fun update_differentView_replacesView() = runTest {
- val view = View(context)
- val lp = WindowManager.LayoutParams()
- val reusableVH = createReusableViewHost()
- reusableVH.updateView(view, lp, context.resources.configuration, null)
-
- assertThat(reusableVH.rootView.childCount).isEqualTo(1)
- assertThat(reusableVH.rootView.getChildAt(0)).isEqualTo(view)
-
- val newView = View(context)
- val newLp = WindowManager.LayoutParams()
- reusableVH.updateView(newView, newLp, context.resources.configuration, null)
-
- assertThat(reusableVH.rootView.childCount).isEqualTo(1)
- assertThat(reusableVH.rootView.getChildAt(0)).isEqualTo(newView)
- }
-
- @OptIn(ExperimentalCoroutinesApi::class)
- @Test
- fun updateView_clearsPendingAsyncJob() = runTest {
- val reusableVH = createReusableViewHost()
- val asyncView = View(context)
- val syncView = View(context)
- val asyncAttrs = WindowManager.LayoutParams(100, 100)
- val syncAttrs = WindowManager.LayoutParams(200, 200)
-
- reusableVH.updateViewAsync(
- view = asyncView,
- attrs = asyncAttrs,
- configuration = context.resources.configuration,
- )
-
- // No view host yet, since the coroutine hasn't run.
- assertThat(reusableVH.viewHostAdapter.isInitialized()).isFalse()
-
- reusableVH.updateView(
- view = syncView,
- attrs = syncAttrs,
- configuration = context.resources.configuration,
- onDrawTransaction = null
- )
-
- // Would run coroutine if it hadn't been cancelled.
- advanceUntilIdle()
-
- assertThat(reusableVH.viewHostAdapter.isInitialized()).isTrue()
- // View host view/attrs should match the ones from the sync call, plus, since the
- // sync/async were made with different views, if the job hadn't been cancelled there
- // would've been an exception thrown as replacing views isn't allowed.
- assertThat(reusableVH.rootView.getChildAt(0)).isEqualTo(syncView)
- assertThat(reusableVH.view()!!.layoutParams.width).isEqualTo(syncAttrs.width)
- }
-
- @OptIn(ExperimentalCoroutinesApi::class)
- @Test
- fun updateViewAsync() = runTest {
- val reusableVH = createReusableViewHost()
- val view = View(context)
- val attrs = WindowManager.LayoutParams(100, 100)
-
- reusableVH.updateViewAsync(
- view = view,
- attrs = attrs,
- configuration = context.resources.configuration,
- )
-
- assertThat(reusableVH.viewHostAdapter.isInitialized()).isFalse()
-
- advanceUntilIdle()
-
- assertThat(reusableVH.viewHostAdapter.isInitialized()).isTrue()
- }
-
- @OptIn(ExperimentalCoroutinesApi::class)
- @Test
- fun updateViewAsync_clearsPendingAsyncJob() = runTest {
- val reusableVH = createReusableViewHost()
-
- val view = View(context)
- reusableVH.updateViewAsync(
- view = view,
- attrs = WindowManager.LayoutParams(100, 100),
- configuration = context.resources.configuration,
- )
- val otherView = View(context)
- reusableVH.updateViewAsync(
- view = otherView,
- attrs = WindowManager.LayoutParams(100, 100),
- configuration = context.resources.configuration,
- )
-
- advanceUntilIdle()
-
- assertThat(reusableVH.viewHostAdapter.isInitialized()).isTrue()
- assertThat(reusableVH.rootView.getChildAt(0)).isEqualTo(otherView)
- }
-
- @Test
- fun release() = runTest {
- val reusableVH = createReusableViewHost()
-
- val view = View(context)
- reusableVH.updateView(
- view = view,
- attrs = WindowManager.LayoutParams(100, 100),
- configuration = context.resources.configuration,
- onDrawTransaction = null
- )
-
- val t = mock(SurfaceControl.Transaction::class.java)
- reusableVH.release(t)
-
- verify(reusableVH.viewHostAdapter).release(t)
- }
-
- private fun CoroutineScope.createReusableViewHost() = ReusableWindowDecorViewHost(
- context = context,
- mainScope = this,
- display = context.display,
- id = 1,
- viewHostAdapter = spy(SurfaceControlViewHostAdapter(context, context.display)),
- )
-
- private fun ReusableWindowDecorViewHost.view(): View? = viewHostAdapter.viewHost?.view
-}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/SurfaceControlViewHostAdapterTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/SurfaceControlViewHostAdapterTest.kt
deleted file mode 100644
index d6c80a7..0000000
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/SurfaceControlViewHostAdapterTest.kt
+++ /dev/null
@@ -1,144 +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.wm.shell.windowdecor.viewhost
-
-import android.testing.AndroidTestingRunner
-import android.testing.TestableLooper
-import android.view.SurfaceControl
-import android.view.SurfaceControlViewHost
-import android.view.View
-import android.view.WindowManager
-import androidx.test.filters.SmallTest
-import com.android.wm.shell.ShellTestCase
-import com.google.common.truth.Truth.assertThat
-import org.junit.Assert.assertThrows
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mockito.mock
-import org.mockito.kotlin.spy
-import org.mockito.kotlin.verify
-
-/**
- * Tests for [SurfaceControlViewHostAdapter].
- *
- * Build/Install/Run:
- * atest WMShellUnitTests:SurfaceControlViewHostAdapterTest
- */
-@SmallTest
-@TestableLooper.RunWithLooper
-@RunWith(AndroidTestingRunner::class)
-class SurfaceControlViewHostAdapterTest : ShellTestCase() {
-
- private lateinit var adapter: SurfaceControlViewHostAdapter
-
- @Before
- fun setUp() {
- adapter = SurfaceControlViewHostAdapter(
- context,
- context.display,
- surfaceControlViewHostFactory = { c, d, wwm, s ->
- spy(SurfaceControlViewHost(c, d, wwm, s))
- }
- )
- }
-
- @Test
- fun prepareViewHost() {
- adapter.prepareViewHost(context.resources.configuration)
-
- assertThat(adapter.viewHost).isNotNull()
- }
-
- @Test
- fun prepareViewHost_alreadyCreated_skips() {
- adapter.prepareViewHost(context.resources.configuration)
-
- val viewHost = adapter.viewHost!!
-
- adapter.prepareViewHost(context.resources.configuration)
-
- assertThat(adapter.viewHost).isEqualTo(viewHost)
- }
-
- @Test
- fun updateView_layoutInViewHost() {
- val view = View(context)
- adapter.prepareViewHost(context.resources.configuration)
-
- adapter.updateView(
- view = view,
- attrs = WindowManager.LayoutParams(100, 100)
- )
-
- assertThat(adapter.isInitialized()).isTrue()
- assertThat(adapter.view()).isEqualTo(view)
- }
-
- @Test
- fun updateView_alreadyLaidOut_relayouts() {
- val view = View(context)
- adapter.prepareViewHost(context.resources.configuration)
- adapter.updateView(
- view = view,
- attrs = WindowManager.LayoutParams(100, 100)
- )
-
- val otherParams = WindowManager.LayoutParams(200, 200)
- adapter.updateView(
- view = view,
- attrs = otherParams
- )
-
- assertThat(adapter.view()).isEqualTo(view)
- assertThat(adapter.view()!!.layoutParams.width).isEqualTo(otherParams.width)
- }
-
- @Test
- fun updateView_replacingView_throws() {
- val view = View(context)
- adapter.prepareViewHost(context.resources.configuration)
- adapter.updateView(
- view = view,
- attrs = WindowManager.LayoutParams(100, 100)
- )
-
- val otherView = View(context)
- assertThrows(Exception::class.java) {
- adapter.updateView(
- view = otherView,
- attrs = WindowManager.LayoutParams(100, 100)
- )
- }
- }
-
- @Test
- fun release() {
- adapter.prepareViewHost(context.resources.configuration)
- adapter.updateView(
- view = View(context),
- attrs = WindowManager.LayoutParams(100, 100)
- )
-
- val mockT = mock(SurfaceControl.Transaction::class.java)
- adapter.release(mockT)
-
- verify(adapter.viewHost!!).release()
- verify(mockT).remove(adapter.rootSurface)
- }
-
- private fun SurfaceControlViewHostAdapter.view(): View? = viewHost?.view
-}
diff --git a/libs/appfunctions/Android.bp b/libs/appfunctions/Android.bp
new file mode 100644
index 0000000..09e2f42
--- /dev/null
+++ b/libs/appfunctions/Android.bp
@@ -0,0 +1,31 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_sdk_library {
+ name: "com.google.android.appfunctions.sidecar",
+ owner: "google",
+ srcs: ["java/**/*.java"],
+ api_packages: ["com.google.android.appfunctions.sidecar"],
+ dex_preopt: {
+ enabled: false,
+ },
+ system_ext_specific: true,
+ no_dist: true,
+ unsafe_ignore_missing_latest_api: true,
+}
diff --git a/libs/appfunctions/api/current.txt b/libs/appfunctions/api/current.txt
new file mode 100644
index 0000000..504e329
--- /dev/null
+++ b/libs/appfunctions/api/current.txt
@@ -0,0 +1,49 @@
+// Signature format: 2.0
+package com.google.android.appfunctions.sidecar {
+
+ public final class AppFunctionManager {
+ ctor public AppFunctionManager(android.content.Context);
+ method public void executeAppFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>);
+ }
+
+ public abstract class AppFunctionService extends android.app.Service {
+ ctor public AppFunctionService();
+ method @NonNull public final android.os.IBinder onBind(@Nullable android.content.Intent);
+ method @MainThread public abstract void onExecuteFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>);
+ field @NonNull public static final String BIND_APP_FUNCTION_SERVICE = "android.permission.BIND_APP_FUNCTION_SERVICE";
+ field @NonNull public static final String SERVICE_INTERFACE = "android.app.appfunctions.AppFunctionService";
+ }
+
+ public final class ExecuteAppFunctionRequest {
+ method @NonNull public android.os.Bundle getExtras();
+ method @NonNull public String getFunctionIdentifier();
+ method @NonNull public android.app.appsearch.GenericDocument getParameters();
+ method @NonNull public String getTargetPackageName();
+ }
+
+ public static final class ExecuteAppFunctionRequest.Builder {
+ ctor public ExecuteAppFunctionRequest.Builder(@NonNull String, @NonNull String);
+ method @NonNull public com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest build();
+ method @NonNull public com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest.Builder setExtras(@NonNull android.os.Bundle);
+ method @NonNull public com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest.Builder setParameters(@NonNull android.app.appsearch.GenericDocument);
+ }
+
+ public final class ExecuteAppFunctionResponse {
+ method @Nullable public String getErrorMessage();
+ method @NonNull public android.os.Bundle getExtras();
+ method public int getResultCode();
+ method @NonNull public android.app.appsearch.GenericDocument getResultDocument();
+ method public boolean isSuccess();
+ method @NonNull public static com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse newFailure(int, @Nullable String, @Nullable android.os.Bundle);
+ method @NonNull public static com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse newSuccess(@NonNull android.app.appsearch.GenericDocument, @Nullable android.os.Bundle);
+ field public static final String PROPERTY_RETURN_VALUE = "returnValue";
+ field public static final int RESULT_APP_UNKNOWN_ERROR = 2; // 0x2
+ field public static final int RESULT_DENIED = 1; // 0x1
+ field public static final int RESULT_INTERNAL_ERROR = 3; // 0x3
+ field public static final int RESULT_INVALID_ARGUMENT = 4; // 0x4
+ field public static final int RESULT_OK = 0; // 0x0
+ field public static final int RESULT_TIMED_OUT = 5; // 0x5
+ }
+
+}
+
diff --git a/libs/appfunctions/api/removed.txt b/libs/appfunctions/api/removed.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/libs/appfunctions/api/removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/libs/appfunctions/api/system-current.txt b/libs/appfunctions/api/system-current.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/libs/appfunctions/api/system-current.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/libs/appfunctions/api/system-removed.txt b/libs/appfunctions/api/system-removed.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/libs/appfunctions/api/system-removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/libs/appfunctions/api/test-current.txt b/libs/appfunctions/api/test-current.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/libs/appfunctions/api/test-current.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/libs/appfunctions/api/test-removed.txt b/libs/appfunctions/api/test-removed.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/libs/appfunctions/api/test-removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java
new file mode 100644
index 0000000..b1dd467
--- /dev/null
+++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java
@@ -0,0 +1,82 @@
+/*
+ * 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.google.android.appfunctions.sidecar;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.content.Context;
+
+import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+
+/**
+ * Provides app functions related functionalities.
+ *
+ * <p>App function is a specific piece of functionality that an app offers to the system. These
+ * functionalities can be integrated into various system features.
+ *
+ * <p>This class wraps {@link android.app.appfunctions.AppFunctionManager} functionalities and
+ * exposes it here as a sidecar library (avoiding direct dependency on the platform API).
+ */
+// TODO(b/357551503): Implement get and set enabled app function APIs.
+// TODO(b/367329899): Add sidecar library to Android B builds.
+public final class AppFunctionManager {
+ private final android.app.appfunctions.AppFunctionManager mManager;
+ private final Context mContext;
+
+ /**
+ * Creates an instance.
+ *
+ * @param context A {@link Context}.
+ * @throws java.lang.IllegalStateException if the underlying {@link
+ * android.app.appfunctions.AppFunctionManager} is not found.
+ */
+ public AppFunctionManager(Context context) {
+ mContext = Objects.requireNonNull(context);
+ mManager = context.getSystemService(android.app.appfunctions.AppFunctionManager.class);
+ if (mManager == null) {
+ throw new IllegalStateException(
+ "Underlying AppFunctionManager system service not found.");
+ }
+ }
+
+ /**
+ * Executes the app function.
+ *
+ * <p>Proxies request and response to the underlying {@link
+ * android.app.appfunctions.AppFunctionManager#executeAppFunction}, converting the request and
+ * response in the appropriate type required by the function.
+ */
+ public void executeAppFunction(
+ @NonNull ExecuteAppFunctionRequest sidecarRequest,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull Consumer<ExecuteAppFunctionResponse> callback) {
+ Objects.requireNonNull(sidecarRequest);
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(callback);
+
+ android.app.appfunctions.ExecuteAppFunctionRequest platformRequest =
+ SidecarConverter.getPlatformExecuteAppFunctionRequest(sidecarRequest);
+ mManager.executeAppFunction(
+ platformRequest, executor, (platformResponse) -> {
+ callback.accept(SidecarConverter.getSidecarExecuteAppFunctionResponse(
+ platformResponse));
+ });
+ }
+}
diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java
new file mode 100644
index 0000000..65959df
--- /dev/null
+++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java
@@ -0,0 +1,114 @@
+/*
+ * 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.google.android.appfunctions.sidecar;
+
+import static android.Manifest.permission.BIND_APP_FUNCTION_SERVICE;
+
+import android.annotation.MainThread;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Service;
+import android.content.Intent;
+import android.os.Binder;
+import android.os.IBinder;
+
+import java.util.function.Consumer;
+
+/**
+ * Abstract base class to provide app functions to the system.
+ *
+ * <p>Include the following in the manifest:
+ *
+ * <pre>
+ * {@literal
+ * <service android:name=".YourService"
+ * android:permission="android.permission.BIND_APP_FUNCTION_SERVICE">
+ * <intent-filter>
+ * <action android:name="android.app.appfunctions.AppFunctionService" />
+ * </intent-filter>
+ * </service>
+ * }
+ * </pre>
+ *
+ * <p>This class wraps {@link android.app.appfunctions.AppFunctionService} functionalities and
+ * exposes it here as a sidecar library (avoiding direct dependency on the platform API).
+ *
+ * @see AppFunctionManager
+ */
+public abstract class AppFunctionService extends Service {
+ /**
+ * The permission to only allow system access to the functions through {@link
+ * AppFunctionManagerService}.
+ */
+ @NonNull
+ public static final String BIND_APP_FUNCTION_SERVICE =
+ "android.permission.BIND_APP_FUNCTION_SERVICE";
+
+ /**
+ * The {@link Intent} that must be declared as handled by the service. To be supported, the
+ * service must also require the {@link BIND_APP_FUNCTION_SERVICE} permission so that other
+ * applications can not abuse it.
+ */
+ @NonNull
+ public static final String SERVICE_INTERFACE = "android.app.appfunctions.AppFunctionService";
+
+ private final Binder mBinder =
+ android.app.appfunctions.AppFunctionService.createBinder(
+ /* context= */ this,
+ /* onExecuteFunction= */ (platformRequest, callback) -> {
+ AppFunctionService.this.onExecuteFunction(
+ SidecarConverter.getSidecarExecuteAppFunctionRequest(
+ platformRequest),
+ (sidecarResponse) -> {
+ callback.accept(
+ SidecarConverter.getPlatformExecuteAppFunctionResponse(
+ sidecarResponse));
+ });
+ }
+ );
+
+ @NonNull
+ @Override
+ public final IBinder onBind(@Nullable Intent intent) {
+ return mBinder;
+ }
+
+ /**
+ * Called by the system to execute a specific app function.
+ *
+ * <p>This method is triggered when the system requests your AppFunctionService to handle a
+ * particular function you have registered and made available.
+ *
+ * <p>To ensure proper routing of function requests, assign a unique identifier to each
+ * function. This identifier doesn't need to be globally unique, but it must be unique within
+ * your app. For example, a function to order food could be identified as "orderFood". In most
+ * cases this identifier should come from the ID automatically generated by the AppFunctions
+ * SDK. You can determine the specific function to invoke by calling {@link
+ * ExecuteAppFunctionRequest#getFunctionIdentifier()}.
+ *
+ * <p>This method is always triggered in the main thread. You should run heavy tasks on a worker
+ * thread and dispatch the result with the given callback. You should always report back the
+ * result using the callback, no matter if the execution was successful or not.
+ *
+ * @param request The function execution request.
+ * @param callback A callback to report back the result.
+ */
+ @MainThread
+ public abstract void onExecuteFunction(
+ @NonNull ExecuteAppFunctionRequest request,
+ @NonNull Consumer<ExecuteAppFunctionResponse> callback);
+}
diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionRequest.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionRequest.java
new file mode 100644
index 0000000..fa6d2ff
--- /dev/null
+++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionRequest.java
@@ -0,0 +1,137 @@
+/*
+ * 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.google.android.appfunctions.sidecar;
+
+import android.annotation.NonNull;
+import android.app.appsearch.GenericDocument;
+import android.os.Bundle;
+
+import java.util.Objects;
+
+/**
+ * A request to execute an app function.
+ *
+ * <p>This class copies {@link android.app.appfunctions.ExecuteAppFunctionRequest} without parcel
+ * functionality and exposes it here as a sidecar library (avoiding direct dependency on the
+ * platform API).
+ */
+public final class ExecuteAppFunctionRequest {
+ /** Returns the package name of the app that hosts the function. */
+ @NonNull private final String mTargetPackageName;
+
+ /**
+ * Returns the unique string identifier of the app function to be executed. TODO(b/357551503):
+ * Document how callers can get the available function identifiers.
+ */
+ @NonNull private final String mFunctionIdentifier;
+
+ /** Returns additional metadata relevant to this function execution request. */
+ @NonNull private final Bundle mExtras;
+
+ /**
+ * Returns the parameters required to invoke this function. Within this [GenericDocument], the
+ * property names are the names of the function parameters and the property values are the
+ * values of those parameters.
+ *
+ * <p>The document may have missing parameters. Developers are advised to implement defensive
+ * handling measures.
+ *
+ * <p>TODO(b/357551503): Document how function parameters can be obtained for function execution
+ */
+ @NonNull private final GenericDocument mParameters;
+
+ private ExecuteAppFunctionRequest(
+ @NonNull String targetPackageName,
+ @NonNull String functionIdentifier,
+ @NonNull Bundle extras,
+ @NonNull GenericDocument parameters) {
+ mTargetPackageName = Objects.requireNonNull(targetPackageName);
+ mFunctionIdentifier = Objects.requireNonNull(functionIdentifier);
+ mExtras = Objects.requireNonNull(extras);
+ mParameters = Objects.requireNonNull(parameters);
+ }
+
+ /** Returns the package name of the app that hosts the function. */
+ @NonNull
+ public String getTargetPackageName() {
+ return mTargetPackageName;
+ }
+
+ /** Returns the unique string identifier of the app function to be executed. */
+ @NonNull
+ public String getFunctionIdentifier() {
+ return mFunctionIdentifier;
+ }
+
+ /**
+ * Returns the function parameters. The key is the parameter name, and the value is the
+ * parameter value.
+ *
+ * <p>The bundle may have missing parameters. Developers are advised to implement defensive
+ * handling measures.
+ */
+ @NonNull
+ public GenericDocument getParameters() {
+ return mParameters;
+ }
+
+ /** Returns the additional data relevant to this function execution. */
+ @NonNull
+ public Bundle getExtras() {
+ return mExtras;
+ }
+
+ /** Builder for {@link ExecuteAppFunctionRequest}. */
+ public static final class Builder {
+ @NonNull private final String mTargetPackageName;
+ @NonNull private final String mFunctionIdentifier;
+ @NonNull private Bundle mExtras = Bundle.EMPTY;
+
+ @NonNull
+ private GenericDocument mParameters = new GenericDocument.Builder<>("", "", "").build();
+
+ public Builder(@NonNull String targetPackageName, @NonNull String functionIdentifier) {
+ mTargetPackageName = Objects.requireNonNull(targetPackageName);
+ mFunctionIdentifier = Objects.requireNonNull(functionIdentifier);
+ }
+
+ /** Sets the additional data relevant to this function execution. */
+ @NonNull
+ public Builder setExtras(@NonNull Bundle extras) {
+ mExtras = Objects.requireNonNull(extras);
+ return this;
+ }
+
+ /** Sets the function parameters. */
+ @NonNull
+ public Builder setParameters(@NonNull GenericDocument parameters) {
+ Objects.requireNonNull(parameters);
+ mParameters = parameters;
+ return this;
+ }
+
+ /** Builds the {@link ExecuteAppFunctionRequest}. */
+ @NonNull
+ public ExecuteAppFunctionRequest build() {
+ return new ExecuteAppFunctionRequest(
+ mTargetPackageName,
+ mFunctionIdentifier,
+ mExtras,
+ mParameters);
+ }
+ }
+}
diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java
new file mode 100644
index 0000000..60c25fa
--- /dev/null
+++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java
@@ -0,0 +1,240 @@
+/*
+ * 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.google.android.appfunctions.sidecar;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.appsearch.GenericDocument;
+import android.os.Bundle;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * The response to an app function execution.
+ *
+ * <p>This class copies {@link android.app.appfunctions.ExecuteAppFunctionResponse} without parcel
+ * functionality and exposes it here as a sidecar library (avoiding direct dependency on the
+ * platform API).
+ */
+public final class ExecuteAppFunctionResponse {
+ /**
+ * The name of the property that stores the function return value within the {@code
+ * resultDocument}.
+ *
+ * <p>See {@link GenericDocument#getProperty(String)} for more information.
+ *
+ * <p>If the function returns {@code void} or throws an error, the {@code resultDocument} will
+ * be empty {@link GenericDocument}.
+ *
+ * <p>If the {@code resultDocument} is empty, {@link GenericDocument#getProperty(String)} will
+ * return {@code null}.
+ *
+ * <p>See {@link #getResultDocument} for more information on extracting the return value.
+ */
+ public static final String PROPERTY_RETURN_VALUE = "returnValue";
+
+ /** The call was successful. */
+ public static final int RESULT_OK = 0;
+
+ /** The caller does not have the permission to execute an app function. */
+ public static final int RESULT_DENIED = 1;
+
+ /** An unknown error occurred while processing the call in the AppFunctionService. */
+ public static final int RESULT_APP_UNKNOWN_ERROR = 2;
+
+ /**
+ * An internal error occurred within AppFunctionManagerService.
+ *
+ * <p>This error may be considered similar to {@link IllegalStateException}
+ */
+ public static final int RESULT_INTERNAL_ERROR = 3;
+
+ /**
+ * The caller supplied invalid arguments to the call.
+ *
+ * <p>This error may be considered similar to {@link IllegalArgumentException}.
+ */
+ public static final int RESULT_INVALID_ARGUMENT = 4;
+
+ /** The operation was timed out. */
+ public static final int RESULT_TIMED_OUT = 5;
+
+ /** The result code of the app function execution. */
+ @ResultCode private final int mResultCode;
+
+ /**
+ * The error message associated with the result, if any. This is {@code null} if the result code
+ * is {@link #RESULT_OK}.
+ */
+ @Nullable private final String mErrorMessage;
+
+ /**
+ * Returns the return value of the executed function.
+ *
+ * <p>The return value is stored in a {@link GenericDocument} with the key {@link
+ * #PROPERTY_RETURN_VALUE}.
+ *
+ * <p>See {@link #getResultDocument} for more information on extracting the return value.
+ */
+ @NonNull private final GenericDocument mResultDocument;
+
+ /** Returns the additional metadata data relevant to this function execution response. */
+ @NonNull private final Bundle mExtras;
+
+ private ExecuteAppFunctionResponse(
+ @NonNull GenericDocument resultDocument,
+ @NonNull Bundle extras,
+ @ResultCode int resultCode,
+ @Nullable String errorMessage) {
+ mResultDocument = Objects.requireNonNull(resultDocument);
+ mExtras = Objects.requireNonNull(extras);
+ mResultCode = resultCode;
+ mErrorMessage = errorMessage;
+ }
+
+ /**
+ * Returns result codes from throwable.
+ *
+ * @hide
+ */
+ static @ResultCode int getResultCode(@NonNull Throwable t) {
+ if (t instanceof IllegalArgumentException) {
+ return ExecuteAppFunctionResponse.RESULT_INVALID_ARGUMENT;
+ }
+ return ExecuteAppFunctionResponse.RESULT_APP_UNKNOWN_ERROR;
+ }
+
+ /**
+ * Returns a successful response.
+ *
+ * @param resultDocument The return value of the executed function.
+ * @param extras The additional metadata data relevant to this function execution response.
+ */
+ @NonNull
+ public static ExecuteAppFunctionResponse newSuccess(
+ @NonNull GenericDocument resultDocument, @Nullable Bundle extras) {
+ Objects.requireNonNull(resultDocument);
+ Bundle actualExtras = getActualExtras(extras);
+
+ return new ExecuteAppFunctionResponse(
+ resultDocument, actualExtras, RESULT_OK, /* errorMessage= */ null);
+ }
+
+ /**
+ * Returns a failure response.
+ *
+ * @param resultCode The result code of the app function execution.
+ * @param extras The additional metadata data relevant to this function execution response.
+ * @param errorMessage The error message associated with the result, if any.
+ */
+ @NonNull
+ public static ExecuteAppFunctionResponse newFailure(
+ @ResultCode int resultCode, @Nullable String errorMessage, @Nullable Bundle extras) {
+ if (resultCode == RESULT_OK) {
+ throw new IllegalArgumentException("resultCode must not be RESULT_OK");
+ }
+ Bundle actualExtras = getActualExtras(extras);
+ GenericDocument emptyDocument = new GenericDocument.Builder<>("", "", "").build();
+ return new ExecuteAppFunctionResponse(
+ emptyDocument, actualExtras, resultCode, errorMessage);
+ }
+
+ private static Bundle getActualExtras(@Nullable Bundle extras) {
+ if (extras == null) {
+ return Bundle.EMPTY;
+ }
+ return extras;
+ }
+
+ /**
+ * Returns a generic document containing the return value of the executed function.
+ *
+ * <p>The {@link #PROPERTY_RETURN_VALUE} key can be used to obtain the return value.
+ *
+ * <p>An empty document is returned if {@link #isSuccess} is {@code false} or if the executed
+ * function does not produce a return value.
+ *
+ * <p>Sample code for extracting the return value:
+ *
+ * <pre>
+ * GenericDocument resultDocument = response.getResultDocument();
+ * Object returnValue = resultDocument.getProperty(PROPERTY_RETURN_VALUE);
+ * if (returnValue != null) {
+ * // Cast returnValue to expected type, or use {@link GenericDocument#getPropertyString},
+ * // {@link GenericDocument#getPropertyLong} etc.
+ * // Do something with the returnValue
+ * }
+ * </pre>
+ */
+ @NonNull
+ public GenericDocument getResultDocument() {
+ return mResultDocument;
+ }
+
+ /** Returns the extras of the app function execution. */
+ @NonNull
+ public Bundle getExtras() {
+ return mExtras;
+ }
+
+ /**
+ * Returns {@code true} if {@link #getResultCode} equals {@link
+ * ExecuteAppFunctionResponse#RESULT_OK}.
+ */
+ public boolean isSuccess() {
+ return getResultCode() == RESULT_OK;
+ }
+
+ /**
+ * Returns one of the {@code RESULT} constants defined in {@link ExecuteAppFunctionResponse}.
+ */
+ @ResultCode
+ public int getResultCode() {
+ return mResultCode;
+ }
+
+ /**
+ * Returns the error message associated with this result.
+ *
+ * <p>If {@link #isSuccess} is {@code true}, the error message is always {@code null}.
+ */
+ @Nullable
+ public String getErrorMessage() {
+ return mErrorMessage;
+ }
+
+ /**
+ * Result codes.
+ *
+ * @hide
+ */
+ @IntDef(
+ prefix = {"RESULT_"},
+ value = {
+ RESULT_OK,
+ RESULT_DENIED,
+ RESULT_APP_UNKNOWN_ERROR,
+ RESULT_INTERNAL_ERROR,
+ RESULT_INVALID_ARGUMENT,
+ RESULT_TIMED_OUT,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ResultCode {}
+}
diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/SidecarConverter.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/SidecarConverter.java
new file mode 100644
index 0000000..b1b05f7
--- /dev/null
+++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/SidecarConverter.java
@@ -0,0 +1,104 @@
+/*
+ * 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.google.android.appfunctions.sidecar;
+
+import android.annotation.NonNull;
+
+/**
+ * Utility class containing methods to convert Sidecar objects of AppFunctions API into the
+ * underlying platform classes.
+ *
+ * @hide
+ */
+public final class SidecarConverter {
+ private SidecarConverter() {}
+
+ /**
+ * Converts sidecar's {@link com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest}
+ * into platform's {@link android.app.appfunctions.ExecuteAppFunctionRequest}
+ *
+ * @hide
+ */
+ @NonNull
+ public static android.app.appfunctions.ExecuteAppFunctionRequest
+ getPlatformExecuteAppFunctionRequest(@NonNull ExecuteAppFunctionRequest request) {
+ return new
+ android.app.appfunctions.ExecuteAppFunctionRequest.Builder(
+ request.getTargetPackageName(),
+ request.getFunctionIdentifier())
+ .setExtras(request.getExtras())
+ .setParameters(request.getParameters())
+ .build();
+ }
+
+ /**
+ * Converts sidecar's {@link com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse}
+ * into platform's {@link android.app.appfunctions.ExecuteAppFunctionResponse}
+ *
+ * @hide
+ */
+ @NonNull
+ public static android.app.appfunctions.ExecuteAppFunctionResponse
+ getPlatformExecuteAppFunctionResponse(@NonNull ExecuteAppFunctionResponse response) {
+ if (response.isSuccess()) {
+ return android.app.appfunctions.ExecuteAppFunctionResponse.newSuccess(
+ response.getResultDocument(), response.getExtras());
+ } else {
+ return android.app.appfunctions.ExecuteAppFunctionResponse.newFailure(
+ response.getResultCode(),
+ response.getErrorMessage(),
+ response.getExtras());
+ }
+ }
+
+ /**
+ * Converts platform's {@link android.app.appfunctions.ExecuteAppFunctionRequest}
+ * into sidecar's {@link com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest}
+ *
+ * @hide
+ */
+ @NonNull
+ public static ExecuteAppFunctionRequest getSidecarExecuteAppFunctionRequest(
+ @NonNull android.app.appfunctions.ExecuteAppFunctionRequest request) {
+ return new ExecuteAppFunctionRequest.Builder(
+ request.getTargetPackageName(),
+ request.getFunctionIdentifier())
+ .setExtras(request.getExtras())
+ .setParameters(request.getParameters())
+ .build();
+ }
+
+ /**
+ * Converts platform's {@link android.app.appfunctions.ExecuteAppFunctionResponse}
+ * into sidecar's {@link com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse}
+ *
+ * @hide
+ */
+ @NonNull
+ public static ExecuteAppFunctionResponse getSidecarExecuteAppFunctionResponse(
+ @NonNull android.app.appfunctions.ExecuteAppFunctionResponse response) {
+ if (response.isSuccess()) {
+ return ExecuteAppFunctionResponse.newSuccess(
+ response.getResultDocument(), response.getExtras());
+ } else {
+ return ExecuteAppFunctionResponse.newFailure(
+ response.getResultCode(),
+ response.getErrorMessage(),
+ response.getExtras());
+ }
+ }
+}
diff --git a/libs/hwui/hwui/Bitmap.cpp b/libs/hwui/hwui/Bitmap.cpp
index 1854361..84bd45d 100644
--- a/libs/hwui/hwui/Bitmap.cpp
+++ b/libs/hwui/hwui/Bitmap.cpp
@@ -264,6 +264,7 @@
, mPixelStorageType(PixelStorageType::Heap) {
mPixelStorage.heap.address = address;
mPixelStorage.heap.size = size;
+ traceBitmapCreate();
}
Bitmap::Bitmap(SkPixelRef& pixelRef, const SkImageInfo& info)
@@ -272,6 +273,7 @@
, mPixelStorageType(PixelStorageType::WrappedPixelRef) {
pixelRef.ref();
mPixelStorage.wrapped.pixelRef = &pixelRef;
+ traceBitmapCreate();
}
Bitmap::Bitmap(void* address, int fd, size_t mappedSize, const SkImageInfo& info, size_t rowBytes)
@@ -281,6 +283,7 @@
mPixelStorage.ashmem.address = address;
mPixelStorage.ashmem.fd = fd;
mPixelStorage.ashmem.size = mappedSize;
+ traceBitmapCreate();
}
#ifdef __ANDROID__ // Layoutlib does not support hardware acceleration
@@ -297,10 +300,12 @@
setImmutable(); // HW bitmaps are always immutable
mImage = SkImages::DeferredFromAHardwareBuffer(buffer, mInfo.alphaType(),
mInfo.refColorSpace());
+ traceBitmapCreate();
}
#endif
Bitmap::~Bitmap() {
+ traceBitmapDelete();
switch (mPixelStorageType) {
case PixelStorageType::WrappedPixelRef:
mPixelStorage.wrapped.pixelRef->unref();
@@ -572,4 +577,28 @@
mGainmap = std::move(gainmap);
}
+std::mutex Bitmap::mLock{};
+size_t Bitmap::mTotalBitmapBytes = 0;
+size_t Bitmap::mTotalBitmapCount = 0;
+
+void Bitmap::traceBitmapCreate() {
+ if (ATRACE_ENABLED()) {
+ std::lock_guard lock{mLock};
+ mTotalBitmapBytes += getAllocationByteCount();
+ mTotalBitmapCount++;
+ ATRACE_INT64("Bitmap Memory", mTotalBitmapBytes);
+ ATRACE_INT64("Bitmap Count", mTotalBitmapCount);
+ }
+}
+
+void Bitmap::traceBitmapDelete() {
+ if (ATRACE_ENABLED()) {
+ std::lock_guard lock{mLock};
+ mTotalBitmapBytes -= getAllocationByteCount();
+ mTotalBitmapCount--;
+ ATRACE_INT64("Bitmap Memory", mTotalBitmapBytes);
+ ATRACE_INT64("Bitmap Count", mTotalBitmapCount);
+ }
+}
+
} // namespace android
diff --git a/libs/hwui/hwui/Bitmap.h b/libs/hwui/hwui/Bitmap.h
index dd344e2..3d55d85 100644
--- a/libs/hwui/hwui/Bitmap.h
+++ b/libs/hwui/hwui/Bitmap.h
@@ -25,6 +25,7 @@
#include <cutils/compiler.h>
#include <utils/StrongPointer.h>
+#include <mutex>
#include <optional>
#ifdef __ANDROID__ // Layoutlib does not support hardware acceleration
@@ -227,6 +228,13 @@
} mPixelStorage;
sk_sp<SkImage> mImage; // Cache is used only for HW Bitmaps with Skia pipeline.
+
+ // for tracing total number and memory usage of bitmaps
+ static std::mutex mLock;
+ static size_t mTotalBitmapBytes;
+ static size_t mTotalBitmapCount;
+ void traceBitmapCreate();
+ void traceBitmapDelete();
};
} // namespace android
diff --git a/libs/hwui/hwui/DrawTextFunctor.h b/libs/hwui/hwui/DrawTextFunctor.h
index d7bf201..e13e1365 100644
--- a/libs/hwui/hwui/DrawTextFunctor.h
+++ b/libs/hwui/hwui/DrawTextFunctor.h
@@ -73,6 +73,7 @@
}
paint->setStrokeJoin(SkPaint::kRound_Join);
paint->setLooper(nullptr);
+ paint->setBlendMode(SkBlendMode::kSrcOver);
}
class DrawTextFunctor {
diff --git a/location/java/android/location/LocationManager.java b/location/java/android/location/LocationManager.java
index 6342489..306643d 100644
--- a/location/java/android/location/LocationManager.java
+++ b/location/java/android/location/LocationManager.java
@@ -451,7 +451,7 @@
private static final long MAX_SINGLE_LOCATION_TIMEOUT_MS = 30 * 1000;
private static final String CACHE_KEY_LOCATION_ENABLED_PROPERTY =
- "cache_key.location_enabled";
+ PropertyInvalidatedCache.createSystemCacheKey("location_enabled");
static ILocationManager getService() throws RemoteException {
try {
diff --git a/location/java/android/location/flags/location.aconfig b/location/java/android/location/flags/location.aconfig
index cddc337..c3cb492 100644
--- a/location/java/android/location/flags/location.aconfig
+++ b/location/java/android/location/flags/location.aconfig
@@ -94,6 +94,16 @@
}
flag {
+ name: "use_legacy_ntp_time"
+ namespace: "location"
+ description: "Flag for switching to legacy NtpNetworkTimeHelper"
+ bug: "368034558"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "subscriptions_changed_listener_thread"
namespace: "location"
description: "Flag for running onSubscriptionsChangedListener on FgThread"
diff --git a/nfc/api/system-current.txt b/nfc/api/system-current.txt
index bc8a7af..9603c0a 100644
--- a/nfc/api/system-current.txt
+++ b/nfc/api/system-current.txt
@@ -60,8 +60,13 @@
method @FlaggedApi("android.nfc.nfc_oem_extension") @NonNull public java.util.List<java.lang.String> getActiveNfceeList();
method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void maybeTriggerFirmwareUpdate();
method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void registerCallback(@NonNull java.util.concurrent.Executor, @NonNull android.nfc.NfcOemExtension.Callback);
+ method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.NFC_SET_CONTROLLER_ALWAYS_ON) public void setControllerAlwaysOn(int);
method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void synchronizeScreenState();
method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void unregisterCallback(@NonNull android.nfc.NfcOemExtension.Callback);
+ field @FlaggedApi("android.nfc.nfc_oem_extension") public static final int DISABLE = 0; // 0x0
+ field @FlaggedApi("android.nfc.nfc_oem_extension") public static final int ENABLE_DEFAULT = 1; // 0x1
+ field @FlaggedApi("android.nfc.nfc_oem_extension") public static final int ENABLE_EE = 3; // 0x3
+ field @FlaggedApi("android.nfc.nfc_oem_extension") public static final int ENABLE_TRANSPARENT = 2; // 0x2
field public static final int HCE_ACTIVATE = 1; // 0x1
field public static final int HCE_DATA_TRANSFERRED = 2; // 0x2
field public static final int HCE_DEACTIVATE = 3; // 0x3
diff --git a/nfc/java/android/nfc/INfcAdapter.aidl b/nfc/java/android/nfc/INfcAdapter.aidl
index e2ec952..246efc7 100644
--- a/nfc/java/android/nfc/INfcAdapter.aidl
+++ b/nfc/java/android/nfc/INfcAdapter.aidl
@@ -73,7 +73,7 @@
boolean setNfcSecure(boolean enable);
NfcAntennaInfo getNfcAntennaInfo();
- boolean setControllerAlwaysOn(boolean value);
+ void setControllerAlwaysOn(int mode);
boolean isControllerAlwaysOn();
boolean isControllerAlwaysOnSupported();
void registerControllerAlwaysOnListener(in INfcControllerAlwaysOnListener listener);
diff --git a/nfc/java/android/nfc/NfcAdapter.java b/nfc/java/android/nfc/NfcAdapter.java
index f478793..de85f1e 100644
--- a/nfc/java/android/nfc/NfcAdapter.java
+++ b/nfc/java/android/nfc/NfcAdapter.java
@@ -559,6 +559,18 @@
@Retention(RetentionPolicy.SOURCE)
public @interface TagIntentAppPreferenceResult {}
+ /**
+ * Mode Type for {@link NfcOemExtension#setControllerAlwaysOn(int)}.
+ * @hide
+ */
+ public static final int CONTROLLER_ALWAYS_ON_MODE_DEFAULT = 1;
+
+ /**
+ * Mode Type for {@link NfcOemExtension#setControllerAlwaysOn(int)}.
+ * @hide
+ */
+ public static final int CONTROLLER_ALWAYS_ON_DISABLE = 0;
+
// Guarded by sLock
static boolean sIsInitialized = false;
static boolean sHasNfcFeature;
@@ -2330,7 +2342,8 @@
* FEATURE_NFC_HOST_CARD_EMULATION, FEATURE_NFC_HOST_CARD_EMULATION_NFCF,
* FEATURE_NFC_OFF_HOST_CARD_EMULATION_UICC and FEATURE_NFC_OFF_HOST_CARD_EMULATION_ESE
* are unavailable
- * @return void
+ * @return true if feature is supported by the device and operation has bee initiated,
+ * false if the feature is not supported by the device.
* @hide
*/
@SystemApi
@@ -2339,8 +2352,13 @@
if (!sHasNfcFeature && !sHasCeFeature) {
throw new UnsupportedOperationException();
}
- return callServiceReturn(() -> sService.setControllerAlwaysOn(value), false);
-
+ int mode = value ? CONTROLLER_ALWAYS_ON_MODE_DEFAULT : CONTROLLER_ALWAYS_ON_DISABLE;
+ try {
+ callService(() -> sService.setControllerAlwaysOn(mode));
+ } catch (UnsupportedOperationException e) {
+ return false;
+ }
+ return true;
}
/**
diff --git a/nfc/java/android/nfc/NfcOemExtension.java b/nfc/java/android/nfc/NfcOemExtension.java
index d51b704..45038d4 100644
--- a/nfc/java/android/nfc/NfcOemExtension.java
+++ b/nfc/java/android/nfc/NfcOemExtension.java
@@ -70,6 +70,58 @@
private boolean mRfDiscoveryStarted = false;
/**
+ * Mode Type for {@link #setControllerAlwaysOn(int)}.
+ * Enables the controller in default mode when NFC is disabled (existing API behavior).
+ * works same as {@link NfcAdapter#setControllerAlwaysOn(boolean)}.
+ * @hide
+ */
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
+ public static final int ENABLE_DEFAULT = NfcAdapter.CONTROLLER_ALWAYS_ON_MODE_DEFAULT;
+
+ /**
+ * Mode Type for {@link #setControllerAlwaysOn(int)}.
+ * Enables the controller in transparent mode when NFC is disabled.
+ * @hide
+ */
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
+ public static final int ENABLE_TRANSPARENT = 2;
+
+ /**
+ * Mode Type for {@link #setControllerAlwaysOn(int)}.
+ * Enables the controller and initializes and enables the EE subsystem when NFC is disabled.
+ * @hide
+ */
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
+ public static final int ENABLE_EE = 3;
+
+ /**
+ * Mode Type for {@link #setControllerAlwaysOn(int)}.
+ * Disable the Controller Always On Mode.
+ * works same as {@link NfcAdapter#setControllerAlwaysOn(boolean)}.
+ * @hide
+ */
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
+ public static final int DISABLE = NfcAdapter.CONTROLLER_ALWAYS_ON_DISABLE;
+
+ /**
+ * Possible controller modes for {@link #setControllerAlwaysOn(int)}.
+ *
+ * @hide
+ */
+ @IntDef(prefix = { "" }, value = {
+ ENABLE_DEFAULT,
+ ENABLE_TRANSPARENT,
+ ENABLE_EE,
+ DISABLE,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ControllerMode{}
+
+ /**
* Event that Host Card Emulation is activated.
*/
public static final int HCE_ACTIVATE = 1;
@@ -389,6 +441,32 @@
NfcAdapter.sService.fetchActiveNfceeList(), new ArrayList<String>());
}
+ /**
+ * Sets NFC controller always on feature.
+ * <p>This API is for the NFCC internal state management. It allows to discriminate
+ * the controller function from the NFC function by keeping the NFC controller on without
+ * any NFC RF enabled if necessary.
+ * <p>This call is asynchronous, register listener {@link NfcAdapter.ControllerAlwaysOnListener}
+ * by {@link NfcAdapter#registerControllerAlwaysOnListener} to find out when the operation is
+ * complete.
+ * @param mode one of {@link ControllerMode} modes
+ * @throws UnsupportedOperationException if
+ * <li> if FEATURE_NFC, FEATURE_NFC_HOST_CARD_EMULATION, FEATURE_NFC_HOST_CARD_EMULATION_NFCF,
+ * FEATURE_NFC_OFF_HOST_CARD_EMULATION_UICC and FEATURE_NFC_OFF_HOST_CARD_EMULATION_ESE
+ * are unavailable </li>
+ * <li> if the feature is unavailable @see NfcAdapter#isNfcControllerAlwaysOnSupported() </li>
+ * @hide
+ */
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
+ @RequiresPermission(android.Manifest.permission.NFC_SET_CONTROLLER_ALWAYS_ON)
+ public void setControllerAlwaysOn(@ControllerMode int mode) {
+ if (!NfcAdapter.sHasNfcFeature && !NfcAdapter.sHasCeFeature) {
+ throw new UnsupportedOperationException();
+ }
+ NfcAdapter.callService(() -> NfcAdapter.sService.setControllerAlwaysOn(mode));
+ }
+
private final class NfcOemExtensionCallback extends INfcOemExtensionCallback.Stub {
@Override
diff --git a/packages/PackageInstaller/Android.bp b/packages/PackageInstaller/Android.bp
index bd84b58..a30c0c3 100644
--- a/packages/PackageInstaller/Android.bp
+++ b/packages/PackageInstaller/Android.bp
@@ -13,6 +13,7 @@
// limitations under the License.
package {
+ default_team: "trendy_team_framework_android_packages",
default_applicable_licenses: [
"frameworks_base_packages_PackageInstaller_license",
],
diff --git a/packages/PackageInstaller/TEST_MAPPING b/packages/PackageInstaller/TEST_MAPPING
index 717ec02..91882fd 100644
--- a/packages/PackageInstaller/TEST_MAPPING
+++ b/packages/PackageInstaller/TEST_MAPPING
@@ -65,6 +65,17 @@
"name": "CtsIntentSignatureTestCases"
},
{
+ "name": "CtsPackageInstallerCUJDeviceAdminTestCases",
+ "options":[
+ {
+ "exclude-annotation":"androidx.test.filters.FlakyTest"
+ },
+ {
+ "exclude-annotation":"org.junit.Ignore"
+ }
+ ]
+ },
+ {
"name": "CtsPackageInstallerCUJInstallationTestCases",
"options":[
{
@@ -76,6 +87,17 @@
]
},
{
+ "name": "CtsPackageInstallerCUJMultiUsersTestCases",
+ "options":[
+ {
+ "exclude-annotation":"androidx.test.filters.FlakyTest"
+ },
+ {
+ "exclude-annotation":"org.junit.Ignore"
+ }
+ ]
+ },
+ {
"name": "CtsPackageInstallerCUJUninstallationTestCases",
"options":[
{
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialog.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialog.kt
index 022dded..265864e 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialog.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialog.kt
@@ -17,11 +17,16 @@
package com.android.settingslib.spa.widget.dialog
import android.content.res.Configuration
+import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Button
+import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
@@ -32,9 +37,11 @@
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
+import com.android.settingslib.spa.framework.theme.isSpaExpressiveEnabled
data class AlertDialogButton(
val text: String,
@@ -85,27 +92,41 @@
AlertDialog(
onDismissRequest = ::close,
modifier = Modifier.width(getDialogWidth()),
- confirmButton = { confirmButton?.let { Button(it) } },
- dismissButton = dismissButton?.let { { Button(it) } },
- title = title?.let { { Text(it) } },
- text = text?.let {
- {
- Column(Modifier.verticalScroll(rememberScrollState())) {
- text()
- }
- }
+ confirmButton = {
+ confirmButton?.let { if (isSpaExpressiveEnabled) ConfirmButton(it) else Button(it) }
},
+ dismissButton =
+ dismissButton?.let {
+ { if (isSpaExpressiveEnabled) DismissButton(it) else Button(it) }
+ },
+ title = title?.let { { CenterRow { Text(it) } } },
+ text =
+ text?.let {
+ { CenterRow { Column(Modifier.verticalScroll(rememberScrollState())) { text() } } }
+ },
properties = DialogProperties(usePlatformDefaultWidth = false),
)
}
@Composable
+internal fun CenterRow(content: @Composable (() -> Unit)) {
+ if (isSpaExpressiveEnabled) {
+ Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
+ content()
+ }
+ } else {
+ content()
+ }
+}
+
+@Composable
fun getDialogWidth(): Dp {
val configuration = LocalConfiguration.current
- return configuration.screenWidthDp.dp * when (configuration.orientation) {
- Configuration.ORIENTATION_LANDSCAPE -> 0.65f
- else -> 0.85f
- }
+ return configuration.screenWidthDp.dp *
+ when (configuration.orientation) {
+ Configuration.ORIENTATION_LANDSCAPE -> 0.65f
+ else -> 0.85f
+ }
}
@Composable
@@ -120,3 +141,47 @@
Text(button.text)
}
}
+
+@Composable
+private fun AlertDialogPresenter.DismissButton(button: AlertDialogButton) {
+ OutlinedButton(
+ onClick = {
+ close()
+ button.onClick()
+ },
+ enabled = button.enabled,
+ ) {
+ Text(button.text)
+ }
+}
+
+@Composable
+private fun AlertDialogPresenter.ConfirmButton(button: AlertDialogButton) {
+ Button(
+ onClick = {
+ close()
+ button.onClick()
+ },
+ enabled = button.enabled,
+ ) {
+ Text(button.text)
+ }
+}
+
+@Preview
+@Composable
+private fun AlertDialogPreview() {
+ val alertDialogPresenter = remember {
+ object : AlertDialogPresenter {
+ override fun open() {}
+
+ override fun close() {}
+ }
+ }
+ alertDialogPresenter.SettingsAlertDialog(
+ confirmButton = AlertDialogButton("Ok"),
+ dismissButton = AlertDialogButton("Cancel"),
+ title = "Title",
+ text = { Text("Text") },
+ )
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialogWithIcon.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialogWithIcon.kt
index 030522d..58a83fa 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialogWithIcon.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialogWithIcon.kt
@@ -40,10 +40,7 @@
dismissButton: AlertDialogButton?,
title: String?,
icon: @Composable (() -> Unit)? = {
- Icon(
- Icons.Default.WarningAmber,
- contentDescription = null
- )
+ Icon(Icons.Default.WarningAmber, contentDescription = null)
},
text: @Composable (() -> Unit)?,
) {
@@ -52,43 +49,22 @@
icon = icon,
modifier = Modifier.width(getDialogWidth()),
confirmButton = {
- confirmButton?.let {
- Button(
- onClick = {
- it.onClick()
- },
- ) {
- Text(it.text)
+ confirmButton?.let { Button(onClick = { it.onClick() }) { Text(it.text) } }
+ },
+ dismissButton =
+ dismissButton?.let { { OutlinedButton(onClick = { it.onClick() }) { Text(it.text) } } },
+ title =
+ title?.let {
+ {
+ CenterRow {
+ Text(it, modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center)
+ }
}
- }
- },
- dismissButton = dismissButton?.let {
- {
- OutlinedButton(
- onClick = {
- it.onClick()
- },
- ) {
- Text(it.text)
- }
- }
- },
- title = title?.let {
- {
- Text(
- it,
- modifier = Modifier.fillMaxWidth(),
- textAlign = TextAlign.Center
- )
- }
- },
- text = text?.let {
- {
- Column(Modifier.verticalScroll(rememberScrollState())) {
- text()
- }
- }
- },
+ },
+ text =
+ text?.let {
+ { CenterRow { Column(Modifier.verticalScroll(rememberScrollState())) { text() } } }
+ },
properties = DialogProperties(usePlatformDefaultWidth = false),
)
-}
\ No newline at end of file
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlterDialogContent.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlterDialogContent.kt
index bef0bca..9f2210d 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlterDialogContent.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlterDialogContent.kt
@@ -58,10 +58,7 @@
dismissButton: AlertDialogButton?,
title: String?,
icon: @Composable (() -> Unit)? = {
- Icon(
- Icons.Default.WarningAmber,
- contentDescription = null
- )
+ Icon(Icons.Default.WarningAmber, contentDescription = null)
},
text: @Composable (() -> Unit)?,
) {
@@ -69,42 +66,22 @@
buttons = {
AlertDialogFlowRow(
mainAxisSpacing = ButtonsMainAxisSpacing,
- crossAxisSpacing = ButtonsCrossAxisSpacing
+ crossAxisSpacing = ButtonsCrossAxisSpacing,
) {
- dismissButton?.let {
- OutlinedButton(onClick = it.onClick) {
- Text(it.text)
- }
- }
- confirmButton?.let {
- Button(
- onClick = {
- it.onClick()
- },
- ) {
- Text(it.text)
- }
- }
+ dismissButton?.let { OutlinedButton(onClick = it.onClick) { Text(it.text) } }
+ confirmButton?.let { Button(onClick = { it.onClick() }) { Text(it.text) } }
}
},
icon = icon,
modifier = Modifier.width(getDialogWidth()),
- title = title?.let {
- {
- Text(
- it,
- modifier = Modifier.fillMaxWidth(),
- textAlign = TextAlign.Center
- )
- }
- },
- text = text?.let {
- {
- Column(Modifier.verticalScroll(rememberScrollState())) {
- text()
- }
- }
- },
+ title =
+ title?.let {
+ { Text(it, modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center) }
+ },
+ text =
+ text?.let {
+ { CenterRow { Column(Modifier.verticalScroll(rememberScrollState())) { text() } } }
+ },
)
}
@@ -121,18 +98,12 @@
shape = SettingsShape.CornerExtraLarge,
color = MaterialTheme.colorScheme.surfaceContainerHigh,
) {
- Column(
- modifier = Modifier.padding(DialogPadding)
- ) {
+ Column(modifier = Modifier.padding(DialogPadding)) {
icon?.let {
CompositionLocalProvider(
- LocalContentColor provides AlertDialogDefaults.iconContentColor,
+ LocalContentColor provides AlertDialogDefaults.iconContentColor
) {
- Box(
- Modifier
- .padding(IconPadding)
- .align(Alignment.CenterHorizontally)
- ) {
+ Box(Modifier.padding(IconPadding).align(Alignment.CenterHorizontally)) {
icon()
}
}
@@ -144,8 +115,7 @@
) {
Box(
// Align the title to the center when an icon is present.
- Modifier
- .padding(TitlePadding)
+ Modifier.padding(TitlePadding)
.align(
if (icon == null) {
Alignment.Start
@@ -161,11 +131,10 @@
text?.let {
ProvideContentColorTextStyle(
contentColor = AlertDialogDefaults.textContentColor,
- textStyle = MaterialTheme.typography.bodyMedium
+ textStyle = MaterialTheme.typography.bodyMedium,
) {
Box(
- Modifier
- .weight(weight = 1f, fill = false)
+ Modifier.weight(weight = 1f, fill = false)
.padding(TextPadding)
.align(Alignment.Start)
) {
@@ -177,7 +146,7 @@
ProvideContentColorTextStyle(
contentColor = MaterialTheme.colorScheme.primary,
textStyle = MaterialTheme.typography.labelLarge,
- content = buttons
+ content = buttons,
)
}
}
@@ -188,7 +157,7 @@
internal fun AlertDialogFlowRow(
mainAxisSpacing: Dp,
crossAxisSpacing: Dp,
- content: @Composable () -> Unit
+ content: @Composable () -> Unit,
) {
Layout(content) { measurables, constraints ->
val sequences = mutableListOf<List<Placeable>>()
@@ -204,8 +173,9 @@
// Return whether the placeable can be added to the current sequence.
fun canAddToCurrentSequence(placeable: Placeable) =
- currentSequence.isEmpty() || currentMainAxisSize + mainAxisSpacing.roundToPx() +
- placeable.width <= constraints.maxWidth
+ currentSequence.isEmpty() ||
+ currentMainAxisSize + mainAxisSpacing.roundToPx() + placeable.width <=
+ constraints.maxWidth
// Store current sequence information and start a new sequence.
fun startNewSequence() {
@@ -213,8 +183,7 @@
crossAxisSpace += crossAxisSpacing.roundToPx()
}
// Ensures that confirming actions appear above dismissive actions.
- @Suppress("ListIterator")
- sequences.add(0, currentSequence.toList())
+ @Suppress("ListIterator") sequences.add(0, currentSequence.toList())
crossAxisSizes += currentCrossAxisSize
crossAxisPositions += crossAxisSpace
@@ -254,23 +223,23 @@
layout(layoutWidth, layoutHeight) {
sequences.fastForEachIndexed { i, placeables ->
- val childrenMainAxisSizes = IntArray(placeables.size) { j ->
- placeables[j].width +
- if (j < placeables.lastIndex) mainAxisSpacing.roundToPx() else 0
- }
+ val childrenMainAxisSizes =
+ IntArray(placeables.size) { j ->
+ placeables[j].width +
+ if (j < placeables.lastIndex) mainAxisSpacing.roundToPx() else 0
+ }
val arrangement = Arrangement.End
val mainAxisPositions = IntArray(childrenMainAxisSizes.size) { 0 }
with(arrangement) {
arrange(
- mainAxisLayoutSize, childrenMainAxisSizes,
- layoutDirection, mainAxisPositions
+ mainAxisLayoutSize,
+ childrenMainAxisSizes,
+ layoutDirection,
+ mainAxisPositions,
)
}
placeables.fastForEachIndexed { j, placeable ->
- placeable.place(
- x = mainAxisPositions[j],
- y = crossAxisPositions[i]
- )
+ placeable.place(x = mainAxisPositions[j], y = crossAxisPositions[i])
}
}
}
@@ -289,8 +258,8 @@
/**
* ProvideContentColorTextStyle
*
- * A convenience method to provide values to both LocalContentColor and LocalTextStyle in
- * one call. This is less expensive than nesting calls to CompositionLocalProvider.
+ * A convenience method to provide values to both LocalContentColor and LocalTextStyle in one call.
+ * This is less expensive than nesting calls to CompositionLocalProvider.
*
* Text styles will be merged with the current value of LocalTextStyle.
*/
@@ -298,12 +267,12 @@
private fun ProvideContentColorTextStyle(
contentColor: Color,
textStyle: TextStyle,
- content: @Composable () -> Unit
+ content: @Composable () -> Unit,
) {
val mergedStyle = LocalTextStyle.current.merge(textStyle)
CompositionLocalProvider(
LocalContentColor provides contentColor,
LocalTextStyle provides mergedStyle,
- content = content
+ content = content,
)
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
index 6f2567b..a3f9e51 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
@@ -80,7 +80,9 @@
"com.android.settings.action.BLUETOOTH_LE_AUDIO_SHARING_STATE_CHANGE";
public static final String EXTRA_LE_AUDIO_SHARING_STATE = "BLUETOOTH_LE_AUDIO_SHARING_STATE";
public static final String EXTRA_BLUETOOTH_DEVICE = "BLUETOOTH_DEVICE";
+ public static final String EXTRA_BT_DEVICE_TO_AUTO_ADD_SOURCE = "BT_DEVICE_TO_AUTO_ADD_SOURCE";
public static final String EXTRA_START_LE_AUDIO_SHARING = "START_LE_AUDIO_SHARING";
+ public static final String EXTRA_PAIR_AND_JOIN_SHARING = "PAIR_AND_JOIN_SHARING";
public static final int BROADCAST_STATE_UNKNOWN = 0;
public static final int BROADCAST_STATE_ON = 1;
public static final int BROADCAST_STATE_OFF = 2;
@@ -1224,7 +1226,7 @@
Intent intent = new Intent(ACTION_LE_AUDIO_SHARING_STATE_CHANGE);
intent.putExtra(EXTRA_LE_AUDIO_SHARING_STATE, state);
intent.setPackage(mContext.getPackageName());
- Log.e(TAG, "notifyBroadcastStateChange for state = " + state);
+ Log.d(TAG, "notifyBroadcastStateChange for state = " + state);
mContext.sendBroadcast(intent);
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/MultiTogglePreference.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/MultiTogglePreference.java
index 01bb6f0..7ee7180 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/MultiTogglePreference.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/MultiTogglePreference.java
@@ -33,6 +33,7 @@
private final String mTitle;
private final ImmutableList<ToggleInfo> mToggleInfos;
private final int mState;
+ private final boolean mIsActive;
private final boolean mIsAllowedChangingState;
private final Bundle mExtras;
@@ -40,6 +41,7 @@
@NonNull String title,
List<ToggleInfo> toggleInfos,
int state,
+ boolean isActive,
boolean allowChangingState,
Bundle extras) {
super(DeviceSettingType.DEVICE_SETTING_TYPE_MULTI_TOGGLE);
@@ -47,6 +49,7 @@
mTitle = title;
mToggleInfos = ImmutableList.copyOf(toggleInfos);
mState = state;
+ mIsActive = isActive;
mIsAllowedChangingState = allowChangingState;
mExtras = extras;
}
@@ -67,9 +70,11 @@
List<ToggleInfo> toggleInfos = new ArrayList<>();
in.readTypedList(toggleInfos, ToggleInfo.CREATOR);
int state = in.readInt();
+ boolean isActive = in.readBoolean();
boolean allowChangingState = in.readBoolean();
Bundle extras = in.readBundle(Bundle.class.getClassLoader());
- return new MultiTogglePreference(title, toggleInfos, state, allowChangingState, extras);
+ return new MultiTogglePreference(
+ title, toggleInfos, state, isActive, allowChangingState, extras);
}
public static final Creator<MultiTogglePreference> CREATOR =
@@ -99,6 +104,7 @@
dest.writeString(mTitle);
dest.writeTypedList(mToggleInfos, flags);
dest.writeInt(mState);
+ dest.writeBoolean(mIsActive);
dest.writeBoolean(mIsAllowedChangingState);
dest.writeBundle(mExtras);
}
@@ -108,6 +114,7 @@
private String mTitle;
private ImmutableList.Builder<ToggleInfo> mToggleInfos = new ImmutableList.Builder<>();
private int mState;
+ private boolean mIsActive;
private boolean mAllowChangingState;
private Bundle mExtras = Bundle.EMPTY;
@@ -148,6 +155,19 @@
}
/**
+ * Sets whether the current state is considered as an "active" state. If it's set to true,
+ * the toggle will be highlighted in UI.
+ *
+ * @param isActive The active state.
+ * @return Returns the Builder object.
+ */
+ @NonNull
+ public Builder setIsActive(boolean isActive) {
+ mIsActive = isActive;
+ return this;
+ }
+
+ /**
* Sets whether state can be changed by user.
*
* @param allowChangingState Whether user is allowed to change state.
@@ -178,7 +198,7 @@
@NonNull
public MultiTogglePreference build() {
return new MultiTogglePreference(
- mTitle, mToggleInfos.build(), mState, mAllowChangingState, mExtras);
+ mTitle, mToggleInfos.build(), mState, mIsActive, mAllowChangingState, mExtras);
}
}
@@ -202,6 +222,16 @@
}
/**
+ * Whether the current state is considered as an active state. If it's set to true, the toggle
+ * will be highlighted in UI.
+ *
+ * @return Returns the active state.
+ */
+ public boolean isActive() {
+ return mIsActive;
+ }
+
+ /**
* Gets the toggle list in the preference.
*
* @return the toggle list.
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepository.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepository.kt
index 29664f6..851b614 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepository.kt
@@ -159,7 +159,7 @@
title = pref.title,
toggles = pref.toggleInfos.map { it.toModel() },
isAllowedChangingState = pref.isAllowedChangingState,
- isActive = true,
+ isActive = pref.isActive,
state = DeviceSettingStateModel.MultiTogglePreferenceState(pref.state),
updateState = { newState ->
coroutineScope.launch(backgroundCoroutineContext) {
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt
index 7eae5b2..3d8ff86 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt
@@ -56,11 +56,13 @@
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
+import kotlinx.coroutines.plus
import kotlinx.coroutines.withContext
@OptIn(ExperimentalCoroutinesApi::class)
@@ -101,8 +103,7 @@
} else if (allStatus.all { it is ServiceConnectionStatus.Connected }) {
allStatus
.filterIsInstance<
- ServiceConnectionStatus.Connected<
- IDeviceSettingsProviderService>
+ ServiceConnectionStatus.Connected<IDeviceSettingsProviderService>
>()
.all { it.service.serviceStatus?.enabled == true }
} else {
@@ -232,7 +233,7 @@
IDeviceSettingsProviderService.Stub::asInterface,
)
.stateIn(
- coroutineScope,
+ coroutineScope.plus(backgroundCoroutineContext),
SharingStarted.WhileSubscribed(),
ServiceConnectionStatus.Connecting,
)
@@ -263,21 +264,30 @@
transform: ((IBinder) -> T),
): Flow<ServiceConnectionStatus<T>> {
return callbackFlow {
- val serviceConnection =
- object : ServiceConnection {
- override fun onServiceConnected(name: ComponentName, service: IBinder) {
- launch { send(ServiceConnectionStatus.Connected(transform(service))) }
- }
+ val serviceConnection =
+ object : ServiceConnection {
+ override fun onServiceConnected(name: ComponentName, service: IBinder) {
+ launch { send(ServiceConnectionStatus.Connected(transform(service))) }
+ }
- override fun onServiceDisconnected(name: ComponentName?) {
- launch { send(ServiceConnectionStatus.Connecting) }
+ override fun onServiceDisconnected(name: ComponentName?) {
+ launch { send(ServiceConnectionStatus.Connecting) }
+ }
}
+ if (
+ !context.bindService(
+ intent,
+ Context.BIND_AUTO_CREATE,
+ { launch { it.run() } },
+ serviceConnection,
+ )
+ ) {
+ Log.w(TAG, "Fail to bind service $intent")
+ launch { send(ServiceConnectionStatus.Failed) }
}
- if (!context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)) {
- launch { send(ServiceConnectionStatus.Failed) }
+ awaitClose { context.unbindService(serviceConnection) }
}
- awaitClose { context.unbindService(serviceConnection) }
- }
+ .flowOn(backgroundCoroutineContext)
}
private suspend fun tryGetEndpointFromMetadata(cachedDevice: CachedBluetoothDevice): EndPoint? =
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InputMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/InputMediaDevice.java
index 766cd43..9dd2dbb 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/InputMediaDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/InputMediaDevice.java
@@ -80,6 +80,10 @@
context, id, audioDeviceInfoType, maxVolume, currentVolume, isVolumeFixed);
}
+ public @AudioDeviceType int getAudioDeviceInfoType() {
+ return mAudioDeviceInfoType;
+ }
+
public static boolean isSupportedInputDevice(@AudioDeviceType int audioDeviceInfoType) {
return switch (audioDeviceInfoType) {
case TYPE_BUILTIN_MIC,
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java
index 874e030..0c50166 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java
@@ -15,13 +15,20 @@
*/
package com.android.settingslib.media;
+import static com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_SELECTED;
+
import android.content.Context;
+import android.media.AudioAttributes;
+import android.media.AudioDeviceAttributes;
import android.media.AudioDeviceCallback;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
+import android.media.MediaRecorder;
import android.os.Handler;
+import android.util.Slog;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
@@ -35,12 +42,18 @@
private static final String TAG = "InputRouteManager";
+ @VisibleForTesting
+ static final AudioAttributes INPUT_ATTRIBUTES =
+ new AudioAttributes.Builder().setCapturePreset(MediaRecorder.AudioSource.MIC).build();
+
private final Context mContext;
private final AudioManager mAudioManager;
@VisibleForTesting final List<MediaDevice> mInputMediaDevices = new CopyOnWriteArrayList<>();
+ private MediaDevice mSelectedInputDevice;
+
private final Collection<InputDeviceCallback> mCallbacks = new CopyOnWriteArrayList<>();
@VisibleForTesting
@@ -76,8 +89,27 @@
mCallbacks.remove(callback);
}
+ public @Nullable MediaDevice getSelectedInputDevice() {
+ return mSelectedInputDevice;
+ }
+
private void dispatchInputDeviceListUpdate() {
- // TODO (b/360175574): Get selected input device.
+ // Get selected input device.
+ List<AudioDeviceAttributes> attributesOfSelectedInputDevices =
+ mAudioManager.getDevicesForAttributes(INPUT_ATTRIBUTES);
+ int selectedInputDeviceAttributesType;
+ if (attributesOfSelectedInputDevices.isEmpty()) {
+ Slog.e(TAG, "Unexpected empty list of input devices. Using built-in mic.");
+ selectedInputDeviceAttributesType = AudioDeviceInfo.TYPE_BUILTIN_MIC;
+ } else {
+ if (attributesOfSelectedInputDevices.size() > 1) {
+ Slog.w(
+ TAG,
+ "AudioManager.getDevicesForAttributes returned more than one element."
+ + " Using the first one.");
+ }
+ selectedInputDeviceAttributesType = attributesOfSelectedInputDevices.get(0).getType();
+ }
// Get all input devices.
AudioDeviceInfo[] audioDeviceInfos =
@@ -93,6 +125,10 @@
getCurrentInputGain(),
isInputGainFixed());
if (mediaDevice != null) {
+ if (info.getType() == selectedInputDeviceAttributesType) {
+ mediaDevice.setState(STATE_SELECTED);
+ mSelectedInputDevice = mediaDevice;
+ }
mInputMediaDevices.add(mediaDevice);
}
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/ConversationIconFactory.java b/packages/SettingsLib/src/com/android/settingslib/notification/ConversationIconFactory.java
index 251cd36..9a9a960 100644
--- a/packages/SettingsLib/src/com/android/settingslib/notification/ConversationIconFactory.java
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/ConversationIconFactory.java
@@ -87,7 +87,7 @@
* Returns the conversation info drawable
*/
public Drawable getBaseIconDrawable(ShortcutInfo shortcutInfo) {
- return mLauncherApps.getShortcutIconDrawable(shortcutInfo, mFillResIconDpi);
+ return mLauncherApps.getShortcutIconDrawable(shortcutInfo, mFullResIconDpi);
}
/**
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/MultiTogglePreferenceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/MultiTogglePreferenceTest.java
index 62fcb5e..1c7b5bc 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/MultiTogglePreferenceTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/MultiTogglePreferenceTest.java
@@ -120,6 +120,7 @@
.addToggleInfo(TOGGLE_INFO_1)
.addToggleInfo(TOGGLE_INFO_2)
.setState(123)
+ .setIsActive(true)
.setAllowChangingState(true)
.setExtras(buildBundle("key1", "value1"))
.build();
@@ -130,6 +131,7 @@
assertThat(fromParcel.getToggleInfos().stream().map(ToggleInfo::getLabel).toList())
.containsExactly("label1", "label2");
assertThat(fromParcel.getState()).isEqualTo(preference.getState());
+ assertThat(fromParcel.isActive()).isEqualTo(preference.isActive());
assertThat(fromParcel.isAllowedChangingState())
.isEqualTo(preference.isAllowedChangingState());
assertThat(fromParcel.getExtras().getString("key1"))
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt
index 81b5634..0cb6bc1 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt
@@ -102,9 +102,9 @@
`when`(settingProviderService2.queryLocalInterface(anyString()))
.thenReturn(settingProviderService2)
- `when`(context.bindService(any(), any(), anyInt())).then { input ->
+ `when`(context.bindService(any(), anyInt(), any(), any())).then { input ->
val intent = input.getArgument<Intent?>(0)
- val connection = input.getArgument<ServiceConnection>(1)
+ val connection = input.getArgument<ServiceConnection>(3)
when (intent?.action) {
CONFIG_SERVICE_INTENT_ACTION ->
@@ -210,7 +210,7 @@
fun getDeviceSettingsConfig_bindingServiceFail_returnNull() {
testScope.runTest {
`when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG)
- doReturn(false).`when`(context).bindService(any(), any(), anyInt())
+ doReturn(false).`when`(context).bindService(any(), anyInt(), any(), any())
val config = underTest.getDeviceSettingsConfig(cachedDevice)
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputRouteManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputRouteManagerTest.java
index 2501ae6..8a18d07 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputRouteManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputRouteManagerTest.java
@@ -16,6 +16,8 @@
package com.android.settingslib.media;
+import static com.android.settingslib.media.InputRouteManager.INPUT_ATTRIBUTES;
+
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.mock;
@@ -23,6 +25,7 @@
import static org.mockito.Mockito.when;
import android.content.Context;
+import android.media.AudioDeviceAttributes;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
@@ -36,6 +39,10 @@
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
@RunWith(RobolectricTestRunner.class)
@Config(shadows = {ShadowRouter2Manager.class})
public class InputRouteManagerTest {
@@ -124,6 +131,97 @@
}
@Test
+ public void getSelectedInputDevice_returnOneFromAudioManager() {
+ final AudioDeviceInfo info1 = mock(AudioDeviceInfo.class);
+ when(info1.getType()).thenReturn(AudioDeviceInfo.TYPE_WIRED_HEADSET);
+ when(info1.getId()).thenReturn(INPUT_WIRED_HEADSET_ID);
+
+ final AudioDeviceInfo info2 = mock(AudioDeviceInfo.class);
+ when(info2.getType()).thenReturn(AudioDeviceInfo.TYPE_BUILTIN_MIC);
+ when(info2.getId()).thenReturn(BUILTIN_MIC_ID);
+
+ final AudioManager audioManager = mock(AudioManager.class);
+ AudioDeviceInfo[] devices = {info1, info2};
+ when(audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).thenReturn(devices);
+
+ // Mock audioManager.getDevicesForAttributes returns exactly one audioDeviceAttributes.
+ AudioDeviceAttributes audioDeviceAttributes = new AudioDeviceAttributes(info1);
+ when(audioManager.getDevicesForAttributes(INPUT_ATTRIBUTES))
+ .thenReturn(Collections.singletonList(audioDeviceAttributes));
+
+ InputRouteManager inputRouteManager = new InputRouteManager(mContext, audioManager);
+ inputRouteManager.mAudioDeviceCallback.onAudioDevicesAdded(devices);
+
+ // The selected input device has the same type as the one returned from AudioManager.
+ InputMediaDevice selectedInputDevice =
+ (InputMediaDevice) inputRouteManager.getSelectedInputDevice();
+ assertThat(selectedInputDevice.getAudioDeviceInfoType())
+ .isEqualTo(AudioDeviceInfo.TYPE_WIRED_HEADSET);
+ }
+
+ @Test
+ public void getSelectedInputDevice_returnMoreThanOneFromAudioManager() {
+ final AudioDeviceInfo info1 = mock(AudioDeviceInfo.class);
+ when(info1.getType()).thenReturn(AudioDeviceInfo.TYPE_WIRED_HEADSET);
+ when(info1.getId()).thenReturn(INPUT_WIRED_HEADSET_ID);
+
+ final AudioDeviceInfo info2 = mock(AudioDeviceInfo.class);
+ when(info2.getType()).thenReturn(AudioDeviceInfo.TYPE_BUILTIN_MIC);
+ when(info2.getId()).thenReturn(BUILTIN_MIC_ID);
+
+ final AudioManager audioManager = mock(AudioManager.class);
+ AudioDeviceInfo[] devices = {info1, info2};
+ when(audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).thenReturn(devices);
+
+ // Mock audioManager.getDevicesForAttributes returns more than one audioDeviceAttributes.
+ AudioDeviceAttributes audioDeviceAttributes1 = new AudioDeviceAttributes(info1);
+ AudioDeviceAttributes audioDeviceAttributes2 = new AudioDeviceAttributes(info2);
+ List<AudioDeviceAttributes> attributesOfSelectedInputDevices = new ArrayList<>();
+ attributesOfSelectedInputDevices.add(audioDeviceAttributes1);
+ attributesOfSelectedInputDevices.add(audioDeviceAttributes2);
+ when(audioManager.getDevicesForAttributes(INPUT_ATTRIBUTES))
+ .thenReturn(attributesOfSelectedInputDevices);
+
+ InputRouteManager inputRouteManager = new InputRouteManager(mContext, audioManager);
+ inputRouteManager.mAudioDeviceCallback.onAudioDevicesAdded(devices);
+
+ // The selected input device has the same type as the first one returned from AudioManager.
+ InputMediaDevice selectedInputDevice =
+ (InputMediaDevice) inputRouteManager.getSelectedInputDevice();
+ assertThat(selectedInputDevice.getAudioDeviceInfoType())
+ .isEqualTo(AudioDeviceInfo.TYPE_WIRED_HEADSET);
+ }
+
+ @Test
+ public void getSelectedInputDevice_returnEmptyFromAudioManager() {
+ final AudioDeviceInfo info1 = mock(AudioDeviceInfo.class);
+ when(info1.getType()).thenReturn(AudioDeviceInfo.TYPE_WIRED_HEADSET);
+ when(info1.getId()).thenReturn(INPUT_WIRED_HEADSET_ID);
+
+ final AudioDeviceInfo info2 = mock(AudioDeviceInfo.class);
+ when(info2.getType()).thenReturn(AudioDeviceInfo.TYPE_BUILTIN_MIC);
+ when(info2.getId()).thenReturn(BUILTIN_MIC_ID);
+
+ final AudioManager audioManager = mock(AudioManager.class);
+ AudioDeviceInfo[] devices = {info1, info2};
+ when(audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).thenReturn(devices);
+
+ // Mock audioManager.getDevicesForAttributes returns empty list of audioDeviceAttributes.
+ List<AudioDeviceAttributes> attributesOfSelectedInputDevices = new ArrayList<>();
+ when(audioManager.getDevicesForAttributes(INPUT_ATTRIBUTES))
+ .thenReturn(attributesOfSelectedInputDevices);
+
+ InputRouteManager inputRouteManager = new InputRouteManager(mContext, audioManager);
+ inputRouteManager.mAudioDeviceCallback.onAudioDevicesAdded(devices);
+
+ // The selected input device has default type AudioDeviceInfo.TYPE_BUILTIN_MIC.
+ InputMediaDevice selectedInputDevice =
+ (InputMediaDevice) inputRouteManager.getSelectedInputDevice();
+ assertThat(selectedInputDevice.getAudioDeviceInfoType())
+ .isEqualTo(AudioDeviceInfo.TYPE_BUILTIN_MIC);
+ }
+
+ @Test
public void getMaxInputGain_returnMaxInputGain() {
assertThat(mInputRouteManager.getMaxInputGain()).isEqualTo(15);
}
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
index 40a8199..d710939 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
@@ -86,6 +86,7 @@
Settings.Secure.DOUBLE_TAP_TO_WAKE,
Settings.Secure.WAKE_GESTURE_ENABLED,
Settings.Secure.LONG_PRESS_TIMEOUT,
+ Settings.Secure.KEY_REPEAT_ENABLED,
Settings.Secure.KEY_REPEAT_TIMEOUT_MS,
Settings.Secure.KEY_REPEAT_DELAY_MS,
Settings.Secure.CAMERA_GESTURE_DISABLED,
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
index 3b9c683..fa16a44 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
@@ -133,6 +133,7 @@
VALIDATORS.put(Secure.DOUBLE_TAP_TO_WAKE, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.WAKE_GESTURE_ENABLED, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.LONG_PRESS_TIMEOUT, NON_NEGATIVE_INTEGER_VALIDATOR);
+ VALIDATORS.put(Secure.KEY_REPEAT_ENABLED, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.KEY_REPEAT_TIMEOUT_MS, NON_NEGATIVE_INTEGER_VALIDATOR);
VALIDATORS.put(Secure.KEY_REPEAT_DELAY_MS, NON_NEGATIVE_INTEGER_VALIDATOR);
VALIDATORS.put(Secure.CAMERA_GESTURE_DISABLED, BOOLEAN_VALIDATOR);
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 855ebe3..c49ffb4 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -1063,6 +1063,13 @@
}
flag {
+ name: "communal_widget_resizing"
+ namespace: "systemui"
+ description: "Allow resizing of widgets on glanceable hub"
+ bug: "368053818"
+}
+
+flag {
name: "app_clips_backlinks"
namespace: "systemui"
description: "Enables Backlinks improvement feature in App Clips"
@@ -1415,3 +1422,13 @@
description: "Allow non-touchscreen devices to bypass falsing"
bug: "319809270"
}
+
+flag {
+ name: "media_projection_dialog_behind_lockscreen"
+ namespace: "systemui"
+ description: "Ensure MediaProjection Dialog appears behind the lockscreen"
+ bug: "351409536"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt
index 70529cc..ee65fbd 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt
@@ -21,6 +21,8 @@
import android.provider.Settings
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.internal.logging.uiEventLogger
+import com.android.internal.logging.uiEventLoggerFake
import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
import com.android.systemui.Flags.FLAG_COMMUNAL_SCENE_KTF_REFACTOR
import com.android.systemui.SysuiTestCase
@@ -28,6 +30,7 @@
import com.android.systemui.communal.domain.interactor.communalSceneInteractor
import com.android.systemui.communal.domain.interactor.communalSettingsInteractor
import com.android.systemui.communal.domain.interactor.setCommunalAvailable
+import com.android.systemui.communal.shared.log.CommunalUiEvent
import com.android.systemui.communal.shared.model.CommunalScenes
import com.android.systemui.communal.shared.model.EditModeState
import com.android.systemui.coroutines.collectLastValue
@@ -35,7 +38,6 @@
import com.android.systemui.dock.fakeDockManager
import com.android.systemui.flags.Flags.COMMUNAL_SERVICE_ENABLED
import com.android.systemui.flags.fakeFeatureFlagsClassic
-import com.android.systemui.flags.featureFlagsClassic
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
@@ -93,6 +95,7 @@
bgScope = applicationCoroutineScope,
mainDispatcher = testDispatcher,
centralSurfacesOpt = centralSurfacesOptional,
+ uiEventLogger = uiEventLoggerFake,
)
.apply { start() }
@@ -119,7 +122,7 @@
fakeKeyguardTransitionRepository.sendTransitionSteps(
from = KeyguardState.PRIMARY_BOUNCER,
to = KeyguardState.GONE,
- testScope = this
+ testScope = this,
)
assertThat(scene).isEqualTo(CommunalScenes.Communal)
@@ -140,7 +143,7 @@
fakeKeyguardTransitionRepository.sendTransitionSteps(
from = KeyguardState.PRIMARY_BOUNCER,
to = KeyguardState.GONE,
- testScope = this
+ testScope = this,
)
assertThat(scene).isEqualTo(CommunalScenes.Communal)
@@ -161,7 +164,7 @@
fakeKeyguardTransitionRepository.sendTransitionSteps(
from = KeyguardState.PRIMARY_BOUNCER,
to = KeyguardState.GONE,
- testScope = this
+ testScope = this,
)
assertThat(scene).isEqualTo(CommunalScenes.Blank)
@@ -181,7 +184,7 @@
fakeKeyguardTransitionRepository.sendTransitionSteps(
from = KeyguardState.ALTERNATE_BOUNCER,
to = KeyguardState.GONE,
- testScope = this
+ testScope = this,
)
// Scene change will be handled in EditWidgetsActivity not here
assertThat(scene).isEqualTo(CommunalScenes.Communal)
@@ -200,7 +203,7 @@
fakeKeyguardTransitionRepository.sendTransitionSteps(
from = KeyguardState.GONE,
to = KeyguardState.LOCKSCREEN,
- testScope = this
+ testScope = this,
)
assertThat(scene).isEqualTo(CommunalScenes.Communal)
}
@@ -220,7 +223,7 @@
fakeKeyguardTransitionRepository.sendTransitionSteps(
from = KeyguardState.GLANCEABLE_HUB,
to = KeyguardState.OCCLUDED,
- testScope = this
+ testScope = this,
)
assertThat(scene).isEqualTo(CommunalScenes.Blank)
}
@@ -240,7 +243,7 @@
fakeKeyguardTransitionRepository.sendTransitionSteps(
from = KeyguardState.GLANCEABLE_HUB,
to = KeyguardState.OCCLUDED,
- testScope = this
+ testScope = this,
)
assertThat(scene).isEqualTo(CommunalScenes.Communal)
}
@@ -258,7 +261,7 @@
fakeKeyguardTransitionRepository.sendTransitionSteps(
from = KeyguardState.GLANCEABLE_HUB,
to = KeyguardState.LOCKSCREEN,
- testScope = this
+ testScope = this,
)
assertThat(scene).isEqualTo(CommunalScenes.Blank)
}
@@ -276,7 +279,7 @@
fakeKeyguardTransitionRepository.sendTransitionSteps(
from = KeyguardState.GLANCEABLE_HUB,
to = KeyguardState.OFF,
- testScope = this
+ testScope = this,
)
assertThat(scene).isEqualTo(CommunalScenes.Communal)
@@ -298,7 +301,7 @@
fakeKeyguardTransitionRepository.sendTransitionSteps(
from = KeyguardState.GLANCEABLE_HUB,
to = KeyguardState.OFF,
- testScope = this
+ testScope = this,
)
assertThat(scene).isEqualTo(CommunalScenes.Communal)
advanceTimeBy(CommunalSceneStartable.AWAKE_DEBOUNCE_DELAY / 2)
@@ -307,7 +310,7 @@
fakeKeyguardTransitionRepository.sendTransitionSteps(
from = KeyguardState.OFF,
to = KeyguardState.GLANCEABLE_HUB,
- testScope = this
+ testScope = this,
)
advanceTimeBy(CommunalSceneStartable.AWAKE_DEBOUNCE_DELAY)
@@ -327,7 +330,7 @@
fakeKeyguardTransitionRepository.sendTransitionSteps(
from = KeyguardState.GLANCEABLE_HUB,
to = KeyguardState.LOCKSCREEN,
- testScope = this
+ testScope = this,
)
updateDocked(true)
@@ -349,7 +352,7 @@
fakeKeyguardTransitionRepository.sendTransitionSteps(
from = KeyguardState.GLANCEABLE_HUB,
to = KeyguardState.LOCKSCREEN,
- testScope = this
+ testScope = this,
)
updateDocked(true)
@@ -361,7 +364,7 @@
fakeKeyguardTransitionRepository.sendTransitionSteps(
from = KeyguardState.LOCKSCREEN,
to = KeyguardState.DREAMING,
- testScope = this
+ testScope = this,
)
advanceTimeBy(CommunalSceneStartable.DOCK_DEBOUNCE_DELAY)
assertThat(scene).isEqualTo(CommunalScenes.Blank)
@@ -511,6 +514,9 @@
advanceTimeBy(SCREEN_TIMEOUT.milliseconds)
assertThat(scene).isEqualTo(CommunalScenes.Blank)
+ assertThat(uiEventLoggerFake.logs.first().eventId)
+ .isEqualTo(CommunalUiEvent.COMMUNAL_HUB_TIMEOUT.id)
+ assertThat(uiEventLoggerFake.numLogs()).isEqualTo(1)
}
}
@@ -526,7 +532,7 @@
fakeKeyguardTransitionRepository.sendTransitionSteps(
from = KeyguardState.DOZING,
to = KeyguardState.GLANCEABLE_HUB,
- testScope = this
+ testScope = this,
)
assertThat(scene).isEqualTo(CommunalScenes.Communal)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt
index 361e768..8f9e238 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt
@@ -17,11 +17,13 @@
package com.android.systemui.keyboard.data.repository
-import android.hardware.input.InputManager
+import android.hardware.input.FakeInputManager
+import android.hardware.input.InputManager.InputDeviceListener
import android.hardware.input.InputManager.KeyboardBacklightListener
import android.hardware.input.KeyboardBacklightState
+import android.hardware.input.fakeInputManager
import android.testing.TestableLooper
-import android.view.InputDevice
+import android.view.InputDevice.SOURCE_KEYBOARD
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
@@ -30,10 +32,7 @@
import com.android.systemui.coroutines.collectValues
import com.android.systemui.inputdevice.data.repository.InputDeviceRepository
import com.android.systemui.keyboard.data.model.Keyboard
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.nullable
-import com.android.systemui.util.mockito.whenever
+import com.android.systemui.testKosmos
import com.android.systemui.utils.os.FakeHandler
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineDispatcher
@@ -50,9 +49,10 @@
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.Captor
-import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.any
+import org.mockito.kotlin.anyOrNull
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@@ -60,10 +60,9 @@
@RunWith(AndroidJUnit4::class)
class KeyboardRepositoryTest : SysuiTestCase() {
- @Captor
- private lateinit var deviceListenerCaptor: ArgumentCaptor<InputManager.InputDeviceListener>
+ @Captor private lateinit var deviceListenerCaptor: ArgumentCaptor<InputDeviceListener>
@Captor private lateinit var backlightListenerCaptor: ArgumentCaptor<KeyboardBacklightListener>
- @Mock private lateinit var inputManager: InputManager
+ private lateinit var fakeInputManager: FakeInputManager
private lateinit var underTest: KeyboardRepository
private lateinit var dispatcher: CoroutineDispatcher
@@ -73,16 +72,14 @@
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
- whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf())
- whenever(inputManager.getInputDevice(any())).then { invocation ->
- val id = invocation.arguments.first()
- INPUT_DEVICES_MAP[id]
- }
+ fakeInputManager = testKosmos().fakeInputManager
dispatcher = StandardTestDispatcher()
testScope = TestScope(dispatcher)
val handler = FakeHandler(TestableLooper.get(this).looper)
- inputDeviceRepo = InputDeviceRepository(handler, testScope.backgroundScope, inputManager)
- underTest = KeyboardRepositoryImpl(dispatcher, inputManager, inputDeviceRepo)
+ inputDeviceRepo =
+ InputDeviceRepository(handler, testScope.backgroundScope, fakeInputManager.inputManager)
+ underTest =
+ KeyboardRepositoryImpl(dispatcher, fakeInputManager.inputManager, inputDeviceRepo)
}
@Test
@@ -95,7 +92,7 @@
@Test
fun emitsConnected_ifKeyboardAlreadyConnectedAtTheStart() =
testScope.runTest {
- whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf(PHYSICAL_FULL_KEYBOARD_ID))
+ fakeInputManager.addPhysicalKeyboard(PHYSICAL_FULL_KEYBOARD_ID)
val initialValue = underTest.isAnyKeyboardConnected.first()
assertThat(initialValue).isTrue()
}
@@ -103,74 +100,77 @@
@Test
fun emitsConnected_whenNewPhysicalKeyboardConnects() =
testScope.runTest {
- val deviceListener = captureDeviceListener()
+ captureDeviceListener()
val isKeyboardConnected by collectLastValue(underTest.isAnyKeyboardConnected)
- deviceListener.onInputDeviceAdded(PHYSICAL_FULL_KEYBOARD_ID)
+ fakeInputManager.addPhysicalKeyboard(PHYSICAL_FULL_KEYBOARD_ID)
assertThat(isKeyboardConnected).isTrue()
}
@Test
- fun emitsDisconnected_whenDeviceWithIdDoesNotExist() =
+ fun emitsDisconnected_whenDeviceNotFound() =
testScope.runTest {
- val deviceListener = captureDeviceListener()
+ captureDeviceListener()
val isKeyboardConnected by collectLastValue(underTest.isAnyKeyboardConnected)
-
- deviceListener.onInputDeviceAdded(NULL_DEVICE_ID)
+ fakeInputManager.addDevice(NULL_DEVICE_ID, SOURCE_KEYBOARD, isNotFound = true)
assertThat(isKeyboardConnected).isFalse()
}
@Test
fun emitsDisconnected_whenKeyboardDisconnects() =
testScope.runTest {
- val deviceListener = captureDeviceListener()
+ captureDeviceListener()
val isKeyboardConnected by collectLastValue(underTest.isAnyKeyboardConnected)
- deviceListener.onInputDeviceAdded(PHYSICAL_FULL_KEYBOARD_ID)
+ fakeInputManager.addPhysicalKeyboard(PHYSICAL_FULL_KEYBOARD_ID)
assertThat(isKeyboardConnected).isTrue()
- deviceListener.onInputDeviceRemoved(PHYSICAL_FULL_KEYBOARD_ID)
+ fakeInputManager.removeDevice(PHYSICAL_FULL_KEYBOARD_ID)
assertThat(isKeyboardConnected).isFalse()
}
- private suspend fun captureDeviceListener(): InputManager.InputDeviceListener {
+ private suspend fun captureDeviceListener() {
underTest.isAnyKeyboardConnected.first()
- verify(inputManager).registerInputDeviceListener(deviceListenerCaptor.capture(), nullable())
- return deviceListenerCaptor.value
+ verify(fakeInputManager.inputManager)
+ .registerInputDeviceListener(deviceListenerCaptor.capture(), anyOrNull())
+ fakeInputManager.registerInputDeviceListener(deviceListenerCaptor.value)
}
@Test
fun emitsDisconnected_whenVirtualOrNotFullKeyboardConnects() =
testScope.runTest {
- val deviceListener = captureDeviceListener()
+ captureDeviceListener()
val isKeyboardConnected by collectLastValue(underTest.isAnyKeyboardConnected)
- deviceListener.onInputDeviceAdded(PHYSICAL_NOT_FULL_KEYBOARD_ID)
+ fakeInputManager.addPhysicalKeyboard(
+ PHYSICAL_NOT_FULL_KEYBOARD_ID,
+ isFullKeyboard = false
+ )
assertThat(isKeyboardConnected).isFalse()
- deviceListener.onInputDeviceAdded(VIRTUAL_FULL_KEYBOARD_ID)
+ fakeInputManager.addDevice(VIRTUAL_FULL_KEYBOARD_ID, SOURCE_KEYBOARD)
assertThat(isKeyboardConnected).isFalse()
}
@Test
fun emitsDisconnected_whenKeyboardDisconnectsAndWasAlreadyConnectedAtTheStart() =
testScope.runTest {
- val deviceListener = captureDeviceListener()
+ captureDeviceListener()
val isKeyboardConnected by collectLastValue(underTest.isAnyKeyboardConnected)
- deviceListener.onInputDeviceRemoved(PHYSICAL_FULL_KEYBOARD_ID)
+ fakeInputManager.removeDevice(PHYSICAL_FULL_KEYBOARD_ID)
assertThat(isKeyboardConnected).isFalse()
}
@Test
fun emitsConnected_whenAnotherDeviceDisconnects() =
testScope.runTest {
- val deviceListener = captureDeviceListener()
+ captureDeviceListener()
val isKeyboardConnected by collectLastValue(underTest.isAnyKeyboardConnected)
- deviceListener.onInputDeviceAdded(PHYSICAL_FULL_KEYBOARD_ID)
- deviceListener.onInputDeviceRemoved(VIRTUAL_FULL_KEYBOARD_ID)
+ fakeInputManager.addPhysicalKeyboard(PHYSICAL_FULL_KEYBOARD_ID)
+ fakeInputManager.removeDevice(VIRTUAL_FULL_KEYBOARD_ID)
assertThat(isKeyboardConnected).isTrue()
}
@@ -178,12 +178,12 @@
@Test
fun emitsConnected_whenOnePhysicalKeyboardDisconnectsButAnotherRemainsConnected() =
testScope.runTest {
- val deviceListener = captureDeviceListener()
+ captureDeviceListener()
val isKeyboardConnected by collectLastValue(underTest.isAnyKeyboardConnected)
- deviceListener.onInputDeviceAdded(PHYSICAL_FULL_KEYBOARD_ID)
- deviceListener.onInputDeviceAdded(ANOTHER_PHYSICAL_FULL_KEYBOARD_ID)
- deviceListener.onInputDeviceRemoved(ANOTHER_PHYSICAL_FULL_KEYBOARD_ID)
+ fakeInputManager.addPhysicalKeyboard(PHYSICAL_FULL_KEYBOARD_ID)
+ fakeInputManager.addPhysicalKeyboard(ANOTHER_PHYSICAL_FULL_KEYBOARD_ID)
+ fakeInputManager.removeDevice(ANOTHER_PHYSICAL_FULL_KEYBOARD_ID)
assertThat(isKeyboardConnected).isTrue()
}
@@ -195,7 +195,7 @@
// subscribed to and listener is actually registered in inputManager
val backlight by collectLastValueImmediately(underTest.backlight)
- verify(inputManager)
+ verify(fakeInputManager.inputManager)
.registerKeyboardBacklightListener(any(), backlightListenerCaptor.capture())
backlightListenerCaptor.value.onBacklightChanged(current = 1, max = 5)
@@ -217,7 +217,7 @@
fun keyboardBacklightValuesNotPassed_fromBacklightListener_whenNotTriggeredByKeyPress() {
testScope.runTest {
val backlight by collectLastValueImmediately(underTest.backlight)
- verify(inputManager)
+ verify(fakeInputManager.inputManager)
.registerKeyboardBacklightListener(any(), backlightListenerCaptor.capture())
backlightListenerCaptor.value.onBacklightChanged(
@@ -233,7 +233,7 @@
fun passesKeyboardBacklightValues_fromBacklightListener_whenTriggeredByKeyPress() {
testScope.runTest {
val backlight by collectLastValueImmediately(underTest.backlight)
- verify(inputManager)
+ verify(fakeInputManager.inputManager)
.registerKeyboardBacklightListener(any(), backlightListenerCaptor.capture())
backlightListenerCaptor.value.onBacklightChanged(
@@ -248,14 +248,11 @@
@Test
fun passessAllKeyboards_thatWereAlreadyConnectedOnInitialization() {
testScope.runTest {
- whenever(inputManager.inputDeviceIds)
- .thenReturn(
- intArrayOf(
- PHYSICAL_FULL_KEYBOARD_ID,
- ANOTHER_PHYSICAL_FULL_KEYBOARD_ID,
- VIRTUAL_FULL_KEYBOARD_ID // not a physical keyboard - that's why result is 2
- )
- )
+ fakeInputManager.addPhysicalKeyboard(PHYSICAL_FULL_KEYBOARD_ID)
+ fakeInputManager.addPhysicalKeyboard(ANOTHER_PHYSICAL_FULL_KEYBOARD_ID)
+ // not a physical keyboard - that's why result is 2
+ fakeInputManager.addDevice(VIRTUAL_FULL_KEYBOARD_ID, SOURCE_KEYBOARD)
+
val keyboards by collectValues(underTest.newlyConnectedKeyboard)
assertThat(keyboards).hasSize(2)
@@ -265,9 +262,9 @@
@Test
fun passesNewlyConnectedKeyboard() {
testScope.runTest {
- val deviceListener = captureDeviceListener()
+ captureDeviceListener()
- deviceListener.onInputDeviceAdded(PHYSICAL_FULL_KEYBOARD_ID)
+ fakeInputManager.addPhysicalKeyboard(PHYSICAL_FULL_KEYBOARD_ID, VENDOR_ID, PRODUCT_ID)
assertThat(underTest.newlyConnectedKeyboard.first())
.isEqualTo(Keyboard(VENDOR_ID, PRODUCT_ID))
@@ -277,13 +274,12 @@
@Test
fun emitsOnlyNewlyConnectedKeyboards() {
testScope.runTest {
- whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf(PHYSICAL_FULL_KEYBOARD_ID))
+ fakeInputManager.addPhysicalKeyboard(PHYSICAL_FULL_KEYBOARD_ID)
underTest.newlyConnectedKeyboard.first()
- verify(inputManager)
- .registerInputDeviceListener(deviceListenerCaptor.capture(), nullable())
- val deviceListener = deviceListenerCaptor.value
- deviceListener.onInputDeviceAdded(ANOTHER_PHYSICAL_FULL_KEYBOARD_ID)
+ captureDeviceListener()
+
+ fakeInputManager.addPhysicalKeyboard(ANOTHER_PHYSICAL_FULL_KEYBOARD_ID)
val keyboards by collectValues(underTest.newlyConnectedKeyboard)
assertThat(keyboards).hasSize(1)
@@ -293,14 +289,11 @@
@Test
fun stillEmitsNewKeyboardEvenIfFlowWasSubscribedAfterOtherFlows() {
testScope.runTest {
- whenever(inputManager.inputDeviceIds)
- .thenReturn(
- intArrayOf(
- PHYSICAL_FULL_KEYBOARD_ID,
- ANOTHER_PHYSICAL_FULL_KEYBOARD_ID,
- VIRTUAL_FULL_KEYBOARD_ID // not a physical keyboard - that's why result is 2
- )
- )
+ fakeInputManager.addPhysicalKeyboard(PHYSICAL_FULL_KEYBOARD_ID)
+ fakeInputManager.addPhysicalKeyboard(ANOTHER_PHYSICAL_FULL_KEYBOARD_ID)
+ // not a physical keyboard - that's why result is 2
+ fakeInputManager.addDevice(VIRTUAL_FULL_KEYBOARD_ID, SOURCE_KEYBOARD)
+
collectLastValueImmediately(underTest.isAnyKeyboardConnected)
// let's pretend second flow is subscribed after some delay
@@ -314,12 +307,12 @@
@Test
fun emitsKeyboardWhenItWasReconnected() {
testScope.runTest {
- val deviceListener = captureDeviceListener()
+ captureDeviceListener()
val keyboards by collectValues(underTest.newlyConnectedKeyboard)
- deviceListener.onInputDeviceAdded(PHYSICAL_FULL_KEYBOARD_ID)
- deviceListener.onInputDeviceRemoved(PHYSICAL_FULL_KEYBOARD_ID)
- deviceListener.onInputDeviceAdded(PHYSICAL_FULL_KEYBOARD_ID)
+ fakeInputManager.addPhysicalKeyboard(PHYSICAL_FULL_KEYBOARD_ID)
+ fakeInputManager.removeDevice(PHYSICAL_FULL_KEYBOARD_ID)
+ fakeInputManager.addPhysicalKeyboard(PHYSICAL_FULL_KEYBOARD_ID)
assertThat(keyboards).hasSize(2)
}
@@ -339,30 +332,13 @@
private companion object {
private const val PHYSICAL_FULL_KEYBOARD_ID = 1
- private const val VIRTUAL_FULL_KEYBOARD_ID = 2
+ private const val VIRTUAL_FULL_KEYBOARD_ID = -2 // Virtual keyboards has id with minus value
private const val PHYSICAL_NOT_FULL_KEYBOARD_ID = 3
private const val ANOTHER_PHYSICAL_FULL_KEYBOARD_ID = 4
- private const val NULL_DEVICE_ID = 5
+ private const val NULL_DEVICE_ID = -5
private const val VENDOR_ID = 99
private const val PRODUCT_ID = 101
-
- private val INPUT_DEVICES_MAP: Map<Int, InputDevice> =
- mapOf(
- PHYSICAL_FULL_KEYBOARD_ID to inputDevice(virtual = false, fullKeyboard = true),
- VIRTUAL_FULL_KEYBOARD_ID to inputDevice(virtual = true, fullKeyboard = true),
- PHYSICAL_NOT_FULL_KEYBOARD_ID to inputDevice(virtual = false, fullKeyboard = false),
- ANOTHER_PHYSICAL_FULL_KEYBOARD_ID to
- inputDevice(virtual = false, fullKeyboard = true)
- )
-
- private fun inputDevice(virtual: Boolean, fullKeyboard: Boolean): InputDevice =
- mock<InputDevice>().also {
- whenever(it.isVirtual).thenReturn(virtual)
- whenever(it.isFullKeyboard).thenReturn(fullKeyboard)
- whenever(it.vendorId).thenReturn(VENDOR_ID)
- whenever(it.productId).thenReturn(PRODUCT_ID)
- }
}
private class TestBacklightState(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaControlInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaControlInteractorTest.kt
index d594f3a..62d0625 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaControlInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaControlInteractorTest.kt
@@ -121,7 +121,7 @@
MediaData(
userId = USER_ID,
instanceId = InstanceId.fakeInstanceId(2),
- artist = ARTIST
+ artist = ARTIST,
)
mediaDataFilter.onMediaDataLoaded(KEY, KEY, mediaData)
@@ -145,10 +145,17 @@
val clickIntent = mock<PendingIntent> { whenever(it.isActivity).thenReturn(true) }
val expandable = mock<Expandable>()
+ val activityController = mock<ActivityTransitionAnimator.Controller>()
+ whenever(expandable.activityTransitionController(any())).thenReturn(activityController)
underTest.startClickIntent(expandable, clickIntent, SMARTSPACE_CARD_CLICK_EVENT, 1)
- verify(clickIntent).send(any<Bundle>())
+ verify(activityStarter)
+ .startPendingIntentMaybeDismissingKeyguard(
+ eq(clickIntent),
+ eq(null),
+ eq(activityController),
+ )
}
@Test
@@ -174,7 +181,7 @@
mediaData.appUid,
surface = SURFACE,
cardinality = 2,
- rank = 1
+ rank = 1,
)
verify(activityStarter)
.postStartActivityDismissingKeyguard(eq(clickIntent), eq(activityController))
@@ -232,7 +239,7 @@
eq(true),
eq(dialogTransitionController),
eq(null),
- eq(null)
+ eq(null),
)
}
@@ -248,7 +255,7 @@
.createBroadcastDialogWithController(
eq(APP_NAME),
eq(PACKAGE_NAME),
- eq(dialogTransitionController)
+ eq(dialogTransitionController),
)
}
@@ -279,7 +286,7 @@
anyInt(),
anyInt(),
anyInt(),
- anyBoolean()
+ anyBoolean(),
)
verify(listener).onMediaDataRemoved(eq(KEY), eq(true))
}
@@ -307,7 +314,7 @@
mediaData.appUid,
surface = SURFACE,
cardinality = 2,
- rank = 1
+ rank = 1,
)
verify(listener).onMediaDataRemoved(eq(KEY), eq(true))
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModelTest.kt
index 9558e5d..0122028 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModelTest.kt
@@ -24,26 +24,25 @@
import android.media.session.PlaybackState
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import com.android.internal.logging.InstanceId
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
-import com.android.systemui.media.controls.domain.pipeline.MediaDataFilterImpl
import com.android.systemui.media.controls.domain.pipeline.mediaDataFilter
import com.android.systemui.media.controls.shared.model.MediaData
import com.android.systemui.media.controls.shared.model.MediaDeviceData
import com.android.systemui.media.controls.util.mediaInstanceId
import com.android.systemui.statusbar.notificationLockscreenUserManager
import com.android.systemui.testKosmos
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers
+import org.mockito.ArgumentMatchers.any
import org.mockito.Mockito
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.whenever
@SmallTest
@RunWith(AndroidJUnit4::class)
@@ -52,30 +51,31 @@
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
- private val mediaDataFilter: MediaDataFilterImpl = kosmos.mediaDataFilter
+ private val mediaDataFilter = kosmos.mediaDataFilter
private val notificationLockscreenUserManager = kosmos.notificationLockscreenUserManager
private val packageManager = kosmos.packageManager
private val drawable = context.getDrawable(R.drawable.ic_media_play)
- private val instanceId: InstanceId = kosmos.mediaInstanceId
-
+ private val instanceId = kosmos.mediaInstanceId
private val underTest: MediaControlViewModel = kosmos.mediaControlViewModel
+ @Before
+ fun setUp() {
+ whenever(packageManager.getApplicationIcon(Mockito.anyString())).thenReturn(drawable)
+ whenever(packageManager.getApplicationIcon(any(ApplicationInfo::class.java)))
+ .thenReturn(drawable)
+ whenever(packageManager.getApplicationInfo(eq(PACKAGE_NAME), ArgumentMatchers.anyInt()))
+ .thenReturn(ApplicationInfo())
+ whenever(packageManager.getApplicationLabel(any())).thenReturn(PACKAGE_NAME)
+ whenever(notificationLockscreenUserManager.isCurrentProfile(USER_ID)).thenReturn(true)
+ whenever(notificationLockscreenUserManager.isProfileAvailable(USER_ID)).thenReturn(true)
+ context.setMockPackageManager(packageManager)
+ }
+
@Test
fun addMediaControl_mediaControlViewModelIsLoaded() =
testScope.runTest {
- whenever(packageManager.getApplicationIcon(Mockito.anyString())).thenReturn(drawable)
- whenever(packageManager.getApplicationIcon(any(ApplicationInfo::class.java)))
- .thenReturn(drawable)
- whenever(packageManager.getApplicationInfo(eq(PACKAGE_NAME), ArgumentMatchers.anyInt()))
- .thenReturn(ApplicationInfo())
- whenever(packageManager.getApplicationLabel(any())).thenReturn(PACKAGE_NAME)
- whenever(notificationLockscreenUserManager.isCurrentProfile(USER_ID)).thenReturn(true)
- whenever(notificationLockscreenUserManager.isProfileAvailable(USER_ID)).thenReturn(true)
val playerModel by collectLastValue(underTest.player)
-
- context.setMockPackageManager(packageManager)
-
- val mediaData = initMediaData()
+ val mediaData = initMediaData(ARTIST, TITLE)
mediaDataFilter.onMediaDataLoaded(KEY, KEY, mediaData)
@@ -88,7 +88,51 @@
assertThat(playerModel?.playTurbulenceNoise).isFalse()
}
- private fun initMediaData(): MediaData {
+ @Test
+ fun emitDuplicateMediaControls_mediaControlIsNotBound() =
+ testScope.runTest {
+ val playerModel by collectLastValue(underTest.player)
+ val mediaData = initMediaData(ARTIST, TITLE)
+
+ mediaDataFilter.onMediaDataLoaded(KEY, KEY, mediaData)
+
+ assertThat(playerModel).isNotNull()
+ assertThat(playerModel?.titleName).isEqualTo(TITLE)
+ assertThat(playerModel?.artistName).isEqualTo(ARTIST)
+ assertThat(underTest.isNewPlayer(playerModel!!)).isTrue()
+
+ mediaDataFilter.onMediaDataLoaded(KEY, KEY, mediaData)
+
+ assertThat(playerModel).isNotNull()
+ assertThat(playerModel?.titleName).isEqualTo(TITLE)
+ assertThat(playerModel?.artistName).isEqualTo(ARTIST)
+ assertThat(underTest.isNewPlayer(playerModel!!)).isFalse()
+ }
+
+ @Test
+ fun emitDifferentMediaControls_mediaControlIsBound() =
+ testScope.runTest {
+ val playerModel by collectLastValue(underTest.player)
+ var mediaData = initMediaData(ARTIST, TITLE)
+
+ mediaDataFilter.onMediaDataLoaded(KEY, KEY, mediaData)
+
+ assertThat(playerModel).isNotNull()
+ assertThat(playerModel?.titleName).isEqualTo(TITLE)
+ assertThat(playerModel?.artistName).isEqualTo(ARTIST)
+ assertThat(underTest.isNewPlayer(playerModel!!)).isTrue()
+
+ mediaData = initMediaData(ARTIST_2, TITLE_2)
+
+ mediaDataFilter.onMediaDataLoaded(KEY, KEY, mediaData)
+
+ assertThat(playerModel).isNotNull()
+ assertThat(playerModel?.titleName).isEqualTo(TITLE_2)
+ assertThat(playerModel?.artistName).isEqualTo(ARTIST_2)
+ assertThat(underTest.isNewPlayer(playerModel!!)).isTrue()
+ }
+
+ private fun initMediaData(artist: String, title: String): MediaData {
val device = MediaDeviceData(true, null, DEVICE_NAME, null, showBroadcastButton = true)
// Create media session
@@ -111,12 +155,12 @@
return MediaData(
userId = USER_ID,
- artist = ARTIST,
- song = TITLE,
+ artist = artist,
+ song = title,
packageName = PACKAGE,
token = session.sessionToken,
device = device,
- instanceId = instanceId
+ instanceId = instanceId,
)
}
@@ -127,6 +171,8 @@
private const val PACKAGE = "PKG"
private const val ARTIST = "ARTIST"
private const val TITLE = "TITLE"
+ private const val ARTIST_2 = "ARTIST_2"
+ private const val TITLE_2 = "TITLE_2"
private const val DEVICE_NAME = "DEVICE_NAME"
private const val SESSION_KEY = "SESSION_KEY"
private const val SESSION_ARTIST = "SESSION_ARTIST"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavBarHelperTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/NavBarHelperTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavBarHelperTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/NavBarHelperTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarRotationContextTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/NavigationBarRotationContextTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarRotationContextTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/NavigationBarRotationContextTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/TaskbarDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/TaskbarDelegateTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/navigationbar/TaskbarDelegateTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/TaskbarDelegateTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/FloatingRotationButtonPositionCalculatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/gestural/FloatingRotationButtonPositionCalculatorTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/FloatingRotationButtonPositionCalculatorTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/gestural/FloatingRotationButtonPositionCalculatorTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarInflaterViewTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/NavigationBarInflaterViewTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarInflaterViewTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/NavigationBarInflaterViewTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarTransitionsTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/NavigationBarTransitionsTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarTransitionsTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/NavigationBarTransitionsTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/buttons/KeyButtonViewTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/buttons/KeyButtonViewTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/buttons/KeyButtonViewTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/buttons/KeyButtonViewTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/buttons/NavigationBarContextTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/buttons/NavigationBarContextTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/buttons/NavigationBarContextTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/buttons/NavigationBarContextTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/buttons/NearestTouchFrameTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/buttons/NearestTouchFrameTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/buttons/NearestTouchFrameTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/buttons/NearestTouchFrameTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/FakeNoteTaskBubbleController.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/FakeNoteTaskBubbleController.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/notetask/FakeNoteTaskBubbleController.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/FakeNoteTaskBubbleController.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskBubblesServiceTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/NoteTaskBubblesServiceTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskBubblesServiceTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/NoteTaskBubblesServiceTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskEventLoggerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/NoteTaskEventLoggerTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskEventLoggerTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/NoteTaskEventLoggerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInfoResolverTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/NoteTaskInfoResolverTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInfoResolverTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/NoteTaskInfoResolverTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInfoTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/NoteTaskInfoTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInfoTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/NoteTaskInfoTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/people/NotificationHelperTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/people/NotificationHelperTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/people/NotificationHelperTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/people/NotificationHelperTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/people/PeopleBackupFollowUpJobTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/people/PeopleBackupFollowUpJobTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/people/PeopleBackupFollowUpJobTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/people/PeopleBackupFollowUpJobTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/people/PeopleProviderTestable.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/people/PeopleProviderTestable.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/people/PeopleProviderTestable.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/people/PeopleProviderTestable.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/people/SharedPreferencesHelperTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/people/SharedPreferencesHelperTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/people/SharedPreferencesHelperTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/people/SharedPreferencesHelperTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/people/widget/LaunchConversationActivityTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/people/widget/LaunchConversationActivityTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/people/widget/LaunchConversationActivityTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/people/widget/LaunchConversationActivityTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/people/widget/PeopleBackupHelperTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/people/widget/PeopleBackupHelperTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/people/widget/PeopleBackupHelperTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/people/widget/PeopleBackupHelperTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyChipBuilderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/privacy/PrivacyChipBuilderTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyChipBuilderTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/privacy/PrivacyChipBuilderTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/privacy/PrivacyDialogTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/privacy/PrivacyDialogTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/process/condition/SystemProcessConditionTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/process/condition/SystemProcessConditionTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/process/condition/SystemProcessConditionTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/process/condition/SystemProcessConditionTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/HeaderPrivacyIconsControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/HeaderPrivacyIconsControllerTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/HeaderPrivacyIconsControllerTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/HeaderPrivacyIconsControllerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/LeftRightArrowPressedListenerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/LeftRightArrowPressedListenerTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/LeftRightArrowPressedListenerTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/LeftRightArrowPressedListenerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/PagedTileLayoutTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/PagedTileLayoutTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/PagedTileLayoutTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/PagedTileLayoutTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSContainerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSContainerImplTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/QSContainerImplTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSContainerImplTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSDisableFlagsLoggerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSDisableFlagsLoggerTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/QSDisableFlagsLoggerTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSDisableFlagsLoggerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseSceneContainerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSPanelControllerBaseSceneContainerTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseSceneContainerTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSPanelControllerBaseSceneContainerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSPanelControllerTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSPanelControllerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelSwitchToParentTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSPanelSwitchToParentTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelSwitchToParentTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSPanelSwitchToParentTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSSquishinessControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSSquishinessControllerTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/QSSquishinessControllerTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSSquishinessControllerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QuickQSPanelTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QuickQSPanelTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickStatusBarHeaderControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QuickStatusBarHeaderControllerTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/QuickStatusBarHeaderControllerTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QuickStatusBarHeaderControllerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/TileStateToProtoTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/TileStateToProtoTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/TileStateToProtoTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/TileStateToProtoTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/TouchAnimatorTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/TouchAnimatorTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/TouchAnimatorTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/TouchAnimatorTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/UserSettingObserverTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/UserSettingObserverTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/UserSettingObserverTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/UserSettingObserverTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileAdapterDelegateTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/customize/TileAdapterDelegateTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileAdapterDelegateTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/customize/TileAdapterDelegateTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileAdapterTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/customize/TileAdapterTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileAdapterTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/customize/TileAdapterTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/external/CustomTileTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/external/CustomTileTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileColorPickerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/external/TileColorPickerTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/external/TileColorPickerTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/external/TileColorPickerTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/external/TileServiceManagerTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceManagerTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/external/TileServiceManagerTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/shared/QSPipelineFlagsRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/shared/QSPipelineFlagsRepositoryTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/shared/QSPipelineFlagsRepositoryTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/shared/QSPipelineFlagsRepositoryTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSFactoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tileimpl/QSFactoryImplTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSFactoryImplTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tileimpl/QSFactoryImplTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/ResourceIconTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tileimpl/ResourceIconTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/ResourceIconTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tileimpl/ResourceIconTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/AirplaneModeTileTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/AirplaneModeTileTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/AirplaneModeTileTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/AirplaneModeTileTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BatterySaverTileTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/BatterySaverTileTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BatterySaverTileTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/BatterySaverTileTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/CameraToggleTileTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/CameraToggleTileTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/CameraToggleTileTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/CameraToggleTileTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/CastTileTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/CastTileTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/CastTileTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/CastTileTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ColorCorrectionTileTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ColorCorrectionTileTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ColorCorrectionTileTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ColorCorrectionTileTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ColorInversionTileTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ColorInversionTileTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ColorInversionTileTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ColorInversionTileTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DataSaverTileTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/DataSaverTileTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DataSaverTileTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/DataSaverTileTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/FlashlightTileTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/FlashlightTileTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/FlashlightTileTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/FlashlightTileTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/FontScalingTileTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/FontScalingTileTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/FontScalingTileTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/FontScalingTileTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/InternetTileTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/InternetTileTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/InternetTileTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/InternetTileTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/LocationTileTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/LocationTileTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/LocationTileTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/LocationTileTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/MicrophoneToggleTileTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/MicrophoneToggleTileTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/MicrophoneToggleTileTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/MicrophoneToggleTileTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/NfcTileTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/NfcTileTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/NfcTileTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/NfcTileTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/NightDisplayTileTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/NightDisplayTileTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/NightDisplayTileTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/NightDisplayTileTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/OneHandedModeTileTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/OneHandedModeTileTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/OneHandedModeTileTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/OneHandedModeTileTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/QRCodeScannerTileTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/QRCodeScannerTileTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/QRCodeScannerTileTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/QRCodeScannerTileTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ReduceBrightColorsTileTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ReduceBrightColorsTileTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ReduceBrightColorsTileTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ReduceBrightColorsTileTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RotationLockTileTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/RotationLockTileTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RotationLockTileTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/RotationLockTileTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UiModeNightTileTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/UiModeNightTileTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UiModeNightTileTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/UiModeNightTileTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImplTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImplTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImplTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetAdapterTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/dialog/InternetAdapterTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetAdapterTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/dialog/InternetAdapterTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/WifiStateWorkerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/dialog/WifiStateWorkerTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/WifiStateWorkerTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/dialog/WifiStateWorkerTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapperTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapperTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapperTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/recordissue/CustomTraceStateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/CustomTraceStateTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/recordissue/CustomTraceStateTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/CustomTraceStateTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/recordissue/IssueRecordingServiceCommandHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/IssueRecordingServiceCommandHandlerTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/recordissue/IssueRecordingServiceCommandHandlerTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/IssueRecordingServiceCommandHandlerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/recordissue/IssueRecordingStateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/IssueRecordingStateTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/recordissue/IssueRecordingStateTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/IssueRecordingStateTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/retail/data/repository/RetailModeSettingsRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/retail/data/repository/RetailModeSettingsRepositoryTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/retail/data/repository/RetailModeSettingsRepositoryTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/retail/data/repository/RetailModeSettingsRepositoryTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/retail/domain/interactor/RetailModeInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/retail/domain/interactor/RetailModeInteractorImplTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/retail/domain/interactor/RetailModeInteractorImplTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/retail/domain/interactor/RetailModeInteractorImplTest.kt
diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt
index ee6223f..08a7c39 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt
@@ -19,11 +19,13 @@
import android.provider.Settings
import com.android.compose.animation.scene.SceneKey
import com.android.compose.animation.scene.TransitionKey
+import com.android.internal.logging.UiEventLogger
import com.android.systemui.CoreStartable
import com.android.systemui.Flags.communalSceneKtfRefactor
import com.android.systemui.communal.domain.interactor.CommunalInteractor
import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor
+import com.android.systemui.communal.shared.log.CommunalUiEvent
import com.android.systemui.communal.shared.model.CommunalScenes
import com.android.systemui.communal.shared.model.CommunalTransitionKeys
import com.android.systemui.communal.shared.model.EditModeState
@@ -84,6 +86,7 @@
@Application private val applicationScope: CoroutineScope,
@Background private val bgScope: CoroutineScope,
@Main private val mainDispatcher: CoroutineDispatcher,
+ private val uiEventLogger: UiEventLogger,
) : CoreStartable {
private var screenTimeout: Int = DEFAULT_SCREEN_TIMEOUT
@@ -184,6 +187,7 @@
CommunalScenes.Blank,
"dream started after timeout",
)
+ uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_TIMEOUT)
}
}
}
@@ -212,6 +216,7 @@
newScene = CommunalScenes.Blank,
loggingReason = "hub timeout",
)
+ uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_TIMEOUT)
}
timeoutJob = null
}
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
index 8f913ff..78a8a42 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
@@ -113,6 +113,7 @@
import android.view.inputmethod.InputMethodManager;
import android.view.textclassifier.TextClassificationManager;
+import androidx.annotation.NonNull;
import androidx.asynclayoutinflater.view.AsyncLayoutInflater;
import androidx.core.app.NotificationManagerCompat;
@@ -718,6 +719,19 @@
@Provides
@Singleton
+ static ViewCaptureAwareWindowManager.Factory viewCaptureAwareWindowManagerFactory(
+ Lazy<ViewCapture> daggerLazyViewCapture) {
+ return new ViewCaptureAwareWindowManager.Factory() {
+ @NonNull
+ @Override
+ public ViewCaptureAwareWindowManager create(@NonNull WindowManager windowManager) {
+ return provideViewCaptureAwareWindowManager(windowManager, daggerLazyViewCapture);
+ }
+ };
+ }
+
+ @Provides
+ @Singleton
static PermissionManager providePermissionManager(Context context) {
PermissionManager pm = context.getSystemService(PermissionManager.class);
if (pm != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 1a0525d..ba23eb3 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -346,6 +346,7 @@
private boolean mShuttingDown;
private boolean mDozing;
private boolean mAnimatingScreenOff;
+ private boolean mIgnoreDismiss;
private final Context mContext;
private final FalsingCollector mFalsingCollector;
@@ -627,18 +628,20 @@
public void onUserSwitching(int userId) {
Log.d(TAG, String.format("onUserSwitching %d", userId));
synchronized (KeyguardViewMediator.this) {
+ mIgnoreDismiss = true;
notifyTrustedChangedLocked(mUpdateMonitor.getUserHasTrust(userId));
resetKeyguardDonePendingLocked();
- dismiss(null /* callback */, null /* message */);
+ resetStateLocked();
adjustStatusBarLocked();
}
}
@Override
public void onUserSwitchComplete(int userId) {
+ mIgnoreDismiss = false;
Log.d(TAG, String.format("onUserSwitchComplete %d", userId));
- // We are calling dismiss again and with a delay as there are race conditions
- // in some scenarios caused by async layout listeners
+ // We are calling dismiss with a delay as there are race conditions in some scenarios
+ // caused by async layout listeners
mHandler.postDelayed(() -> dismiss(null /* callback */, null /* message */), 500);
}
@@ -2442,6 +2445,10 @@
}
public void dismiss(IKeyguardDismissCallback callback, CharSequence message) {
+ if (mIgnoreDismiss) {
+ android.util.Log.i(TAG, "Ignoring request to dismiss (user switch in progress?)");
+ return;
+ }
mHandler.obtainMessage(DISMISS, new DismissMessage(callback, message)).sendToTarget();
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
index deb0b2d..b5f6b41 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
@@ -261,7 +261,10 @@
->
if (biometricMessage?.message != null) {
chipbarCoordinator!!.displayView(
- createChipbarInfo(biometricMessage.message, R.drawable.ic_lock)
+ createChipbarInfo(
+ biometricMessage.message,
+ R.drawable.ic_lock,
+ )
)
} else {
chipbarCoordinator!!.removeView(ID, "occludingAppMsgNull")
@@ -324,16 +327,12 @@
.getDimensionPixelSize(R.dimen.shelf_appear_translation)
.stateIn(this)
viewModel.isNotifIconContainerVisible.collect { isVisible ->
- if (isVisible.value) {
- blueprintViewModel.refreshBlueprint()
- } else {
- childViews[aodNotificationIconContainerId]
- ?.setAodNotifIconContainerIsVisible(
- isVisible,
- iconsAppearTranslationPx.value,
- screenOffAnimationController,
- )
- }
+ childViews[aodNotificationIconContainerId]
+ ?.setAodNotifIconContainerIsVisible(
+ isVisible,
+ iconsAppearTranslationPx.value,
+ screenOffAnimationController,
+ )
}
}
@@ -383,7 +382,7 @@
if (msdlFeedback()) {
msdlPlayer?.playToken(
MSDLToken.UNLOCK,
- authInteractionProperties,
+ authInteractionProperties
)
} else {
vibratorHelper.performHapticFeedback(
@@ -399,7 +398,7 @@
if (msdlFeedback()) {
msdlPlayer?.playToken(
MSDLToken.FAILURE,
- authInteractionProperties,
+ authInteractionProperties
)
} else {
vibratorHelper.performHapticFeedback(
@@ -426,7 +425,7 @@
blueprintViewModel,
clockViewModel,
childViews,
- burnInParams,
+ burnInParams
)
)
@@ -465,7 +464,11 @@
*/
private fun createChipbarInfo(message: String, @DrawableRes icon: Int): ChipbarInfo {
return ChipbarInfo(
- startIcon = TintedIcon(Icon.Resource(icon, null), ChipbarInfo.DEFAULT_ICON_TINT),
+ startIcon =
+ TintedIcon(
+ Icon.Resource(icon, null),
+ ChipbarInfo.DEFAULT_ICON_TINT,
+ ),
text = Text.Loaded(message),
endItem = null,
vibrationEffect = null,
@@ -496,7 +499,7 @@
oldLeft: Int,
oldTop: Int,
oldRight: Int,
- oldBottom: Int,
+ oldBottom: Int
) {
// After layout, ensure the notifications are positioned correctly
childViews[nsslPlaceholderId]?.let { notificationListPlaceholder ->
@@ -512,7 +515,7 @@
viewModel.onNotificationContainerBoundsChanged(
notificationListPlaceholder.top.toFloat(),
notificationListPlaceholder.bottom.toFloat(),
- animate = shouldAnimate,
+ animate = shouldAnimate
)
}
@@ -528,7 +531,7 @@
Int.MAX_VALUE
} else {
view.getTop()
- },
+ }
)
}
} else {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt
index 4cf3c4e..c6efcfa 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt
@@ -25,18 +25,20 @@
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Config
-import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Type
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
-data class TransitionData(val config: Config, val start: Long = System.currentTimeMillis())
+data class TransitionData(
+ val config: Config,
+ val start: Long = System.currentTimeMillis(),
+)
class KeyguardBlueprintViewModel
@Inject
constructor(
@Main private val handler: Handler,
- private val keyguardBlueprintInteractor: KeyguardBlueprintInteractor,
+ keyguardBlueprintInteractor: KeyguardBlueprintInteractor,
) {
val blueprint = keyguardBlueprintInteractor.blueprint
val blueprintId = keyguardBlueprintInteractor.blueprintId
@@ -74,9 +76,6 @@
}
}
- fun refreshBlueprint(type: Type = Type.NoTransition) =
- keyguardBlueprintInteractor.refreshBlueprint(type)
-
fun updateTransitions(data: TransitionData?, mutate: MutableSet<Transition>.() -> Unit) {
runningTransitions.mutate()
@@ -96,7 +95,7 @@
Log.w(
TAG,
"runTransition: skipping ${transition::class.simpleName}: " +
- "currentPriority=$currentPriority; config=$config",
+ "currentPriority=$currentPriority; config=$config"
)
}
apply()
@@ -107,7 +106,7 @@
Log.i(
TAG,
"runTransition: running ${transition::class.simpleName}: " +
- "currentPriority=$currentPriority; config=$config",
+ "currentPriority=$currentPriority; config=$config"
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractor.kt
index 130868d..1f339dd 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractor.kt
@@ -33,6 +33,7 @@
import com.android.systemui.media.controls.data.repository.MediaFilterRepository
import com.android.systemui.media.controls.domain.pipeline.MediaDataProcessor
import com.android.systemui.media.controls.domain.pipeline.getNotificationActions
+import com.android.systemui.media.controls.shared.MediaLogger
import com.android.systemui.media.controls.shared.model.MediaControlModel
import com.android.systemui.media.controls.shared.model.MediaData
import com.android.systemui.media.controls.util.MediaSmartspaceLogger
@@ -59,6 +60,7 @@
private val lockscreenUserManager: NotificationLockscreenUserManager,
private val mediaOutputDialogManager: MediaOutputDialogManager,
private val broadcastDialogController: BroadcastDialogController,
+ private val mediaLogger: MediaLogger,
) {
val mediaControl: Flow<MediaControlModel?> =
@@ -73,7 +75,7 @@
instanceId: InstanceId,
delayMs: Long,
eventId: Int,
- location: Int
+ location: Int,
): Boolean {
logSmartspaceUserEvent(eventId, location)
val dismissed =
@@ -81,7 +83,7 @@
if (!dismissed) {
Log.w(
TAG,
- "Manager failed to dismiss media of instanceId=$instanceId, Token uid=${token?.uid}"
+ "Manager failed to dismiss media of instanceId=$instanceId, Token uid=${token?.uid}",
)
}
return dismissed
@@ -120,20 +122,20 @@
expandable: Expandable,
clickIntent: PendingIntent,
eventId: Int,
- location: Int
+ location: Int,
) {
logSmartspaceUserEvent(eventId, location)
- if (!launchOverLockscreen(clickIntent)) {
+ if (!launchOverLockscreen(expandable, clickIntent)) {
activityStarter.postStartActivityDismissingKeyguard(
clickIntent,
- expandable.activityTransitionController(Cuj.CUJ_SHADE_APP_LAUNCH_FROM_MEDIA_PLAYER)
+ expandable.activityTransitionController(Cuj.CUJ_SHADE_APP_LAUNCH_FROM_MEDIA_PLAYER),
)
}
}
fun startDeviceIntent(deviceIntent: PendingIntent) {
if (deviceIntent.isActivity) {
- if (!launchOverLockscreen(deviceIntent)) {
+ if (!launchOverLockscreen(expandable = null, deviceIntent)) {
activityStarter.postStartActivityDismissingKeyguard(deviceIntent)
}
} else {
@@ -141,20 +143,33 @@
}
}
- private fun launchOverLockscreen(pendingIntent: PendingIntent): Boolean {
+ private fun launchOverLockscreen(
+ expandable: Expandable?,
+ pendingIntent: PendingIntent,
+ ): Boolean {
val showOverLockscreen =
keyguardStateController.isShowing &&
activityIntentHelper.wouldPendingShowOverLockscreen(
pendingIntent,
- lockscreenUserManager.currentUserId
+ lockscreenUserManager.currentUserId,
)
if (showOverLockscreen) {
try {
- val options = BroadcastOptions.makeBasic()
- options.isInteractive = true
- options.pendingIntentBackgroundActivityStartMode =
- ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
- pendingIntent.send(options.toBundle())
+ if (expandable != null) {
+ activityStarter.startPendingIntentMaybeDismissingKeyguard(
+ pendingIntent,
+ /* intentSentUiThreadCallback = */ null,
+ expandable.activityTransitionController(
+ Cuj.CUJ_SHADE_APP_LAUNCH_FROM_MEDIA_PLAYER
+ ),
+ )
+ } else {
+ val options = BroadcastOptions.makeBasic()
+ options.isInteractive = true
+ options.pendingIntentBackgroundActivityStartMode =
+ ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
+ pendingIntent.send(options.toBundle())
+ }
} catch (e: PendingIntent.CanceledException) {
Log.e(TAG, "pending intent of $instanceId was canceled")
}
@@ -166,7 +181,7 @@
fun startMediaOutputDialog(
expandable: Expandable,
packageName: String,
- token: MediaSession.Token? = null
+ token: MediaSession.Token? = null,
) {
mediaOutputDialogManager.createAndShowWithController(
packageName,
@@ -180,7 +195,7 @@
broadcastDialogController.createBroadcastDialogWithController(
broadcastApp,
packageName,
- expandable.dialogTransitionController()
+ expandable.dialogTransitionController(),
)
}
@@ -188,10 +203,14 @@
repository.logSmartspaceCardUserEvent(
eventId,
MediaSmartspaceLogger.getSurface(location),
- instanceId = instanceId
+ instanceId = instanceId,
)
}
+ fun logMediaControlIsBound(artistName: CharSequence, songName: CharSequence) {
+ mediaLogger.logMediaControlIsBound(instanceId, artistName, songName)
+ }
+
private fun Expandable.dialogController(): DialogTransitionAnimator.Controller? {
return dialogTransitionController(
cuj =
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/shared/MediaLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/shared/MediaLogger.kt
index 7d20e17..88c47ba 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/shared/MediaLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/shared/MediaLogger.kt
@@ -36,7 +36,7 @@
bool1 = active
str2 = reason
},
- { "add media $str1, active: $bool1, reason: $str2" }
+ { "add media $str1, active: $bool1, reason: $str2" },
)
}
@@ -48,7 +48,7 @@
str1 = instanceId.toString()
str2 = reason
},
- { "removing media $str1, reason: $str2" }
+ { "removing media $str1, reason: $str2" },
)
}
@@ -61,7 +61,7 @@
bool1 = isActive
str2 = reason
},
- { "add recommendation $str1, active $bool1, reason: $str2" }
+ { "add recommendation $str1, active $bool1, reason: $str2" },
)
}
@@ -74,7 +74,7 @@
bool1 = immediately
str2 = reason
},
- { "removing recommendation $str1, immediate=$bool1, reason: $str2" }
+ { "removing recommendation $str1, immediate=$bool1, reason: $str2" },
)
}
@@ -83,7 +83,7 @@
TAG,
LogLevel.DEBUG,
{ str1 = instanceId.toString() },
- { "adding media card $str1 to carousel" }
+ { "adding media card $str1 to carousel" },
)
}
@@ -92,7 +92,7 @@
TAG,
LogLevel.DEBUG,
{ str1 = instanceId.toString() },
- { "removing media card $str1 from carousel" }
+ { "removing media card $str1 from carousel" },
)
}
@@ -101,7 +101,7 @@
TAG,
LogLevel.DEBUG,
{ str1 = key },
- { "adding recommendation card $str1 to carousel" }
+ { "adding recommendation card $str1 to carousel" },
)
}
@@ -110,7 +110,7 @@
TAG,
LogLevel.DEBUG,
{ str1 = key },
- { "removing recommendation card $str1 from carousel" }
+ { "removing recommendation card $str1 from carousel" },
)
}
@@ -119,7 +119,24 @@
TAG,
LogLevel.DEBUG,
{ str1 = key },
- { "duplicate media notification $str1 posted" }
+ { "duplicate media notification $str1 posted" },
+ )
+ }
+
+ fun logMediaControlIsBound(
+ instanceId: InstanceId,
+ artistName: CharSequence,
+ title: CharSequence,
+ ) {
+ buffer.log(
+ TAG,
+ LogLevel.DEBUG,
+ {
+ str1 = instanceId.toString()
+ str2 = artistName.toString()
+ str3 = title.toString()
+ },
+ { "binding media control, instance id= $str1, artist= $str2, title= $str3" },
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/MediaControlViewBinder.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/MediaControlViewBinder.kt
index 6373fed..4055818 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/MediaControlViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/MediaControlViewBinder.kt
@@ -80,16 +80,19 @@
mediaCard.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
- viewModel.player.collectLatest { playerViewModel ->
- playerViewModel?.let {
- bindMediaCard(
- viewHolder,
- viewController,
- it,
- falsingManager,
- backgroundDispatcher,
- mainDispatcher,
- )
+ viewModel.player.collectLatest { player ->
+ player?.let {
+ if (viewModel.isNewPlayer(it)) {
+ bindMediaCard(
+ viewHolder,
+ viewController,
+ it,
+ falsingManager,
+ backgroundDispatcher,
+ mainDispatcher,
+ )
+ viewModel.onMediaControlIsBound(it.artistName, it.titleName)
+ }
}
}
}
@@ -143,7 +146,7 @@
viewHolder,
viewModel.outputSwitcher,
viewController,
- falsingManager
+ falsingManager,
)
bindGutsViewModel(viewHolder, viewModel, viewController, falsingManager)
bindActionButtons(viewHolder, viewModel, viewController, falsingManager)
@@ -157,7 +160,7 @@
viewController,
backgroundDispatcher,
mainDispatcher,
- isSongUpdated
+ isSongUpdated,
)
if (viewModel.playTurbulenceNoise) {
@@ -259,12 +262,12 @@
if (buttonView.id == R.id.actionPrev) {
viewController.setUpPrevButtonInfo(
buttonModel.isEnabled,
- buttonModel.notVisibleValue
+ buttonModel.notVisibleValue,
)
} else if (buttonView.id == R.id.actionNext) {
viewController.setUpNextButtonInfo(
buttonModel.isEnabled,
- buttonModel.notVisibleValue
+ buttonModel.notVisibleValue,
)
}
val animHandler = (buttonView.tag ?: AnimationBindHandler()) as AnimationBindHandler
@@ -295,7 +298,7 @@
viewController.collapsedLayout,
visible,
buttonModel.notVisibleValue,
- buttonModel.showInCollapsed
+ buttonModel.showInCollapsed,
)
}
}
@@ -350,7 +353,7 @@
createTouchRippleAnimation(
button,
viewController.colorSchemeTransition,
- multiRippleView
+ multiRippleView,
)
)
@@ -382,12 +385,12 @@
setVisibleAndAlpha(
expandedSet,
R.id.media_explicit_indicator,
- viewModel.isExplicitVisible
+ viewModel.isExplicitVisible,
)
setVisibleAndAlpha(
collapsedSet,
R.id.media_explicit_indicator,
- viewModel.isExplicitVisible
+ viewModel.isExplicitVisible,
)
// refreshState is required here to resize the text views (and prevent ellipsis)
@@ -398,7 +401,7 @@
// something is incorrectly bound, but needs to be run if other elements were
// updated while the enter animation was running
viewController.refreshState()
- }
+ },
)
}
@@ -427,7 +430,7 @@
viewModel.backgroundCover!!,
viewModel.colorScheme,
width,
- height
+ height,
)
} else {
ColorDrawable(Color.TRANSPARENT)
@@ -493,7 +496,7 @@
transitionDrawable: TransitionDrawable,
layer: Int,
targetWidth: Int,
- targetHeight: Int
+ targetHeight: Int,
) {
val drawable = transitionDrawable.getDrawable(layer) ?: return
val width = drawable.intrinsicWidth
@@ -509,7 +512,7 @@
artworkIcon: android.graphics.drawable.Icon,
mutableColorScheme: ColorScheme,
width: Int,
- height: Int
+ height: Int,
): LayerDrawable {
val albumArt = MediaArtworkHelper.getScaledBackground(context, artworkIcon, width, height)
return MediaArtworkHelper.setUpGradientColorOnDrawable(
@@ -517,7 +520,7 @@
context.getDrawable(R.drawable.qs_media_scrim)?.mutate() as GradientDrawable,
mutableColorScheme,
MEDIA_PLAYER_SCRIM_START_ALPHA,
- MEDIA_PLAYER_SCRIM_END_ALPHA
+ MEDIA_PLAYER_SCRIM_END_ALPHA,
)
}
@@ -544,7 +547,7 @@
private fun createTouchRippleAnimation(
button: ImageButton,
colorSchemeTransition: ColorSchemeTransition,
- multiRippleView: MultiRippleView
+ multiRippleView: MultiRippleView,
): RippleAnimation {
val maxSize = (multiRippleView.width * 2).toFloat()
return RippleAnimation(
@@ -562,7 +565,7 @@
baseRingFadeParams = null,
sparkleRingFadeParams = null,
centerFillFadeParams = null,
- shouldDistort = false
+ shouldDistort = false,
)
)
}
@@ -596,7 +599,7 @@
set: ConstraintSet,
resId: Int,
visible: Boolean,
- notVisibleValue: Int
+ notVisibleValue: Int,
) {
set.setVisibility(resId, if (visible) ConstraintSet.VISIBLE else notVisibleValue)
set.setAlpha(resId, if (visible) 1.0f else 0.0f)
@@ -618,7 +621,7 @@
collapsedSet: ConstraintSet,
visible: Boolean,
notVisibleValue: Int,
- showInCollapsed: Boolean
+ showInCollapsed: Boolean,
) {
if (notVisibleValue == ConstraintSet.INVISIBLE) {
// Since time views should appear instead of buttons.
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaActionViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaActionViewModel.kt
index 82099e6..3f22d54 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaActionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaActionViewModel.kt
@@ -32,4 +32,16 @@
val buttonId: Int? = null,
val isEnabled: Boolean,
val onClicked: (Int) -> Unit,
-)
+) {
+ fun contentEquals(other: MediaActionViewModel?): Boolean {
+ return other?.let {
+ contentDescription == other.contentDescription &&
+ isVisibleWhenScrubbing == other.isVisibleWhenScrubbing &&
+ notVisibleValue == other.notVisibleValue &&
+ showInCollapsed == other.showInCollapsed &&
+ rebindId == other.rebindId &&
+ buttonId == other.buttonId &&
+ isEnabled == other.isEnabled
+ } ?: false
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModel.kt
index 64820e0..104d155 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModel.kt
@@ -69,18 +69,30 @@
mediaControl?.let { toViewModel(it) }
}
}
- .distinctUntilChanged()
+ .distinctUntilChanged { old, new ->
+ (new == null && old == null) || new?.contentEquals(old) ?: false
+ }
.flowOn(backgroundDispatcher)
private var isPlaying = false
private var isAnyButtonClicked = false
private var location = -1
+ private var playerViewModel: MediaPlayerViewModel? = null
+
+ fun isNewPlayer(viewModel: MediaPlayerViewModel): Boolean {
+ val contentEquals = playerViewModel?.contentEquals(viewModel) ?: false
+ return (!contentEquals).also { playerViewModel = viewModel }
+ }
+
+ fun onMediaControlIsBound(artistName: CharSequence, titleName: CharSequence) {
+ interactor.logMediaControlIsBound(artistName, titleName)
+ }
private fun onDismissMediaData(
token: Token?,
uid: Int,
packageName: String,
- instanceId: InstanceId
+ instanceId: InstanceId,
) {
logger.logLongPressDismiss(uid, packageName, instanceId)
interactor.removeMediaControl(
@@ -88,7 +100,7 @@
instanceId,
MEDIA_PLAYER_ANIMATION_DELAY,
SMARTSPACE_CARD_DISMISS_EVENT,
- location
+ location,
)
}
@@ -99,7 +111,7 @@
applicationContext,
backgroundDispatcher,
model.artwork,
- TAG
+ TAG,
)
val scheme =
wallpaperColors?.let { ColorScheme(it, true, Style.CONTENT) }
@@ -107,7 +119,7 @@
applicationContext,
model.packageName,
TAG,
- Style.CONTENT
+ Style.CONTENT,
)
?: return null
@@ -131,7 +143,7 @@
R.string.controls_media_playing_item_description,
model.songName,
model.artistName,
- model.appName
+ model.appName,
)
}
},
@@ -157,7 +169,7 @@
expandable,
clickIntent,
SMARTSPACE_CARD_CLICK_EVENT,
- location
+ location,
)
}
},
@@ -177,7 +189,7 @@
}
}
},
- onLocationChanged = { location = it }
+ onLocationChanged = { location = it },
)
}
@@ -191,7 +203,7 @@
device?.name?.let {
TextUtils.equals(
it,
- applicationContext.getString(R.string.broadcasting_description_is_broadcasting)
+ applicationContext.getString(R.string.broadcasting_description_is_broadcasting),
)
} ?: false
val useDisabledAlpha =
@@ -236,19 +248,19 @@
logger.logOpenBroadcastDialog(
model.uid,
model.packageName,
- model.instanceId
+ model.instanceId,
)
interactor.startBroadcastDialog(
expandable,
device?.name.toString(),
- model.packageName
+ model.packageName,
)
} else {
logger.logOpenOutputSwitcher(model.uid, model.packageName, model.instanceId)
interactor.startMediaOutputDialog(
expandable,
model.packageName,
- model.token
+ model.token,
)
}
} else {
@@ -257,10 +269,10 @@
?: interactor.startMediaOutputDialog(
expandable,
model.packageName,
- model.token
+ model.token,
)
}
- }
+ },
)
}
@@ -270,7 +282,7 @@
if (model.isDismissible) {
applicationContext.getString(
R.string.controls_media_close_session,
- model.appName
+ model.appName,
)
} else {
applicationContext.getString(R.string.controls_media_active_session)
@@ -304,7 +316,7 @@
model,
mediaButton.getActionById(buttonId),
buttonId,
- isScrubbingTimeEnabled
+ isScrubbingTimeEnabled,
)
}
}
@@ -319,7 +331,7 @@
model: MediaControlModel,
mediaAction: MediaAction?,
buttonId: Int,
- canShowScrubbingTimeViews: Boolean
+ canShowScrubbingTimeViews: Boolean,
): MediaActionViewModel {
val showInCollapsed = SEMANTIC_ACTIONS_COMPACT.contains(buttonId)
val hideWhenScrubbing = SEMANTIC_ACTIONS_HIDE_WHEN_SCRUBBING.contains(buttonId)
@@ -353,7 +365,7 @@
private fun toNotifActionViewModel(
model: MediaControlModel,
mediaAction: MediaAction,
- index: Int
+ index: Int,
): MediaActionViewModel {
return MediaActionViewModel(
icon = mediaAction.icon,
@@ -375,7 +387,7 @@
uid: Int,
packageName: String,
instanceId: InstanceId,
- action: Runnable
+ action: Runnable,
) {
logger.logTapAction(id, uid, packageName, instanceId)
interactor.logSmartspaceUserEvent(SMARTSPACE_CARD_CLICK_EVENT, location)
@@ -424,7 +436,7 @@
R.id.actionPrev,
R.id.actionNext,
R.id.action0,
- R.id.action1
+ R.id.action1,
)
const val TURBULENCE_NOISE_PLAY_MS_DURATION = 7500L
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaOutputSwitcherViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaOutputSwitcherViewModel.kt
index 9df9bcc..2a47a5a 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaOutputSwitcherViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaOutputSwitcherViewModel.kt
@@ -29,4 +29,15 @@
val alpha: Float,
val isVisible: Boolean,
val onClicked: (Expandable) -> Unit,
-)
+) {
+ fun contentEquals(other: MediaOutputSwitcherViewModel?): Boolean {
+ return (other?.let {
+ isTapEnabled == other.isTapEnabled &&
+ deviceString == other.deviceString &&
+ isCurrentBroadcastApp == other.isCurrentBroadcastApp &&
+ isIntentValid == other.isIntentValid &&
+ alpha == other.alpha &&
+ isVisible == other.isVisible
+ } ?: false)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaPlayerViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaPlayerViewModel.kt
index 96e7fc7..f4b0d6e 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaPlayerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaPlayerViewModel.kt
@@ -43,4 +43,30 @@
val onSeek: () -> Unit,
val onBindSeekbar: (SeekBarViewModel) -> Unit,
val onLocationChanged: (Int) -> Unit,
-)
+) {
+ fun contentEquals(other: MediaPlayerViewModel?): Boolean {
+ return other?.let {
+ other.backgroundCover == backgroundCover &&
+ appIcon == other.appIcon &&
+ useGrayColorFilter == other.useGrayColorFilter &&
+ artistName == other.artistName &&
+ titleName == other.titleName &&
+ isExplicitVisible == other.isExplicitVisible &&
+ shouldAddGradient == other.shouldAddGradient &&
+ canShowTime == other.canShowTime &&
+ playTurbulenceNoise == other.playTurbulenceNoise &&
+ useSemanticActions == other.useSemanticActions &&
+ areActionsEqual(other.actionButtons) &&
+ outputSwitcher.contentEquals(other.outputSwitcher)
+ } ?: false
+ }
+
+ private fun areActionsEqual(other: List<MediaActionViewModel>): Boolean {
+ actionButtons.forEachIndexed { index, mediaActionViewModel ->
+ if (!mediaActionViewModel.contentEquals(other[index])) {
+ return false
+ }
+ }
+ return true
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
index 2cbc7575..f7b7353 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
@@ -157,7 +157,7 @@
@VisibleForTesting
boolean mNeedRefresh = false;
private MediaController mMediaController;
- private InputRouteManager mInputRouteManager;
+ @VisibleForTesting InputRouteManager mInputRouteManager;
@VisibleForTesting
Callback mCallback;
@VisibleForTesting
@@ -927,7 +927,18 @@
}
public List<MediaDevice> getSelectedMediaDevice() {
- return mLocalMediaManager.getSelectedMediaDevice();
+ if (!enableInputRouting()) {
+ return mLocalMediaManager.getSelectedMediaDevice();
+ }
+
+ // Add selected input device if input routing is supported.
+ List<MediaDevice> selectedDevices =
+ new ArrayList<>(mLocalMediaManager.getSelectedMediaDevice());
+ MediaDevice selectedInputDevice = mInputRouteManager.getSelectedInputDevice();
+ if (selectedInputDevice != null) {
+ selectedDevices.add(selectedInputDevice);
+ }
+ return selectedDevices;
}
List<MediaDevice> getDeselectableMediaDevice() {
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java
index 4251b81..8351597 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java
@@ -34,6 +34,7 @@
import android.app.Activity;
import android.app.ActivityOptions.LaunchCookie;
import android.app.AlertDialog;
+import android.app.KeyguardManager;
import android.app.StatusBarManager;
import android.app.compat.CompatChanges;
import android.content.Context;
@@ -83,6 +84,7 @@
private final StatusBarManager mStatusBarManager;
private final MediaProjectionMetricsLogger mMediaProjectionMetricsLogger;
private final ScreenCaptureDisabledDialogDelegate mScreenCaptureDisabledDialogDelegate;
+ private final KeyguardManager mKeyguardManager;
private String mPackageName;
private int mUid;
@@ -101,11 +103,13 @@
FeatureFlags featureFlags,
Lazy<ScreenCaptureDevicePolicyResolver> screenCaptureDevicePolicyResolver,
StatusBarManager statusBarManager,
+ KeyguardManager keyguardManager,
MediaProjectionMetricsLogger mediaProjectionMetricsLogger,
ScreenCaptureDisabledDialogDelegate screenCaptureDisabledDialogDelegate) {
mFeatureFlags = featureFlags;
mScreenCaptureDevicePolicyResolver = screenCaptureDevicePolicyResolver;
mStatusBarManager = statusBarManager;
+ mKeyguardManager = keyguardManager;
mMediaProjectionMetricsLogger = mediaProjectionMetricsLogger;
mScreenCaptureDisabledDialogDelegate = screenCaptureDisabledDialogDelegate;
}
@@ -208,7 +212,14 @@
}
setUpDialog(mDialog);
- mDialog.show();
+
+ boolean shouldDismissKeyguard =
+ com.android.systemui.Flags.mediaProjectionDialogBehindLockscreen();
+ if (shouldDismissKeyguard && mKeyguardManager.isDeviceLocked()) {
+ requestDeviceUnlock();
+ } else {
+ mDialog.show();
+ }
if (savedInstanceState == null) {
mMediaProjectionMetricsLogger.notifyPermissionRequestDisplayed(mUid);
@@ -332,6 +343,16 @@
return false;
}
+ private void requestDeviceUnlock() {
+ mKeyguardManager.requestDismissKeyguard(this,
+ new KeyguardManager.KeyguardDismissCallback() {
+ @Override
+ public void onDismissSucceeded() {
+ mDialog.show();
+ }
+ });
+ }
+
private void grantMediaProjectionPermission(
int screenShareMode, boolean hasCastingCapabilities) {
try {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
index 526c64c..55943a5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
@@ -16,6 +16,8 @@
package com.android.systemui.statusbar.dagger
+import android.content.Context
+import com.android.app.viewcapture.ViewCaptureAwareWindowManager
import com.android.systemui.CoreStartable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.log.LogBuffer
@@ -63,12 +65,18 @@
@Binds abstract fun statusBarInitializer(impl: StatusBarInitializerImpl): StatusBarInitializer
- @Binds
- abstract fun statusBarWindowController(
- impl: StatusBarWindowControllerImpl
- ): StatusBarWindowController
-
companion object {
+
+ @Provides
+ @SysUISingleton
+ fun statusBarWindowController(
+ context: Context?,
+ viewCaptureAwareWindowManager: ViewCaptureAwareWindowManager?,
+ factory: StatusBarWindowControllerImpl.Factory,
+ ): StatusBarWindowController {
+ return factory.create(context, viewCaptureAwareWindowManager)
+ }
+
@Provides
@SysUISingleton
@OngoingCallLog
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImpl.java
index 1a0327c..1ee7cf3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImpl.java
@@ -28,7 +28,6 @@
import static com.android.systemui.util.leak.RotationUtils.ROTATION_UPSIDE_DOWN;
import android.content.Context;
-import android.content.res.Resources;
import android.graphics.Insets;
import android.graphics.PixelFormat;
import android.graphics.Rect;
@@ -52,23 +51,23 @@
import com.android.internal.policy.SystemBarUtils;
import com.android.systemui.animation.ActivityTransitionAnimator;
import com.android.systemui.animation.DelegateTransitionAnimatorController;
-import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.fragments.FragmentHostManager;
import com.android.systemui.fragments.FragmentService;
import com.android.systemui.res.R;
import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider;
+import com.android.systemui.statusbar.window.StatusBarWindowModule.InternalWindowViewInflater;
import com.android.systemui.unfold.UnfoldTransitionProgressProvider;
import com.android.systemui.unfold.util.JankMonitorTransitionProgressListener;
-import java.util.Optional;
+import dagger.assisted.Assisted;
+import dagger.assisted.AssistedFactory;
+import dagger.assisted.AssistedInject;
-import javax.inject.Inject;
+import java.util.Optional;
/**
* Encapsulates all logic for the status bar window state management.
*/
-@SysUISingleton
public class StatusBarWindowControllerImpl implements StatusBarWindowController {
private static final String TAG = "StatusBarWindowController";
private static final boolean DEBUG = false;
@@ -90,21 +89,20 @@
private final WindowManager.LayoutParams mLpChanged;
private final Binder mInsetsSourceOwner = new Binder();
- @Inject
+ @AssistedInject
public StatusBarWindowControllerImpl(
- Context context,
- @StatusBarWindowModule.InternalWindowView StatusBarWindowView statusBarWindowView,
- ViewCaptureAwareWindowManager viewCaptureAwareWindowManager,
+ @Assisted Context context,
+ @InternalWindowViewInflater StatusBarWindowViewInflater statusBarWindowViewInflater,
+ @Assisted ViewCaptureAwareWindowManager viewCaptureAwareWindowManager,
IWindowManager iWindowManager,
StatusBarContentInsetsProvider contentInsetsProvider,
FragmentService fragmentService,
- @Main Resources resources,
Optional<UnfoldTransitionProgressProvider> unfoldTransitionProgressProvider) {
mContext = context;
mWindowManager = viewCaptureAwareWindowManager;
mIWindowManager = iWindowManager;
mContentInsetsProvider = contentInsetsProvider;
- mStatusBarWindowView = statusBarWindowView;
+ mStatusBarWindowView = statusBarWindowViewInflater.inflate(context);
mFragmentService = fragmentService;
mLaunchAnimationContainer = mStatusBarWindowView.findViewById(
R.id.status_bar_launch_animation_container);
@@ -354,4 +352,13 @@
mLpChanged.forciblyShownTypes &= ~WindowInsets.Type.statusBars();
}
}
+
+ @AssistedFactory
+ public interface Factory {
+ /** Creates a new instance. */
+ StatusBarWindowControllerImpl create(
+ Context context,
+ ViewCaptureAwareWindowManager viewCaptureAwareWindowManager);
+ }
+
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowModule.kt
index 1c7debc..ebfbac7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowModule.kt
@@ -1,47 +1,36 @@
package com.android.systemui.statusbar.window
-import android.view.LayoutInflater
-import com.android.systemui.res.R
import com.android.systemui.dagger.SysUISingleton
+import dagger.Binds
import dagger.Module
-import dagger.Provides
import javax.inject.Qualifier
/** Module providing dependencies related to the status bar window. */
@Module
abstract class StatusBarWindowModule {
- /**
- * Provides a [StatusBarWindowView].
- *
- * Only [StatusBarWindowController] should inject the view.
- */
- @Module
- companion object {
- @JvmStatic
- @Provides
- @SysUISingleton
- @InternalWindowView
- fun providesStatusBarWindowView(layoutInflater: LayoutInflater): StatusBarWindowView {
- return layoutInflater.inflate(
- R.layout.super_status_bar,
- /* root= */null
- ) as StatusBarWindowView?
- ?: throw IllegalStateException(
- "R.layout.super_status_bar could not be properly inflated"
- )
- }
- }
/**
- * We want [StatusBarWindowView] to be provided to [StatusBarWindowController]'s constructor via
- * dagger so that we can provide a fake window view when testing the controller. However, we wan
- * want *only* the controller to be able to inject the window view.
+ * Binds a [StatusBarWindowViewInflater].
*
- * This protected qualifier annotation achieves this. [StatusBarWindowView] can only be injected
- * if it's annotated with [InternalWindowView], and only classes inside this [statusbar.window]
- * package can access the annotation.
+ * Only [StatusBarWindowControllerImpl] should inject it.
+ */
+ @Binds
+ @SysUISingleton
+ @InternalWindowViewInflater
+ abstract fun providesStatusBarWindowViewInflater(
+ inflaterImpl: StatusBarWindowViewInflaterImpl
+ ): StatusBarWindowViewInflater
+
+ /**
+ * We want [StatusBarWindowViewInflater] to be provided to [StatusBarWindowControllerImpl]'s
+ * constructor via dagger so that we can provide a fake window view when testing the controller.
+ * However, we wan want *only* the controller to be able to inject the window view.
+ *
+ * This protected qualifier annotation achieves this. [StatusBarWindowViewInflater] can only be
+ * injected if it's annotated with [InternalWindowViewInflater], and only classes inside this
+ * [statusbar.window] package can access the annotation.
*/
@Retention(AnnotationRetention.BINARY)
@Qualifier
- protected annotation class InternalWindowView
+ protected annotation class InternalWindowViewInflater
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowViewInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowViewInflater.kt
new file mode 100644
index 0000000..f030a4a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowViewInflater.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.window
+
+import android.content.Context
+import android.view.LayoutInflater
+import com.android.systemui.res.R
+import javax.inject.Inject
+
+/**
+ * Inflates a [StatusBarWindowView]. Exists so that it can be injected into
+ * [StatusBarWindowControllerImpl] and be swapped for a fake implementation in tests.
+ */
+interface StatusBarWindowViewInflater {
+ fun inflate(context: Context): StatusBarWindowView
+}
+
+class StatusBarWindowViewInflaterImpl @Inject constructor() : StatusBarWindowViewInflater {
+
+ override fun inflate(context: Context): StatusBarWindowView {
+ val layoutInflater = LayoutInflater.from(context)
+ return layoutInflater.inflate(R.layout.super_status_bar, /* root= */ null)
+ as StatusBarWindowView?
+ ?: throw IllegalStateException(
+ "R.layout.super_status_bar could not be properly inflated"
+ )
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java b/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java
index bbfa32b..32a4f12 100644
--- a/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java
+++ b/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java
@@ -117,7 +117,14 @@
int displayId) {
Runnable showToastRunnable = () -> {
UserHandle userHandle = UserHandle.getUserHandleForUid(uid);
- Context context = mContext.createContextAsUser(userHandle, 0);
+ Context context;
+ try {
+ context = mContext.createContextAsUser(userHandle, 0);
+ } catch (IllegalStateException e) {
+ // b/366533044 : Own package not found for systemui
+ Log.e(TAG, "Cannot create toast because cannot create context", e);
+ return;
+ }
DisplayManager mDisplayManager = mContext.getSystemService(DisplayManager.class);
Display display = mDisplayManager.getDisplay(displayId);
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
index 7385b82..e764015 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
@@ -22,6 +22,7 @@
import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL;
import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL_ALL;
import static android.service.notification.NotificationListenerService.REASON_GROUP_SUMMARY_CANCELED;
+import static android.service.notification.NotificationListenerService.REASON_PACKAGE_BANNED;
import static android.service.notification.NotificationStats.DISMISSAL_BUBBLE;
import static android.service.notification.NotificationStats.DISMISS_SENTIMENT_NEUTRAL;
@@ -449,7 +450,8 @@
@Override
public void onEntryRemoved(NotificationEntry entry,
@NotifCollection.CancellationReason int reason) {
- if (reason == REASON_APP_CANCEL || reason == REASON_APP_CANCEL_ALL) {
+ if (reason == REASON_APP_CANCEL || reason == REASON_APP_CANCEL_ALL
+ || reason == REASON_PACKAGE_BANNED) {
BubblesManager.this.onEntryRemoved(entry);
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java
index d3e20c6..53f0800 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java
@@ -73,6 +73,7 @@
import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.media.InputMediaDevice;
+import com.android.settingslib.media.InputRouteManager;
import com.android.settingslib.media.LocalMediaManager;
import com.android.settingslib.media.MediaDevice;
import com.android.systemui.SysuiTestCase;
@@ -100,6 +101,7 @@
import org.mockito.MockitoAnnotations;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
@SmallTest
@@ -115,6 +117,10 @@
private static final String TEST_SONG = "test_song";
private static final String TEST_SESSION_ID = "test_session_id";
private static final String TEST_SESSION_NAME = "test_session_name";
+ private static final int MAX_VOLUME = 1;
+ private static final int CURRENT_VOLUME = 0;
+ private static final boolean VOLUME_FIXED_TRUE = true;
+
@Mock
private DialogTransitionAnimator mDialogTransitionAnimator;
@Mock
@@ -181,6 +187,7 @@
private String mPackageName = null;
private MediaSwitchingController mMediaSwitchingController;
private LocalMediaManager mLocalMediaManager;
+ private InputRouteManager mInputRouteManager;
private List<MediaController> mMediaControllers = new ArrayList<>();
private List<MediaDevice> mMediaDevices = new ArrayList<>();
private List<NearbyDevice> mNearbyDevices = new ArrayList<>();
@@ -228,6 +235,10 @@
mLocalMediaManager = spy(mMediaSwitchingController.mLocalMediaManager);
when(mLocalMediaManager.isPreferenceRouteListingExist()).thenReturn(false);
mMediaSwitchingController.mLocalMediaManager = mLocalMediaManager;
+ mMediaSwitchingController.mInputRouteManager =
+ new InputRouteManager(mContext, mAudioManager);
+ mInputRouteManager = spy(mMediaSwitchingController.mInputRouteManager);
+ mMediaSwitchingController.mInputRouteManager = mInputRouteManager;
MediaDescription.Builder builder = new MediaDescription.Builder();
builder.setTitle(TEST_SONG);
builder.setSubtitle(TEST_ARTIST);
@@ -545,9 +556,6 @@
// Output devices have changed.
mMediaSwitchingController.onDeviceListUpdate(mMediaDevices);
- final int MAX_VOLUME = 1;
- final int CURRENT_VOLUME = 0;
- final boolean IS_VOLUME_FIXED = true;
final MediaDevice mediaDevice3 =
InputMediaDevice.create(
mContext,
@@ -555,7 +563,7 @@
AudioDeviceInfo.TYPE_BUILTIN_MIC,
MAX_VOLUME,
CURRENT_VOLUME,
- IS_VOLUME_FIXED);
+ VOLUME_FIXED_TRUE);
final MediaDevice mediaDevice4 =
InputMediaDevice.create(
mContext,
@@ -563,7 +571,7 @@
AudioDeviceInfo.TYPE_WIRED_HEADSET,
MAX_VOLUME,
CURRENT_VOLUME,
- IS_VOLUME_FIXED);
+ VOLUME_FIXED_TRUE);
final List<MediaDevice> inputDevices = new ArrayList<>();
inputDevices.add(mediaDevice3);
inputDevices.add(mediaDevice4);
@@ -1312,4 +1320,23 @@
verify(mCallback).dismissDialog();
}
+
+ @EnableFlags(Flags.FLAG_ENABLE_AUDIO_INPUT_DEVICE_ROUTING_AND_VOLUME_CONTROL)
+ @Test
+ public void getSelectedMediaDevice() {
+ // Mock MediaDevice since none of the output media device constructor is publicly available
+ // outside of SettingsLib package.
+ final MediaDevice selectedOutputMediaDevice = mock(MediaDevice.class);
+ doReturn(Collections.singletonList(selectedOutputMediaDevice))
+ .when(mLocalMediaManager)
+ .getSelectedMediaDevice();
+
+ // Mock selected input media device.
+ final MediaDevice selectedInputMediaDevice = mock(MediaDevice.class);
+ doReturn(selectedInputMediaDevice).when(mInputRouteManager).getSelectedInputDevice();
+
+ List<MediaDevice> selectedMediaDevices = mMediaSwitchingController.getSelectedMediaDevice();
+ assertThat(selectedMediaDevices)
+ .containsExactly(selectedOutputMediaDevice, selectedInputMediaDevice);
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index 3e7980d..0d39834 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -24,6 +24,7 @@
import static android.service.notification.NotificationListenerService.NOTIFICATION_CHANNEL_OR_GROUP_UPDATED;
import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL;
import static android.service.notification.NotificationListenerService.REASON_GROUP_SUMMARY_CANCELED;
+import static android.service.notification.NotificationListenerService.REASON_PACKAGE_BANNED;
import static androidx.test.ext.truth.content.IntentSubject.assertThat;
@@ -1100,6 +1101,18 @@
}
@Test
+ public void testNotifsBanned_entryListenerRemove() {
+ mEntryListener.onEntryAdded(mRow);
+ mBubbleController.updateBubble(mBubbleEntry);
+
+ assertTrue(mBubbleController.hasBubbles());
+
+ // Removes the notification
+ mEntryListener.onEntryRemoved(mRow, REASON_PACKAGE_BANNED);
+ assertFalse(mBubbleController.hasBubbles());
+ }
+
+ @Test
public void removeBubble_intercepted() {
mEntryListener.onEntryAdded(mRow);
mBubbleController.updateBubble(mBubbleEntry);
diff --git a/packages/SystemUI/tests/utils/src/android/hardware/input/FakeInputManager.kt b/packages/SystemUI/tests/utils/src/android/hardware/input/FakeInputManager.kt
index ee36cad..de4bbec 100644
--- a/packages/SystemUI/tests/utils/src/android/hardware/input/FakeInputManager.kt
+++ b/packages/SystemUI/tests/utils/src/android/hardware/input/FakeInputManager.kt
@@ -84,7 +84,7 @@
if (devices.containsKey(deviceId)) {
return
}
- addPhysicalKeyboard(deviceId, enabled)
+ addPhysicalKeyboard(deviceId, enabled = enabled)
}
fun registerInputDeviceListener(listener: InputDeviceListener) {
@@ -92,9 +92,15 @@
inputDeviceListener = listener
}
- fun addPhysicalKeyboard(id: Int, enabled: Boolean = true) {
+ fun addPhysicalKeyboard(
+ id: Int,
+ vendorId: Int = 0,
+ productId: Int = 0,
+ isFullKeyboard: Boolean = true,
+ enabled: Boolean = true
+ ) {
check(id > 0) { "Physical keyboard ids have to be > 0" }
- addKeyboard(id, enabled)
+ addKeyboard(id, vendorId, productId, isFullKeyboard, enabled)
}
fun removeKeysFromKeyboard(deviceId: Int, vararg keyCodes: Int) {
@@ -102,20 +108,38 @@
supportedKeyCodesByDeviceId[deviceId]!!.removeAll(keyCodes.asList())
}
- private fun addKeyboard(id: Int, enabled: Boolean = true) {
- devices[id] =
+ private fun addKeyboard(
+ id: Int,
+ vendorId: Int = 0,
+ productId: Int = 0,
+ isFullKeyboard: Boolean = true,
+ enabled: Boolean = true
+ ) {
+ val keyboardType =
+ if (isFullKeyboard) InputDevice.KEYBOARD_TYPE_ALPHABETIC
+ else InputDevice.KEYBOARD_TYPE_NON_ALPHABETIC
+ // VendorId and productId are set to 0 if not specified, which is the same as the default
+ // values used in InputDevice.Builder
+ val builder =
InputDevice.Builder()
.setId(id)
- .setKeyboardType(InputDevice.KEYBOARD_TYPE_ALPHABETIC)
+ .setVendorId(vendorId)
+ .setProductId(productId)
+ .setKeyboardType(keyboardType)
.setSources(InputDevice.SOURCE_KEYBOARD)
.setEnabled(enabled)
.setKeyCharacterMap(keyCharacterMap)
- .build()
+ devices[id] = builder.build()
+ inputDeviceListener?.onInputDeviceAdded(id)
supportedKeyCodesByDeviceId[id] = allKeyCodes.toMutableSet()
}
- fun addDevice(id: Int, sources: Int) {
- devices[id] = InputDevice.Builder().setId(id).setSources(sources).build()
+ fun addDevice(id: Int, sources: Int, isNotFound: Boolean = false) {
+ // there's not way of differentiate device connection vs registry in current implementation.
+ // If the device isNotFound, it means that we connect an unregistered device.
+ if (!isNotFound) {
+ devices[id] = InputDevice.Builder().setId(id).setSources(sources).build()
+ }
inputDeviceListener?.onInputDeviceAdded(id)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractorKosmos.kt
index 6e650a3..77afa79 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractorKosmos.kt
@@ -21,6 +21,7 @@
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.media.controls.data.repository.mediaFilterRepository
import com.android.systemui.media.controls.domain.pipeline.mediaDataProcessor
+import com.android.systemui.media.controls.shared.mediaLogger
import com.android.systemui.media.controls.util.mediaInstanceId
import com.android.systemui.media.mediaOutputDialogManager
import com.android.systemui.plugins.activityStarter
@@ -39,5 +40,6 @@
lockscreenUserManager = notificationLockscreenUserManager,
mediaOutputDialogManager = mediaOutputDialogManager,
broadcastDialogController = mockBroadcastDialogController,
+ mediaLogger = mediaLogger,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/interactor/factory/MediaControlInteractorFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/interactor/factory/MediaControlInteractorFactoryKosmos.kt
index e490b75..9ea660f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/interactor/factory/MediaControlInteractorFactoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/interactor/factory/MediaControlInteractorFactoryKosmos.kt
@@ -23,6 +23,7 @@
import com.android.systemui.media.controls.data.repository.mediaFilterRepository
import com.android.systemui.media.controls.domain.pipeline.interactor.MediaControlInteractor
import com.android.systemui.media.controls.domain.pipeline.mediaDataProcessor
+import com.android.systemui.media.controls.shared.mediaLogger
import com.android.systemui.media.mediaOutputDialogManager
import com.android.systemui.plugins.activityStarter
import com.android.systemui.statusbar.notificationLockscreenUserManager
@@ -42,6 +43,7 @@
lockscreenUserManager = notificationLockscreenUserManager,
mediaOutputDialogManager = mediaOutputDialogManager,
broadcastDialogController = mockBroadcastDialogController,
+ mediaLogger = mediaLogger,
)
}
}
diff --git a/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java b/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java
index 8c6f50e..e29b6e4 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java
@@ -145,13 +145,25 @@
ArrayMap<String, ArraySet<String>> removedFunctionsDiffMap =
getRemovedFunctionsDiffMap(staticPackageToFunctionMap, runtimePackageToFunctionMap);
- Set<AppSearchSchema> appRuntimeMetadataSchemas =
- getAllRuntimeMetadataSchemas(staticPackageToFunctionMap.keySet());
- appRuntimeMetadataSchemas.add(
- AppFunctionRuntimeMetadata.createParentAppFunctionRuntimeSchema());
+ if (!staticPackageToFunctionMap.keySet().equals(runtimePackageToFunctionMap.keySet())) {
+ // Drop removed packages from removedFunctionsDiffMap, as setSchema() deletes them
+ ArraySet<String> removedPackages =
+ getRemovedPackages(
+ staticPackageToFunctionMap.keySet(), removedFunctionsDiffMap.keySet());
+ for (String packageName : removedPackages) {
+ removedFunctionsDiffMap.remove(packageName);
+ }
+ Set<AppSearchSchema> appRuntimeMetadataSchemas =
+ getAllRuntimeMetadataSchemas(staticPackageToFunctionMap.keySet());
+ appRuntimeMetadataSchemas.add(
+ AppFunctionRuntimeMetadata.createParentAppFunctionRuntimeSchema());
+ SetSchemaRequest addSetSchemaRequest =
+ buildSetSchemaRequestForRuntimeMetadataSchemas(
+ mPackageManager, appRuntimeMetadataSchemas);
+ Objects.requireNonNull(
+ runtimeMetadataSearchSession.setSchema(addSetSchemaRequest).get());
+ }
- // Operation order matters here. i.e. remove -> setSchema -> add. Otherwise we would
- // encounter an error trying to delete a document with no existing schema.
if (!removedFunctionsDiffMap.isEmpty()) {
RemoveByDocumentIdRequest removeByDocumentIdRequest =
buildRemoveRuntimeMetadataRequest(removedFunctionsDiffMap);
@@ -164,12 +176,6 @@
}
if (!addedFunctionsDiffMap.isEmpty()) {
- // TODO(b/357551503): only set schema on package diff
- SetSchemaRequest addSetSchemaRequest =
- buildSetSchemaRequestForRuntimeMetadataSchemas(
- mPackageManager, appRuntimeMetadataSchemas);
- Objects.requireNonNull(
- runtimeMetadataSearchSession.setSchema(addSetSchemaRequest).get());
PutDocumentsRequest putDocumentsRequest =
buildPutRuntimeMetadataRequest(addedFunctionsDiffMap);
AppSearchBatchResult<String, Void> putDocumentBatchResult =
@@ -276,6 +282,30 @@
}
/**
+ * This method returns a set of packages that are in the removed function packages but not in
+ * the all existing static packages.
+ *
+ * @param allExistingStaticPackages A set of all existing static metadata packages.
+ * @param removedFunctionPackages A set of all removed function packages.
+ * @return A set of packages that are in the removed function packages but not in the all
+ * existing static packages.
+ */
+ @NonNull
+ private static ArraySet<String> getRemovedPackages(
+ @NonNull Set<String> allExistingStaticPackages,
+ @NonNull Set<String> removedFunctionPackages) {
+ ArraySet<String> removedPackages = new ArraySet<>();
+
+ for (String packageName : removedFunctionPackages) {
+ if (!allExistingStaticPackages.contains(packageName)) {
+ removedPackages.add(packageName);
+ }
+ }
+
+ return removedPackages;
+ }
+
+ /**
* This method returns a map of package names to a set of function ids that are in the static
* metadata but not in the runtime metadata.
*
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index b109472..2fa0e0d 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -720,6 +720,9 @@
void handleInlineSuggestionRequest(InlineSuggestionsRequest inlineSuggestionsRequest,
ViewState viewState) {
+ if (sVerbose) {
+ Slog.v(TAG, "handleInlineSuggestionRequest(): inline suggestion request received");
+ }
synchronized (mLock) {
if (!mWaitForInlineRequest || mPendingInlineSuggestionsRequest != null) {
return;
@@ -734,15 +737,27 @@
@GuardedBy("mLock")
void maybeRequestFillLocked() {
if (mPendingFillRequest == null) {
+ if (sVerbose) {
+ Slog.v(TAG, "maybeRequestFillLocked(): cancelling calling fill request "
+ + "due to empty pending fill request");
+ }
return;
}
mFieldClassificationIdSnapshot = sIdCounterForPcc.get();
if (mWaitForInlineRequest) {
if (mPendingInlineSuggestionsRequest == null) {
+ if (sVerbose) {
+ Slog.v(TAG, "maybeRequestFillLocked(): cancelling calling fill request "
+ + "due to waiting for inline request and pending inline request is "
+ + "currently empty");
+ }
return;
}
-
+ if (sVerbose) {
+ Slog.v(TAG, "maybeRequestFillLocked(): adding inline request to pending "
+ + "fill request");
+ }
mPendingFillRequest = new FillRequest(mPendingFillRequest.getId(),
mPendingFillRequest.getFillContexts(),
mPendingFillRequest.getHints(),
@@ -750,8 +765,17 @@
mPendingFillRequest.getFlags(),
mPendingInlineSuggestionsRequest,
mPendingFillRequest.getDelayedFillIntentSender());
+ } else {
+ if (sVerbose) {
+ Slog.v(TAG, "maybeRequestFillLocked(): not adding inline request to pending "
+ + "fill request");
+ }
}
+
mLastFillRequest = mPendingFillRequest;
+ if (sVerbose) {
+ Slog.v(TAG, "maybeRequestFillLocked(): sending fill request");
+ }
if (shouldRequestSecondaryProvider(mPendingFillRequest.getFlags())
&& mSecondaryProviderHandler != null) {
Slog.v(TAG, "Requesting fill response to secondary provider.");
diff --git a/services/core/java/com/android/server/BatteryService.java b/services/core/java/com/android/server/BatteryService.java
index 6657c1c..59dea09 100644
--- a/services/core/java/com/android/server/BatteryService.java
+++ b/services/core/java/com/android/server/BatteryService.java
@@ -160,6 +160,7 @@
private int mLastChargeCounter;
private int mLastBatteryCycleCount;
private int mLastChargingState;
+ private int mLastBatteryCapacityLevel;
/**
* The last seen charging policy. This requires the
* {@link android.Manifest.permission#BATTERY_STATS} permission and should therefore not be
@@ -609,7 +610,8 @@
|| mHealthInfo.batteryChargeCounterUah != mLastChargeCounter
|| mInvalidCharger != mLastInvalidCharger
|| mHealthInfo.batteryCycleCount != mLastBatteryCycleCount
- || mHealthInfo.chargingState != mLastChargingState)) {
+ || mHealthInfo.chargingState != mLastChargingState
+ || mHealthInfo.batteryCapacityLevel != mLastBatteryCapacityLevel)) {
if (mPlugType != mLastPlugType) {
if (mLastPlugType == BATTERY_PLUGGED_NONE) {
@@ -829,6 +831,7 @@
mLastInvalidCharger = mInvalidCharger;
mLastBatteryCycleCount = mHealthInfo.batteryCycleCount;
mLastChargingState = mHealthInfo.chargingState;
+ mLastBatteryCapacityLevel = mHealthInfo.batteryCapacityLevel;
}
}
@@ -862,6 +865,7 @@
intent.putExtra(BatteryManager.EXTRA_CHARGE_COUNTER, mHealthInfo.batteryChargeCounterUah);
intent.putExtra(BatteryManager.EXTRA_CYCLE_COUNT, mHealthInfo.batteryCycleCount);
intent.putExtra(BatteryManager.EXTRA_CHARGING_STATUS, mHealthInfo.chargingState);
+ intent.putExtra(BatteryManager.EXTRA_CAPACITY_LEVEL, mHealthInfo.batteryCapacityLevel);
if (DEBUG) {
Slog.d(TAG, "Sending ACTION_BATTERY_CHANGED. scale:" + BATTERY_SCALE
+ ", info:" + mHealthInfo.toString());
@@ -964,6 +968,7 @@
event.putLong(BatteryManager.EXTRA_EVENT_TIMESTAMP, now);
event.putInt(BatteryManager.EXTRA_CYCLE_COUNT, mHealthInfo.batteryCycleCount);
event.putInt(BatteryManager.EXTRA_CHARGING_STATUS, mHealthInfo.chargingState);
+ event.putInt(BatteryManager.EXTRA_CAPACITY_LEVEL, mHealthInfo.batteryCapacityLevel);
boolean queueWasEmpty = mBatteryLevelsEventQueue.isEmpty();
mBatteryLevelsEventQueue.add(event);
@@ -1401,6 +1406,7 @@
pw.println(" technology: " + mHealthInfo.batteryTechnology);
pw.println(" Charging state: " + mHealthInfo.chargingState);
pw.println(" Charging policy: " + mHealthInfo.chargingPolicy);
+ pw.println(" Capacity level: " + mHealthInfo.batteryCapacityLevel);
} else {
Shell shell = new Shell();
shell.exec(mBinderService, null, fd, null, args, null, new ResultReceiver(null));
diff --git a/services/core/java/com/android/server/UiModeManagerService.java b/services/core/java/com/android/server/UiModeManagerService.java
index 1c13ad5..f32031de 100644
--- a/services/core/java/com/android/server/UiModeManagerService.java
+++ b/services/core/java/com/android/server/UiModeManagerService.java
@@ -32,11 +32,11 @@
import static android.app.UiModeManager.PROJECTION_TYPE_NONE;
import static android.os.UserHandle.USER_SYSTEM;
import static android.os.UserHandle.getCallingUserId;
-import static android.os.UserManager.isVisibleBackgroundUsersEnabled;
import static android.provider.Settings.Secure.CONTRAST_LEVEL;
import static android.util.TimeUtils.isTimeBetween;
import static com.android.internal.util.FunctionalUtils.ignoreRemoteException;
+import static com.android.server.pm.UserManagerService.enforceCurrentUserIfVisibleBackgroundEnabled;
import android.annotation.IntRange;
import android.annotation.NonNull;
@@ -100,7 +100,6 @@
import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
import com.android.internal.notification.SystemNotificationChannels;
import com.android.internal.util.DumpUtils;
-import com.android.server.pm.UserManagerService;
import com.android.server.twilight.TwilightListener;
import com.android.server.twilight.TwilightManager;
import com.android.server.twilight.TwilightState;
@@ -850,7 +849,7 @@
}
final int user = UserHandle.getCallingUserId();
- enforceValidCallingUser(user);
+ enforceCurrentUserIfVisibleBackgroundEnabled(user);
final long ident = Binder.clearCallingIdentity();
try {
@@ -914,7 +913,7 @@
@AttentionModeThemeOverlayType int attentionModeThemeOverlayType) {
setAttentionModeThemeOverlay_enforcePermission();
- enforceValidCallingUser(UserHandle.getCallingUserId());
+ enforceCurrentUserIfVisibleBackgroundEnabled(UserHandle.getCallingUserId());
synchronized (mLock) {
if (mAttentionModeThemeOverlay != attentionModeThemeOverlayType) {
@@ -1005,7 +1004,7 @@
return false;
}
final int user = Binder.getCallingUserHandle().getIdentifier();
- enforceValidCallingUser(user);
+ enforceCurrentUserIfVisibleBackgroundEnabled(user);
if (user != mCurrentUser && getContext().checkCallingOrSelfPermission(
android.Manifest.permission.INTERACT_ACROSS_USERS)
@@ -1064,7 +1063,7 @@
return;
}
final int user = UserHandle.getCallingUserId();
- enforceValidCallingUser(user);
+ enforceCurrentUserIfVisibleBackgroundEnabled(user);
final long ident = Binder.clearCallingIdentity();
try {
@@ -1094,7 +1093,7 @@
return;
}
final int user = UserHandle.getCallingUserId();
- enforceValidCallingUser(user);
+ enforceCurrentUserIfVisibleBackgroundEnabled(user);
final long ident = Binder.clearCallingIdentity();
try {
@@ -1116,7 +1115,7 @@
assertLegit(callingPackage);
assertSingleProjectionType(projectionType);
enforceProjectionTypePermissions(projectionType);
- enforceValidCallingUser(getCallingUserId());
+ enforceCurrentUserIfVisibleBackgroundEnabled(getCallingUserId());
synchronized (mLock) {
if (mProjectionHolders == null) {
@@ -1162,7 +1161,7 @@
assertLegit(callingPackage);
assertSingleProjectionType(projectionType);
enforceProjectionTypePermissions(projectionType);
- enforceValidCallingUser(getCallingUserId());
+ enforceCurrentUserIfVisibleBackgroundEnabled(getCallingUserId());
return releaseProjectionUnchecked(projectionType, callingPackage);
}
@@ -1204,7 +1203,7 @@
return;
}
- enforceValidCallingUser(getCallingUserId());
+ enforceCurrentUserIfVisibleBackgroundEnabled(getCallingUserId());
synchronized (mLock) {
if (mProjectionListeners == null) {
@@ -1253,32 +1252,6 @@
}
};
- // This method validates whether calling user is valid in visible background users
- // feature. Valid user is the current user or the system or in the same profile group as
- // the current user.
- private void enforceValidCallingUser(int userId) {
- if (!isVisibleBackgroundUsersEnabled()) {
- return;
- }
- if (LOG) {
- Slog.d(TAG, "enforceValidCallingUser: userId=" + userId
- + " isSystemUser=" + (userId == USER_SYSTEM) + " current user=" + mCurrentUser
- + " callingPid=" + Binder.getCallingPid()
- + " callingUid=" + mInjector.getCallingUid());
- }
- long ident = Binder.clearCallingIdentity();
- try {
- if (userId != USER_SYSTEM && userId != mCurrentUser
- && !UserManagerService.getInstance().isSameProfileGroup(userId, mCurrentUser)) {
- throw new SecurityException(
- "Calling user is not valid for level-1 compatibility in MUMD. "
- + "callingUserId=" + userId + " currentUserId=" + mCurrentUser);
- }
- } finally {
- Binder.restoreCallingIdentity(ident);
- }
- }
-
private void enforceProjectionTypePermissions(@UiModeManager.ProjectionType int p) {
if ((p & PROJECTION_TYPE_AUTOMOTIVE) != 0) {
getContext().enforceCallingPermission(
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 54a7410..871c320 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -714,12 +714,14 @@
/**
* Map userId to its companion app uids.
*/
+ @GuardedBy("mCompanionAppUidsMap")
private final Map<Integer, Set<Integer>> mCompanionAppUidsMap = new ArrayMap<>();
/**
* The profile owner UIDs.
*/
- private ArraySet<Integer> mProfileOwnerUids = null;
+ @GuardedBy("mProfileOwnerUids")
+ private final ArraySet<Integer> mProfileOwnerUids = new ArraySet<>();
final UserController mUserController;
@VisibleForTesting
@@ -17535,32 +17537,35 @@
@Override
public void setProfileOwnerUid(ArraySet<Integer> profileOwnerUids) {
- synchronized (ActivityManagerService.this) {
- mProfileOwnerUids = profileOwnerUids;
+ synchronized (mProfileOwnerUids) {
+ mProfileOwnerUids.clear();
+ mProfileOwnerUids.addAll(profileOwnerUids);
}
}
@Override
public boolean isProfileOwner(int uid) {
- synchronized (ActivityManagerService.this) {
- return mProfileOwnerUids != null && mProfileOwnerUids.indexOf(uid) >= 0;
+ synchronized (mProfileOwnerUids) {
+ return mProfileOwnerUids.indexOf(uid) >= 0;
}
}
@Override
public void setCompanionAppUids(int userId, Set<Integer> companionAppUids) {
- synchronized (ActivityManagerService.this) {
+ synchronized (mCompanionAppUidsMap) {
mCompanionAppUidsMap.put(userId, companionAppUids);
}
}
@Override
public boolean isAssociatedCompanionApp(int userId, int uid) {
- final Set<Integer> allUids = mCompanionAppUidsMap.get(userId);
- if (allUids == null) {
- return false;
+ synchronized (mCompanionAppUidsMap) {
+ final Set<Integer> allUids = mCompanionAppUidsMap.get(userId);
+ if (allUids == null) {
+ return false;
+ }
+ return allUids.contains(uid);
}
- return allUids.contains(uid);
}
@Override
diff --git a/services/core/java/com/android/server/am/CoreSettingsObserver.java b/services/core/java/com/android/server/am/CoreSettingsObserver.java
index 6bb56c9..e885c14 100644
--- a/services/core/java/com/android/server/am/CoreSettingsObserver.java
+++ b/services/core/java/com/android/server/am/CoreSettingsObserver.java
@@ -79,6 +79,7 @@
sSecureSettingToTypeMap.put(Settings.Secure.MULTI_PRESS_TIMEOUT, int.class);
sSecureSettingToTypeMap.put(Settings.Secure.KEY_REPEAT_TIMEOUT_MS, int.class);
sSecureSettingToTypeMap.put(Settings.Secure.KEY_REPEAT_DELAY_MS, int.class);
+ sSecureSettingToTypeMap.put(Settings.Secure.KEY_REPEAT_ENABLED, int.class);
sSecureSettingToTypeMap.put(Settings.Secure.STYLUS_POINTER_ICON_ENABLED, int.class);
// add other secure settings here...
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index b186eaa..262c76e 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -156,6 +156,9 @@
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
@@ -223,18 +226,9 @@
private static final int USER_SWITCH_CALLBACKS_TIMEOUT_MS = 5 * 1000;
/**
- * Amount of time waited for
- * {@link ActivityTaskManagerInternal.ScreenObserver#onKeyguardStateChanged} callbacks to be
- * called after calling {@link WindowManagerService#lockDeviceNow}.
- * Otherwise, we should throw a {@link RuntimeException} and never dismiss the
- * {@link UserSwitchingDialog}.
- */
- static final int SHOW_KEYGUARD_TIMEOUT_MS = 20 * 1000;
-
- /**
* Amount of time waited for {@link WindowManagerService#dismissKeyguard} callbacks to be
* called after dismissing the keyguard.
- * Otherwise, we should move on to dismiss the dialog {@link #dismissUserSwitchDialog}}
+ * Otherwise, we should move on to dismiss the dialog {@link #dismissUserSwitchDialog()}
* and report user switch is complete {@link #REPORT_USER_SWITCH_COMPLETE_MSG}.
*/
private static final int DISMISS_KEYGUARD_TIMEOUT_MS = 2 * 1000;
@@ -1986,10 +1980,18 @@
// it should be moved outside, but for now it's not as there are many calls to
// external components here afterwards
updateProfileRelatedCaches();
+ dispatchOnBeforeUserSwitching(userId);
mInjector.getWindowManager().setCurrentUser(userId);
mInjector.reportCurWakefulnessUsageEvent();
+ // Once the internal notion of the active user has switched, we lock the device
+ // with the option to show the user switcher on the keyguard.
if (userSwitchUiEnabled) {
mInjector.getWindowManager().setSwitchingUser(true);
+ // Only lock if the user has a secure keyguard PIN/Pattern/Pwd
+ if (mInjector.getKeyguardManager().isDeviceSecure(userId)) {
+ // Make sure the device is locked before moving on with the user switch
+ mInjector.lockDeviceNowAndWaitForKeyguardShown();
+ }
}
} else {
@@ -2284,6 +2286,25 @@
mUserSwitchObservers.finishBroadcast();
}
+ private void dispatchOnBeforeUserSwitching(@UserIdInt int newUserId) {
+ final TimingsTraceAndSlog t = new TimingsTraceAndSlog();
+ t.traceBegin("dispatchOnBeforeUserSwitching-" + newUserId);
+ final int observerCount = mUserSwitchObservers.beginBroadcast();
+ for (int i = 0; i < observerCount; i++) {
+ final String name = "#" + i + " " + mUserSwitchObservers.getBroadcastCookie(i);
+ t.traceBegin("onBeforeUserSwitching-" + name);
+ try {
+ mUserSwitchObservers.getBroadcastItem(i).onBeforeUserSwitching(newUserId);
+ } catch (RemoteException e) {
+ // Ignore
+ } finally {
+ t.traceEnd();
+ }
+ }
+ mUserSwitchObservers.finishBroadcast();
+ t.traceEnd();
+ }
+
/** Called on handler thread */
@VisibleForTesting
void dispatchUserSwitchComplete(@UserIdInt int oldUserId, @UserIdInt int newUserId) {
@@ -2499,17 +2520,6 @@
final int observerCount = mUserSwitchObservers.beginBroadcast();
if (observerCount > 0) {
- for (int i = 0; i < observerCount; i++) {
- final String name = "#" + i + " " + mUserSwitchObservers.getBroadcastCookie(i);
- t.traceBegin("onBeforeUserSwitching-" + name);
- try {
- mUserSwitchObservers.getBroadcastItem(i).onBeforeUserSwitching(newUserId);
- } catch (RemoteException e) {
- // Ignore
- } finally {
- t.traceEnd();
- }
- }
final ArraySet<String> curWaitingUserSwitchCallbacks = new ArraySet<>();
synchronized (mLock) {
uss.switching = true;
@@ -2606,56 +2616,34 @@
@VisibleForTesting
void completeUserSwitch(int oldUserId, int newUserId) {
- final Runnable sendUserSwitchCompleteMessage = () -> {
- mHandler.removeMessages(REPORT_USER_SWITCH_COMPLETE_MSG);
- mHandler.sendMessage(mHandler.obtainMessage(
- REPORT_USER_SWITCH_COMPLETE_MSG, oldUserId, newUserId));
- };
- if (isUserSwitchUiEnabled()) {
- if (mInjector.getKeyguardManager().isDeviceSecure(newUserId)) {
- this.showKeyguard(() -> dismissUserSwitchDialog(sendUserSwitchCompleteMessage));
- } else {
- this.dismissKeyguard(() -> dismissUserSwitchDialog(sendUserSwitchCompleteMessage));
- }
+ final boolean isUserSwitchUiEnabled = isUserSwitchUiEnabled();
+ // serialize each conditional step
+ await(
+ // STEP 1 - If there is no challenge set, dismiss the keyguard right away
+ isUserSwitchUiEnabled && !mInjector.getKeyguardManager().isDeviceSecure(newUserId),
+ mInjector::dismissKeyguard,
+ () -> await(
+ // STEP 2 - If user switch ui was enabled, dismiss user switch dialog
+ isUserSwitchUiEnabled,
+ this::dismissUserSwitchDialog,
+ () -> {
+ // STEP 3 - Send REPORT_USER_SWITCH_COMPLETE_MSG to broadcast
+ // ACTION_USER_SWITCHED & call UserSwitchObservers.onUserSwitchComplete
+ mHandler.removeMessages(REPORT_USER_SWITCH_COMPLETE_MSG);
+ mHandler.sendMessage(mHandler.obtainMessage(
+ REPORT_USER_SWITCH_COMPLETE_MSG, oldUserId, newUserId));
+ }
+ ));
+ }
+
+ private void await(boolean condition, Consumer<Runnable> conditionalStep, Runnable nextStep) {
+ if (condition) {
+ conditionalStep.accept(nextStep);
} else {
- sendUserSwitchCompleteMessage.run();
+ nextStep.run();
}
}
- protected void showKeyguard(Runnable runnable) {
- runWithTimeout(mInjector::showKeyguard, SHOW_KEYGUARD_TIMEOUT_MS, runnable, () -> {
- throw new RuntimeException(
- "Keyguard is not shown in " + SHOW_KEYGUARD_TIMEOUT_MS + " ms.");
- }, "showKeyguard");
- }
-
- protected void dismissKeyguard(Runnable runnable) {
- runWithTimeout(mInjector::dismissKeyguard, DISMISS_KEYGUARD_TIMEOUT_MS, runnable, runnable,
- "dismissKeyguard");
- }
-
- private void runWithTimeout(Consumer<Runnable> task, int timeoutMs, Runnable onSuccess,
- Runnable onTimeout, String traceMsg) {
- final AtomicInteger state = new AtomicInteger(0); // state = 0 (RUNNING)
-
- asyncTraceBegin(traceMsg, 0);
-
- mHandler.postDelayed(() -> {
- if (state.compareAndSet(0, 1)) { // state = 1 (TIMEOUT)
- asyncTraceEnd(traceMsg, 0);
- Slogf.w(TAG, "Timeout: %s did not finish in %d ms", traceMsg, timeoutMs);
- onTimeout.run();
- }
- }, timeoutMs);
-
- task.accept(() -> {
- if (state.compareAndSet(0, 2)) { // state = 2 (SUCCESS)
- asyncTraceEnd(traceMsg, 0);
- onSuccess.run();
- }
- });
- }
-
private void moveUserToForeground(UserState uss, int newUserId) {
boolean homeInFront = mInjector.taskSupervisorSwitchUser(newUserId, uss);
if (homeInFront) {
@@ -4100,45 +4088,29 @@
return IStorageManager.Stub.asInterface(ServiceManager.getService("mount"));
}
- protected void showKeyguard(Runnable runnable) {
- if (getWindowManager().isKeyguardLocked()) {
- runnable.run();
- return;
- }
- getActivityTaskManagerInternal().registerScreenObserver(
- new ActivityTaskManagerInternal.ScreenObserver() {
- @Override
- public void onAwakeStateChanged(boolean isAwake) {
-
- }
-
- @Override
- public void onKeyguardStateChanged(boolean isShowing) {
- if (isShowing) {
- getActivityTaskManagerInternal().unregisterScreenObserver(this);
- runnable.run();
- }
- }
- }
- );
- getWindowManager().lockDeviceNow();
- }
-
protected void dismissKeyguard(Runnable runnable) {
+ final AtomicBoolean isFirst = new AtomicBoolean(true);
+ final Runnable runOnce = () -> {
+ if (isFirst.getAndSet(false)) {
+ runnable.run();
+ }
+ };
+
+ mHandler.postDelayed(runOnce, DISMISS_KEYGUARD_TIMEOUT_MS);
getWindowManager().dismissKeyguard(new IKeyguardDismissCallback.Stub() {
@Override
public void onDismissError() throws RemoteException {
- runnable.run();
+ mHandler.post(runOnce);
}
@Override
public void onDismissSucceeded() throws RemoteException {
- runnable.run();
+ mHandler.post(runOnce);
}
@Override
public void onDismissCancelled() throws RemoteException {
- runnable.run();
+ mHandler.post(runOnce);
}
}, /* message= */ null);
}
@@ -4164,5 +4136,43 @@
void onSystemUserVisibilityChanged(boolean visible) {
getUserManagerInternal().onSystemUserVisibilityChanged(visible);
}
+
+ void lockDeviceNowAndWaitForKeyguardShown() {
+ if (getWindowManager().isKeyguardLocked()) {
+ return;
+ }
+
+ final TimingsTraceAndSlog t = new TimingsTraceAndSlog();
+ t.traceBegin("lockDeviceNowAndWaitForKeyguardShown");
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ ActivityTaskManagerInternal.ScreenObserver screenObserver =
+ new ActivityTaskManagerInternal.ScreenObserver() {
+ @Override
+ public void onAwakeStateChanged(boolean isAwake) {
+
+ }
+
+ @Override
+ public void onKeyguardStateChanged(boolean isShowing) {
+ if (isShowing) {
+ latch.countDown();
+ }
+ }
+ };
+
+ getActivityTaskManagerInternal().registerScreenObserver(screenObserver);
+ getWindowManager().lockDeviceNow();
+ try {
+ if (!latch.await(20, TimeUnit.SECONDS)) {
+ throw new RuntimeException("Keyguard is not shown in 20 seconds");
+ }
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ } finally {
+ getActivityTaskManagerInternal().unregisterScreenObserver(screenObserver);
+ t.traceEnd();
+ }
+ }
}
}
diff --git a/services/core/java/com/android/server/input/InputSettingsObserver.java b/services/core/java/com/android/server/input/InputSettingsObserver.java
index 835fb72..d70bd8b 100644
--- a/services/core/java/com/android/server/input/InputSettingsObserver.java
+++ b/services/core/java/com/android/server/input/InputSettingsObserver.java
@@ -94,6 +94,8 @@
(reason) -> updateKeyRepeatInfo()),
Map.entry(Settings.Secure.getUriFor(Settings.Secure.KEY_REPEAT_DELAY_MS),
(reason) -> updateKeyRepeatInfo()),
+ Map.entry(Settings.Secure.getUriFor(Settings.Secure.KEY_REPEAT_ENABLED),
+ (reason) -> updateKeyRepeatInfo()),
Map.entry(Settings.System.getUriFor(Settings.System.SHOW_ROTARY_INPUT),
(reason) -> updateShowRotaryInput()),
Map.entry(Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_BOUNCE_KEYS),
@@ -230,6 +232,11 @@
}
private void updateKeyRepeatInfo() {
+ // Key repeat is enabled by default
+ final boolean keyRepeatEnabled = Settings.Secure.getIntForUser(
+ mContext.getContentResolver(), Settings.Secure.KEY_REPEAT_ENABLED, 1,
+ UserHandle.USER_CURRENT) != 0;
+
// Use ViewConfiguration getters only as fallbacks because they may return stale values.
final int timeoutMs = Settings.Secure.getIntForUser(mContext.getContentResolver(),
Settings.Secure.KEY_REPEAT_TIMEOUT_MS, ViewConfiguration.getKeyRepeatTimeout(),
@@ -237,7 +244,7 @@
final int delayMs = Settings.Secure.getIntForUser(mContext.getContentResolver(),
Settings.Secure.KEY_REPEAT_DELAY_MS, ViewConfiguration.getKeyRepeatDelay(),
UserHandle.USER_CURRENT);
- mNative.setKeyRepeatConfiguration(timeoutMs, delayMs);
+ mNative.setKeyRepeatConfiguration(timeoutMs, delayMs, keyRepeatEnabled);
}
private void updateMaximumObscuringOpacityForTouch() {
diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java
index 1e7c97f9..5dd461d 100644
--- a/services/core/java/com/android/server/input/NativeInputManagerService.java
+++ b/services/core/java/com/android/server/input/NativeInputManagerService.java
@@ -212,7 +212,7 @@
void setMotionClassifierEnabled(boolean enabled);
- void setKeyRepeatConfiguration(int timeoutMs, int delayMs);
+ void setKeyRepeatConfiguration(int timeoutMs, int delayMs, boolean keyRepeatEnabled);
InputSensorInfo[] getSensorList(int deviceId);
@@ -509,7 +509,8 @@
public native void setMotionClassifierEnabled(boolean enabled);
@Override
- public native void setKeyRepeatConfiguration(int timeoutMs, int delayMs);
+ public native void setKeyRepeatConfiguration(int timeoutMs, int delayMs,
+ boolean keyRepeatEnabled);
@Override
public native InputSensorInfo[] getSensorList(int deviceId);
diff --git a/services/core/java/com/android/server/location/gnss/NetworkTimeHelper.java b/services/core/java/com/android/server/location/gnss/NetworkTimeHelper.java
index 01c108b..494ea77 100644
--- a/services/core/java/com/android/server/location/gnss/NetworkTimeHelper.java
+++ b/services/core/java/com/android/server/location/gnss/NetworkTimeHelper.java
@@ -19,6 +19,7 @@
import android.annotation.ElapsedRealtimeLong;
import android.annotation.NonNull;
import android.content.Context;
+import android.location.flags.Flags;
import android.os.Looper;
import java.io.PrintWriter;
@@ -55,7 +56,7 @@
static NetworkTimeHelper create(
@NonNull Context context, @NonNull Looper looper,
@NonNull InjectTimeCallback injectTimeCallback) {
- if (USE_TIME_DETECTOR_IMPL) {
+ if (!Flags.useLegacyNtpTime()) {
TimeDetectorNetworkTimeHelper.Environment environment =
new TimeDetectorNetworkTimeHelper.EnvironmentImpl(looper);
return new TimeDetectorNetworkTimeHelper(environment, injectTimeCallback);
diff --git a/services/core/java/com/android/server/location/injector/SystemEmergencyHelper.java b/services/core/java/com/android/server/location/injector/SystemEmergencyHelper.java
index df45a6e..177eefb 100644
--- a/services/core/java/com/android/server/location/injector/SystemEmergencyHelper.java
+++ b/services/core/java/com/android/server/location/injector/SystemEmergencyHelper.java
@@ -76,11 +76,12 @@
try {
mIsInEmergencyCall = mTelephonyManager.isEmergencyNumber(
intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER));
- dispatchEmergencyStateChanged();
} catch (IllegalStateException | UnsupportedOperationException e) {
Log.w(TAG, "Failed to call TelephonyManager.isEmergencyNumber().", e);
}
}
+
+ dispatchEmergencyStateChanged();
}
}, new IntentFilter(Intent.ACTION_NEW_OUTGOING_CALL));
@@ -140,9 +141,10 @@
if (mIsInEmergencyCall) {
mEmergencyCallEndRealtimeMs = SystemClock.elapsedRealtime();
mIsInEmergencyCall = false;
- dispatchEmergencyStateChanged();
}
}
+
+ dispatchEmergencyStateChanged();
}
}
}
diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
index 621c090..48d24f2 100644
--- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
+++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
@@ -179,7 +179,8 @@
/**
* In order to record the keyguard, the MediaProjection package must be either:
* - a holder of RECORD_SENSITIVE_CONTENT permission, or
- * - be one of the bugreport whitelisted packages
+ * - be one of the bugreport allowlisted packages, or
+ * - hold the OP_PROJECT_MEDIA AppOp.
*/
private boolean canCaptureKeyguard() {
if (!android.companion.virtualdevice.flags.Flags.mediaProjectionKeyguardRestrictions()) {
@@ -194,6 +195,14 @@
== PackageManager.PERMISSION_GRANTED) {
return true;
}
+ boolean operationActive = mAppOps.isOperationActive(AppOpsManager.OP_PROJECT_MEDIA,
+ mProjectionGrant.uid,
+ mProjectionGrant.packageName);
+ if (operationActive) {
+ // Some tools use media projection by granting the OP_PROJECT_MEDIA app
+ // op via a shell command. Those tools can be granted keyguard capture
+ return true;
+ }
return SystemConfig.getInstance().getBugreportWhitelistedPackages()
.contains(mProjectionGrant.packageName);
}
diff --git a/services/core/java/com/android/server/pm/TEST_MAPPING b/services/core/java/com/android/server/pm/TEST_MAPPING
index c75622c..21a6df2 100644
--- a/services/core/java/com/android/server/pm/TEST_MAPPING
+++ b/services/core/java/com/android/server/pm/TEST_MAPPING
@@ -208,6 +208,22 @@
"name": "CtsUpdateOwnershipEnforcementTestCases"
},
{
+ "name": "CtsPackageInstallerCUJDeviceAdminTestCases",
+ "file_patterns": [
+ "core/java/.*Install.*",
+ "services/core/.*Install.*",
+ "services/core/java/com/android/server/pm/.*"
+ ],
+ "options":[
+ {
+ "exclude-annotation":"androidx.test.filters.FlakyTest"
+ },
+ {
+ "exclude-annotation":"org.junit.Ignore"
+ }
+ ]
+ },
+ {
"name": "CtsPackageInstallerCUJInstallationTestCases",
"file_patterns": [
"core/java/.*Install.*",
@@ -224,6 +240,22 @@
]
},
{
+ "name": "CtsPackageInstallerCUJMultiUsersTestCases",
+ "file_patterns": [
+ "core/java/.*Install.*",
+ "services/core/.*Install.*",
+ "services/core/java/com/android/server/pm/.*"
+ ],
+ "options":[
+ {
+ "exclude-annotation":"androidx.test.filters.FlakyTest"
+ },
+ {
+ "exclude-annotation":"org.junit.Ignore"
+ }
+ ]
+ },
+ {
"name": "CtsPackageInstallerCUJUninstallationTestCases",
"file_patterns": [
"core/java/.*Install.*",
diff --git a/services/core/java/com/android/server/rollback/RollbackPackageHealthObserver.java b/services/core/java/com/android/server/rollback/RollbackPackageHealthObserver.java
index 68026ea..e3d71e4 100644
--- a/services/core/java/com/android/server/rollback/RollbackPackageHealthObserver.java
+++ b/services/core/java/com/android/server/rollback/RollbackPackageHealthObserver.java
@@ -24,8 +24,11 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.WorkerThread;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
+import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
@@ -78,6 +81,8 @@
public final class RollbackPackageHealthObserver implements PackageHealthObserver {
private static final String TAG = "RollbackPackageHealthObserver";
private static final String NAME = "rollback-observer";
+ private static final String ACTION_NAME = RollbackPackageHealthObserver.class.getName();
+
private static final int PERSISTENT_MASK = ApplicationInfo.FLAG_PERSISTENT
| ApplicationInfo.FLAG_SYSTEM;
@@ -596,12 +601,40 @@
}
};
- final LocalIntentReceiver rollbackReceiver = new LocalIntentReceiver(result -> {
- mHandler.post(() -> onResult.accept(result));
- });
+ if (Flags.refactorCrashrecovery()) {
+ // Define a BroadcastReceiver to handle the result
+ BroadcastReceiver rollbackReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent result) {
+ mHandler.post(() -> onResult.accept(result));
+ }
+ };
- rollbackManager.commitRollback(rollback.getRollbackId(),
- Collections.singletonList(failedPackage), rollbackReceiver.getIntentSender());
+ // Register the BroadcastReceiver
+ mContext.registerReceiver(rollbackReceiver,
+ new IntentFilter(ACTION_NAME),
+ Context.RECEIVER_NOT_EXPORTED);
+
+ Intent intentReceiver = new Intent(ACTION_NAME);
+ intentReceiver.putExtra("rollbackId", rollback.getRollbackId());
+ intentReceiver.setPackage(mContext.getPackageName());
+
+ PendingIntent rollbackPendingIntent = PendingIntent.getBroadcast(mContext,
+ rollback.getRollbackId(),
+ intentReceiver,
+ PendingIntent.FLAG_MUTABLE);
+
+ rollbackManager.commitRollback(rollback.getRollbackId(),
+ Collections.singletonList(failedPackage),
+ rollbackPendingIntent.getIntentSender());
+ } else {
+ final LocalIntentReceiver rollbackReceiver = new LocalIntentReceiver(result -> {
+ mHandler.post(() -> onResult.accept(result));
+ });
+
+ rollbackManager.commitRollback(rollback.getRollbackId(),
+ Collections.singletonList(failedPackage), rollbackReceiver.getIntentSender());
+ }
}
/**
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 3d5b273..6009b4a 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -6218,7 +6218,8 @@
public void onProcessRemoved(String name, int uid) {
synchronized (mGlobalLockWithoutBoost) {
final WindowProcessController proc = mProcessNames.remove(name, uid);
- if (proc != null && !mStartingProcessActivities.isEmpty()) {
+ if (proc != null && !proc.mHasEverAttached
+ && !mStartingProcessActivities.isEmpty()) {
// Use a copy in case finishIfPossible changes the list indirectly.
final ArrayList<ActivityRecord> activities =
new ArrayList<>(mStartingProcessActivities);
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index 8f5612c..84072e2 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -1841,6 +1841,7 @@
}
boolean attachApplication(WindowProcessController app) throws RemoteException {
+ app.mHasEverAttached = true;
final ArrayList<ActivityRecord> activities = mService.mStartingProcessActivities;
RemoteException remoteException = null;
boolean hasActivityStarted = false;
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 655a6fb..0a47522 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -2925,6 +2925,9 @@
final TaskFragment taskFragment = target.asTaskFragment();
final boolean isEmbeddedTaskFragment = taskFragment != null
&& taskFragment.isEmbedded();
+ final IBinder taskFragmentToken =
+ taskFragment != null ? taskFragment.getFragmentToken() : null;
+ change.setTaskFragmentToken(taskFragmentToken);
final ActivityRecord activityRecord = target.asActivityRecord();
if (task != null) {
diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java
index 30d6f0a..32fe303 100644
--- a/services/core/java/com/android/server/wm/WindowProcessController.java
+++ b/services/core/java/com/android/server/wm/WindowProcessController.java
@@ -204,6 +204,9 @@
// Set to true when process was launched with a wrapper attached
private volatile boolean mUsingWrapper;
+ /** Whether this process has ever completed ActivityThread#handleBindApplication. */
+ boolean mHasEverAttached;
+
/** Non-null if this process may have a window. */
@Nullable
Session mWindowSession;
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index 155e73c..d2493c5 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -2725,12 +2725,13 @@
}
static void nativeSetKeyRepeatConfiguration(JNIEnv* env, jobject nativeImplObj, jint timeoutMs,
- jint delayMs) {
+ jint delayMs, jboolean keyRepeatEnabled) {
NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
im->getInputManager()->getDispatcher().setKeyRepeatConfiguration(std::chrono::milliseconds(
timeoutMs),
std::chrono::milliseconds(
- delayMs));
+ delayMs),
+ keyRepeatEnabled);
}
static jobject createInputSensorInfo(JNIEnv* env, jstring name, jstring vendor, jint version,
@@ -3029,7 +3030,7 @@
{"setDisplayEligibilityForPointerCapture", "(IZ)V",
(void*)nativeSetDisplayEligibilityForPointerCapture},
{"setMotionClassifierEnabled", "(Z)V", (void*)nativeSetMotionClassifierEnabled},
- {"setKeyRepeatConfiguration", "(II)V", (void*)nativeSetKeyRepeatConfiguration},
+ {"setKeyRepeatConfiguration", "(IIZ)V", (void*)nativeSetKeyRepeatConfiguration},
{"getSensorList", "(I)[Landroid/hardware/input/InputSensorInfo;",
(void*)nativeGetSensorList},
{"getTouchpadHardwareProperties",
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index ab459df..3b334ec 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -107,7 +107,7 @@
import com.android.internal.os.RuntimeInit;
import com.android.internal.policy.AttributeCache;
import com.android.internal.protolog.ProtoLog;
-import com.android.internal.protolog.ProtoLogConfigurationService;
+import com.android.internal.protolog.ProtoLogConfigurationServiceImpl;
import com.android.internal.protolog.ProtoLogGroup;
import com.android.internal.util.ConcurrentUtils;
import com.android.internal.util.EmergencyAffordanceManager;
@@ -436,6 +436,10 @@
private static final String PROFILING_SERVICE_JAR_PATH =
"/apex/com.android.profiling/javalib/service-profiling.jar";
+ private static final String RANGING_APEX_SERVICE_JAR_PATH =
+ "/apex/com.android.uwb/javalib/service-ranging.jar";
+ private static final String RANGING_SERVICE_CLASS = "com.android.server.ranging.RangingService";
+
private static final String TETHERING_CONNECTOR_CLASS = "android.net.ITetheringConnector";
private static final String PERSISTENT_DATA_BLOCK_PROP = "ro.frp.pst";
@@ -1097,7 +1101,7 @@
if (android.tracing.Flags.clientSideProtoLogging()) {
t.traceBegin("StartProtoLogConfigurationService");
ServiceManager.addService(
- Context.PROTOLOG_CONFIGURATION_SERVICE, new ProtoLogConfigurationService());
+ Context.PROTOLOG_CONFIGURATION_SERVICE, new ProtoLogConfigurationServiceImpl());
t.traceEnd();
}
@@ -3015,6 +3019,17 @@
t.traceEnd();
}
+ if (com.android.ranging.flags.Flags.rangingStackEnabled()) {
+ if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_UWB)
+ || context.getPackageManager().hasSystemFeature(
+ PackageManager.FEATURE_WIFI_RTT)) {
+ t.traceBegin("RangingService");
+ mSystemServiceManager.startServiceFromJar(RANGING_SERVICE_CLASS,
+ RANGING_APEX_SERVICE_JAR_PATH);
+ t.traceEnd();
+ }
+ }
+
t.traceBegin("StartBootPhaseDeviceSpecificServicesReady");
mSystemServiceManager.startBootPhase(t, SystemService.PHASE_DEVICE_SPECIFIC_SERVICES_READY);
t.traceEnd();
diff --git a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
index a25621a..390eb93 100644
--- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
@@ -61,7 +61,6 @@
import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doCallRealMethod;
import static org.mockito.Mockito.doNothing;
@@ -95,7 +94,6 @@
import android.os.Message;
import android.os.PowerManagerInternal;
import android.os.RemoteException;
-import android.os.SystemClock;
import android.os.UserHandle;
import android.os.UserManager;
import android.os.storage.IStorageManager;
@@ -214,10 +212,7 @@
doNothing().when(mInjector).activityManagerOnUserStopped(anyInt());
doNothing().when(mInjector).clearBroadcastQueueForUser(anyInt());
doNothing().when(mInjector).taskSupervisorRemoveUser(anyInt());
- doAnswer(invocation -> {
- ((Runnable) invocation.getArgument(0)).run();
- return null;
- }).when(mInjector).showKeyguard(any());
+ doNothing().when(mInjector).lockDeviceNowAndWaitForKeyguardShown();
mockIsUsersOnSecondaryDisplaysEnabled(false);
// All UserController params are set to default.
@@ -432,6 +427,7 @@
mUserController.registerUserSwitchObserver(observer, "mock");
// Start user -- this will update state of mUserController
mUserController.startUser(TEST_USER_ID, USER_START_MODE_FOREGROUND);
+ verify(observer, times(1)).onBeforeUserSwitching(eq(TEST_USER_ID));
Message reportMsg = mInjector.mHandler.getMessageForCode(REPORT_USER_SWITCH_MSG);
assertNotNull(reportMsg);
UserState userState = (UserState) reportMsg.obj;
@@ -440,7 +436,6 @@
// Call dispatchUserSwitch and verify that observer was called only once
mInjector.mHandler.clearAllRecordedMessages();
mUserController.dispatchUserSwitch(userState, oldUserId, newUserId);
- verify(observer, times(1)).onBeforeUserSwitching(eq(TEST_USER_ID));
verify(observer, times(1)).onUserSwitching(eq(TEST_USER_ID), any());
Set<Integer> expectedCodes = Collections.singleton(CONTINUE_USER_SWITCH_MSG);
Set<Integer> actualCodes = mInjector.mHandler.getMessageCodes();
@@ -463,6 +458,7 @@
mUserController.registerUserSwitchObserver(observer, "mock");
// Start user -- this will update state of mUserController
mUserController.startUser(TEST_USER_ID, USER_START_MODE_FOREGROUND);
+ verify(observer, times(1)).onBeforeUserSwitching(eq(TEST_USER_ID));
Message reportMsg = mInjector.mHandler.getMessageForCode(REPORT_USER_SWITCH_MSG);
assertNotNull(reportMsg);
UserState userState = (UserState) reportMsg.obj;
@@ -471,7 +467,6 @@
// Call dispatchUserSwitch and verify that observer was called only once
mInjector.mHandler.clearAllRecordedMessages();
mUserController.dispatchUserSwitch(userState, oldUserId, newUserId);
- verify(observer, times(1)).onBeforeUserSwitching(eq(TEST_USER_ID));
verify(observer, times(1)).onUserSwitching(eq(TEST_USER_ID), any());
// Verify that CONTINUE_USER_SWITCH_MSG is not sent (triggers timeout)
Set<Integer> actualCodes = mInjector.mHandler.getMessageCodes();
@@ -554,6 +549,7 @@
expectedCodes.add(REPORT_USER_SWITCH_COMPLETE_MSG);
if (backgroundUserStopping) {
expectedCodes.add(CLEAR_USER_JOURNEY_SESSION_MSG);
+ expectedCodes.add(0); // this is for directly posting in stopping.
}
if (expectScheduleBackgroundUserStopping) {
expectedCodes.add(SCHEDULED_STOP_BACKGROUND_USER_MSG);
@@ -1579,13 +1575,21 @@
// mock the device to be secure in order to expect the keyguard to be shown
when(mInjector.mKeyguardManagerMock.isDeviceSecure(anyInt())).thenReturn(true);
- // call real showKeyguard method for this test
- doCallRealMethod().when(mInjector).showKeyguard(any());
+ // call real lockDeviceNowAndWaitForKeyguardShown method for this test
+ doCallRealMethod().when(mInjector).lockDeviceNowAndWaitForKeyguardShown();
- mUserController.completeUserSwitch(TEST_USER_ID1, TEST_USER_ID2);
+ // call startUser on a thread because we're expecting it to be blocked
+ Thread threadStartUser = new Thread(()-> {
+ mUserController.startUser(TEST_USER_ID, USER_START_MODE_FOREGROUND);
+ });
+ threadStartUser.start();
- // make sure the switch is stalled by checking the UserSwitchingDialog is not dismissed yet
- verify(mInjector, never()).dismissUserSwitchingDialog(any());
+ // make sure the switch is stalled...
+ Thread.sleep(2000);
+ // by checking REPORT_USER_SWITCH_MSG is not sent yet
+ assertNull(mInjector.mHandler.getMessageForCode(REPORT_USER_SWITCH_MSG));
+ // and the thread is still alive
+ assertTrue(threadStartUser.isAlive());
// mock send the keyguard shown event
ArgumentCaptor<ActivityTaskManagerInternal.ScreenObserver> captor = ArgumentCaptor.forClass(
@@ -1593,42 +1597,12 @@
verify(mInjector.mActivityTaskManagerInternal).registerScreenObserver(captor.capture());
captor.getValue().onKeyguardStateChanged(true);
- // verify the switch now moves on by checking the UserSwitchingDialog is dismissed
- verify(mInjector, atLeastOnce()).dismissUserSwitchingDialog(any());
-
- // verify that SHOW_KEYGUARD_TIMEOUT is ignored and does not crash the system
- try {
- mInjector.mHandler.processPostDelayedCallbacksWithin(
- UserController.SHOW_KEYGUARD_TIMEOUT_MS);
- } catch (RuntimeException e) {
- throw new AssertionError(
- "SHOW_KEYGUARD_TIMEOUT is not ignored and crashed the system", e);
- }
- }
-
- @Test
- public void testRuntimeExceptionIsThrownIfTheKeyguardIsNotShown() throws Exception {
- // enable user switch ui, because keyguard is only shown then
- mUserController.setInitialConfig(/* userSwitchUiEnabled= */ true,
- /* maxRunningUsers= */ 3, /* delayUserDataLocking= */ false,
- /* backgroundUserScheduledStopTimeSecs= */ -1);
-
- // mock the device to be secure in order to expect the keyguard to be shown
- when(mInjector.mKeyguardManagerMock.isDeviceSecure(anyInt())).thenReturn(true);
-
- // suppress showKeyguard method for this test
- doNothing().when(mInjector).showKeyguard(any());
-
- mUserController.completeUserSwitch(TEST_USER_ID1, TEST_USER_ID2);
-
- // verify that the system has crashed
- assertThrows("Should have thrown RuntimeException", RuntimeException.class, () -> {
- mInjector.mHandler.processPostDelayedCallbacksWithin(
- UserController.SHOW_KEYGUARD_TIMEOUT_MS);
- });
-
- // make sure the UserSwitchingDialog is not dismissed
- verify(mInjector, never()).dismissUserSwitchingDialog(any());
+ // verify the switch now moves on...
+ Thread.sleep(1000);
+ // by checking REPORT_USER_SWITCH_MSG is sent
+ assertNotNull(mInjector.mHandler.getMessageForCode(REPORT_USER_SWITCH_MSG));
+ // and the thread is finished
+ assertFalse(threadStartUser.isAlive());
}
private void setUpAndStartUserInBackground(int userId) throws Exception {
@@ -1989,9 +1963,7 @@
Set<Integer> getMessageCodes() {
Set<Integer> result = new LinkedHashSet<>();
for (Message msg : mMessages) {
- if (msg.what != 0) { // ignore mHandle.post and mHandler.postDelayed messages
- result.add(msg.what);
- }
+ result.add(msg.what);
}
return result;
}
@@ -2015,28 +1987,14 @@
@Override
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
- final Runnable cb = msg.getCallback();
- if (cb != null && uptimeMillis <= SystemClock.uptimeMillis()) {
- // run mHandler.post calls immediately
- cb.run();
- return true;
- }
Message copy = new Message();
copy.copyFrom(msg);
- copy.setCallback(cb);
mMessages.add(copy);
- return super.sendMessageAtTime(msg, uptimeMillis);
- }
-
- public void processPostDelayedCallbacksWithin(long millis) {
- final long whenMax = SystemClock.uptimeMillis() + millis;
- for (Message msg : mMessages) {
- final Runnable cb = msg.getCallback();
- if (cb != null && msg.getWhen() <= whenMax) {
- msg.setCallback(null);
- cb.run();
- }
+ if (msg.getCallback() != null) {
+ msg.getCallback().run();
+ msg.setCallback(null);
}
+ return super.sendMessageAtTime(msg, uptimeMillis);
}
}
}
diff --git a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java
index 689b241..abc9ce3 100644
--- a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java
@@ -50,11 +50,12 @@
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertThrows;
+import android.annotation.SuppressLint;
import android.app.ActivityManagerInternal;
import android.app.ActivityOptions.LaunchCookie;
+import android.app.AppOpsManager;
import android.app.KeyguardManager;
import android.content.Context;
-import android.content.ContextWrapper;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.ApplicationInfoFlags;
@@ -72,6 +73,7 @@
import android.platform.test.annotations.EnableFlags;
import android.platform.test.annotations.Presubmit;
import android.platform.test.flag.junit.SetFlagsRule;
+import android.testing.TestableContext;
import android.view.ContentRecordingSession;
import android.view.ContentRecordingSession.RecordContent;
@@ -99,13 +101,14 @@
/**
* Tests for the {@link MediaProjectionManagerService} class.
- *
+ * <p>
* Build/Install/Run:
* atest FrameworksServicesTests:MediaProjectionManagerServiceTest
*/
@SmallTest
@Presubmit
@RunWith(AndroidJUnit4.class)
+@SuppressLint({"UseCheckPermission", "VisibleForTests", "MissingPermission"})
public class MediaProjectionManagerServiceTest {
private static final int UID = 10;
private static final String PACKAGE_NAME = "test.package";
@@ -151,7 +154,10 @@
}
};
- private Context mContext;
+ @Rule
+ public final TestableContext mContext = spy(
+ new TestableContext(InstrumentationRegistry.getInstrumentation().getContext()));
+
private MediaProjectionManagerService mService;
private OffsettableClock mClock;
private ContentRecordingSession mWaitingDisplaySession =
@@ -169,6 +175,8 @@
@Mock
private KeyguardManager mKeyguardManager;
@Mock
+ AppOpsManager mAppOpsManager;
+ @Mock
private IMediaProjectionWatcherCallback mWatcherCallback;
@Mock
private MediaProjectionMetricsLogger mMediaProjectionMetricsLogger;
@@ -185,10 +193,9 @@
LocalServices.removeServiceForTest(WindowManagerInternal.class);
LocalServices.addService(WindowManagerInternal.class, mWindowManagerInternal);
- mContext = spy(new ContextWrapper(
- InstrumentationRegistry.getInstrumentation().getTargetContext()));
- doReturn(mPackageManager).when(mContext).getPackageManager();
- doReturn(mKeyguardManager).when(mContext).getSystemService(eq(Context.KEYGUARD_SERVICE));
+ mContext.addMockSystemService(AppOpsManager.class, mAppOpsManager);
+ mContext.addMockSystemService(KeyguardManager.class, mKeyguardManager);
+ mContext.setMockPackageManager(mPackageManager);
mClock = new OffsettableClock.Stopped();
mWaitingDisplaySession.setWaitingForConsent(true);
@@ -291,6 +298,27 @@
assertThat(mService.getActiveProjectionInfo()).isNotNull();
}
+ @SuppressLint("MissingPermission")
+ @EnableFlags(android.companion.virtualdevice.flags
+ .Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS)
+ @Test
+ public void testCreateProjection_keyguardLocked_AppOpMediaProjection()
+ throws NameNotFoundException {
+ MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
+ doReturn(true).when(mAppOpsManager).isOperationActive(eq(AppOpsManager.OP_PROJECT_MEDIA),
+ eq(projection.uid), eq(projection.packageName));
+ doReturn(true).when(mKeyguardManager).isKeyguardLocked();
+
+ doReturn(PackageManager.PERMISSION_DENIED).when(mPackageManager).checkPermission(
+ RECORD_SENSITIVE_CONTENT, projection.packageName);
+
+ projection.start(mIMediaProjectionCallback);
+ projection.notifyVirtualDisplayCreated(10);
+
+ // The projection was started because it was allowed to capture the keyguard.
+ assertThat(mService.getActiveProjectionInfo()).isNotNull();
+ }
+
@Test
public void testCreateProjection_attemptReuse_noPriorProjectionGrant()
throws NameNotFoundException {
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 96ddf80..b8f9767 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -2815,7 +2815,8 @@
}
@Test
- @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+ @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING,
+ android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST})
public void testOnlyForceGroupIfNeeded_newNotification_notAutogrouped() {
NotificationRecord r = generateNotificationRecord(mTestNotificationChannel, 0, null, false);
when(mGroupHelper.onNotificationPosted(any(), anyBoolean())).thenReturn(false);
@@ -2834,7 +2835,8 @@
}
@Test
- @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+ @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING,
+ android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST})
public void testOnlyForceGroupIfNeeded_newNotification_wasAutogrouped() {
NotificationRecord r = generateNotificationRecord(mTestNotificationChannel, 0, null, false);
when(mGroupHelper.onNotificationPosted(any(), anyBoolean())).thenReturn(true);
diff --git a/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java
index 957b5e0..ae0c6e5 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java
@@ -331,6 +331,7 @@
final WindowProcessController proc = mSystemServicesTestRule.addProcess(
activity.packageName, activity.processName,
6789 /* pid */, activity.info.applicationInfo.uid);
+ assertFalse(proc.mHasEverAttached);
try {
mRootWindowContainer.attachApplication(proc);
verify(mSupervisor).realStartActivityLocked(eq(topActivity), eq(proc),
@@ -338,6 +339,15 @@
} catch (RemoteException e) {
e.rethrowAsRuntimeException();
}
+
+ // Verify that onProcessRemoved won't clear the launching activities if an attached process
+ // is died. Because in real case, it should be handled from WindowProcessController's
+ // and ActivityRecord's handleAppDied to decide whether to remove the activities.
+ assertTrue(proc.mHasEverAttached);
+ assertTrue(mAtm.mStartingProcessActivities.isEmpty());
+ mAtm.mStartingProcessActivities.add(activity);
+ mAtm.mInternal.onProcessRemoved(proc.mName, proc.mUid);
+ assertFalse(mAtm.mStartingProcessActivities.isEmpty());
}
/**
diff --git a/telephony/java/android/telephony/satellite/ISatelliteModemStateCallback.aidl b/telephony/java/android/telephony/satellite/ISatelliteModemStateCallback.aidl
index 66a20ae..50e3a0e 100644
--- a/telephony/java/android/telephony/satellite/ISatelliteModemStateCallback.aidl
+++ b/telephony/java/android/telephony/satellite/ISatelliteModemStateCallback.aidl
@@ -34,4 +34,12 @@
* @param isEmergency True means satellite enabled for emergency mode, false otherwise.
*/
void onEmergencyModeChanged(in boolean isEmergency);
+
+ /**
+ * Indicates that the satellite registration failed with following failure code
+ *
+ * @param causeCode the primary failure cause code of the procedure.
+ * For LTE (EMM), cause codes are TS 24.301 Sec 9.9.3.9
+ */
+ void onRegistrationFailure(in int causeCode);
}
diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java
index 90dae3b..4eefaac 100644
--- a/telephony/java/android/telephony/satellite/SatelliteManager.java
+++ b/telephony/java/android/telephony/satellite/SatelliteManager.java
@@ -19,6 +19,7 @@
import android.Manifest;
import android.annotation.CallbackExecutor;
import android.annotation.FlaggedApi;
+import android.annotation.Hide;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -1579,6 +1580,13 @@
executor.execute(() -> Binder.withCleanCallingIdentity(() ->
callback.onEmergencyModeChanged(isEmergency)));
}
+
+ @Hide
+ @Override
+ public void onRegistrationFailure(int causeCode) {
+ executor.execute(() -> Binder.withCleanCallingIdentity(() ->
+ callback.onRegistrationFailure(causeCode)));
+ }
};
sSatelliteModemStateCallbackMap.put(callback, internalCallback);
return telephony.registerForSatelliteModemStateChanged(internalCallback);
diff --git a/telephony/java/android/telephony/satellite/SatelliteModemStateCallback.java b/telephony/java/android/telephony/satellite/SatelliteModemStateCallback.java
index 423a785..13af469 100644
--- a/telephony/java/android/telephony/satellite/SatelliteModemStateCallback.java
+++ b/telephony/java/android/telephony/satellite/SatelliteModemStateCallback.java
@@ -45,4 +45,13 @@
*/
@FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
default void onEmergencyModeChanged(boolean isEmergency) {};
+
+ /**
+ * Indicates that the satellite registration failed with following failure code
+ *
+ * @param causeCode the primary failure cause code of the procedure.
+ * For LTE (EMM), cause codes are TS 24.301 Sec 9.9.3.9
+ * @hide
+ */
+ default void onRegistrationFailure(int causeCode) {};
}
diff --git a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
index 2a82d5f..351ec463 100644
--- a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
+++ b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
@@ -212,9 +212,10 @@
verify(native).setMotionClassifierEnabled(anyBoolean())
verify(native).setMaximumObscuringOpacityForTouch(anyFloat())
verify(native).setStylusPointerIconEnabled(anyBoolean())
- // Called twice at boot, since there are individual callbacks to update the
- // key repeat timeout and the key repeat delay.
- verify(native, times(2)).setKeyRepeatConfiguration(anyInt(), anyInt())
+ // Called thrice at boot, since there are individual callbacks to update the
+ // key repeat timeout, the key repeat delay and whether key repeat enabled.
+ verify(native, times(3)).setKeyRepeatConfiguration(anyInt(), anyInt(),
+ anyBoolean())
}
@Test
diff --git a/core/tests/coretests/src/android/tracing/perfetto/DataSourceTest.java b/tests/Tracing/src/android/tracing/perfetto/DataSourceTest.java
similarity index 100%
rename from core/tests/coretests/src/android/tracing/perfetto/DataSourceTest.java
rename to tests/Tracing/src/android/tracing/perfetto/DataSourceTest.java
diff --git a/core/tests/coretests/src/android/tracing/perfetto/TestDataSource.java b/tests/Tracing/src/android/tracing/perfetto/TestDataSource.java
similarity index 100%
rename from core/tests/coretests/src/android/tracing/perfetto/TestDataSource.java
rename to tests/Tracing/src/android/tracing/perfetto/TestDataSource.java
diff --git a/tests/Tracing/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java b/tests/Tracing/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java
index e841d9e..cfb2645 100644
--- a/tests/Tracing/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java
+++ b/tests/Tracing/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java
@@ -16,6 +16,8 @@
package com.android.internal.protolog;
+import static android.tools.traces.Utils.executeShellCommand;
+
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
@@ -42,12 +44,13 @@
import androidx.test.platform.app.InstrumentationRegistry;
-import com.android.internal.protolog.ProtoLogConfigurationService.ViewerConfigFileTracer;
+import com.android.internal.protolog.ProtoLogConfigurationServiceImpl.ViewerConfigFileTracer;
import com.android.internal.protolog.common.IProtoLogGroup;
import com.android.internal.protolog.common.LogDataType;
import com.android.internal.protolog.common.LogLevel;
import com.google.common.truth.Truth;
+import com.google.protobuf.InvalidProtocolBufferException;
import org.junit.After;
import org.junit.Before;
@@ -57,12 +60,14 @@
import org.junit.runners.JUnit4;
import org.mockito.Mockito;
+import perfetto.protos.PerfettoConfig.TracingServiceState;
import perfetto.protos.Protolog;
import perfetto.protos.ProtologCommon;
import java.io.File;
import java.io.IOException;
import java.util.List;
+import java.util.Optional;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;
@@ -166,7 +171,8 @@
return new ProtoInputStream(sViewerConfigBuilder.build().toByteArray());
});
};
- sProtoLogConfigurationService = new ProtoLogConfigurationService(dataSourceBuilder, tracer);
+ sProtoLogConfigurationService =
+ new ProtoLogConfigurationServiceImpl(dataSourceBuilder, tracer);
if (android.tracing.Flags.clientSideProtoLogging()) {
sProtoLog = new PerfettoProtoLogImpl(
@@ -177,6 +183,8 @@
viewerConfigInputStreamProvider, sReader, () -> sCacheUpdater.run(),
TestProtoLogGroup.values(), dataSourceBuilder, sProtoLogConfigurationService);
}
+
+ waitDataSourceIsAvailable();
}
@Before
@@ -862,6 +870,54 @@
.isEqualTo("This message should also be logged 567");
}
+ private static void waitDataSourceIsAvailable() {
+ final int timeoutMs = 10000;
+ final int busyWaitIntervalMs = 100;
+
+ int elapsedMs = 0;
+
+ while (!isDataSourceAvailable()) {
+ SystemClock.sleep(busyWaitIntervalMs);
+ elapsedMs += busyWaitIntervalMs;
+ if (elapsedMs >= timeoutMs) {
+ throw new RuntimeException("Data source didn't become available."
+ + " Waited for: " + timeoutMs + " ms");
+ }
+ }
+ }
+
+ private static boolean isDataSourceAvailable() {
+ byte[] proto = executeShellCommand("perfetto --query-raw");
+
+ try {
+ TracingServiceState state = TracingServiceState.parseFrom(proto);
+
+ Optional<Integer> producerId = Optional.empty();
+
+ for (TracingServiceState.Producer producer : state.getProducersList()) {
+ if (producer.getPid() == android.os.Process.myPid()) {
+ producerId = Optional.of(producer.getId());
+ break;
+ }
+ }
+
+ if (producerId.isEmpty()) {
+ return false;
+ }
+
+ for (TracingServiceState.DataSource ds : state.getDataSourcesList()) {
+ if (ds.getDsDescriptor().getName().equals(TEST_PROTOLOG_DATASOURCE_NAME)
+ && ds.getProducerId() == producerId.get()) {
+ return true;
+ }
+ }
+ } catch (InvalidProtocolBufferException e) {
+ throw new RuntimeException(e);
+ }
+
+ return false;
+ }
+
private enum TestProtoLogGroup implements IProtoLogGroup {
TEST_GROUP(true, true, false, "TEST_TAG");
diff --git a/tests/Tracing/src/com/android/internal/protolog/ProtoLogConfigurationServiceTest.java b/tests/Tracing/src/com/android/internal/protolog/ProtoLogConfigurationServiceTest.java
index e1bdd77..a3d03a8 100644
--- a/tests/Tracing/src/com/android/internal/protolog/ProtoLogConfigurationServiceTest.java
+++ b/tests/Tracing/src/com/android/internal/protolog/ProtoLogConfigurationServiceTest.java
@@ -150,11 +150,11 @@
@Test
public void canRegisterClientWithGroupsOnly() throws RemoteException {
- final ProtoLogConfigurationService service = new ProtoLogConfigurationService();
+ final ProtoLogConfigurationService service = new ProtoLogConfigurationServiceImpl();
- final ProtoLogConfigurationService.RegisterClientArgs args =
- new ProtoLogConfigurationService.RegisterClientArgs()
- .setGroups(new ProtoLogConfigurationService.RegisterClientArgs
+ final ProtoLogConfigurationServiceImpl.RegisterClientArgs args =
+ new ProtoLogConfigurationServiceImpl.RegisterClientArgs()
+ .setGroups(new ProtoLogConfigurationServiceImpl.RegisterClientArgs
.GroupConfig(TEST_GROUP, true));
service.registerClient(mMockClient, args);
@@ -165,11 +165,11 @@
@Test
public void willDumpViewerConfigOnlyOnceOnTraceStop()
throws RemoteException, InvalidProtocolBufferException {
- final ProtoLogConfigurationService service = new ProtoLogConfigurationService();
+ final ProtoLogConfigurationService service = new ProtoLogConfigurationServiceImpl();
- final ProtoLogConfigurationService.RegisterClientArgs args =
- new ProtoLogConfigurationService.RegisterClientArgs()
- .setGroups(new ProtoLogConfigurationService.RegisterClientArgs
+ final ProtoLogConfigurationServiceImpl.RegisterClientArgs args =
+ new ProtoLogConfigurationServiceImpl.RegisterClientArgs()
+ .setGroups(new ProtoLogConfigurationServiceImpl.RegisterClientArgs
.GroupConfig(TEST_GROUP, true))
.setViewerConfigFile(mViewerConfigFile.getAbsolutePath());
service.registerClient(mMockClient, args);
@@ -200,13 +200,13 @@
@Test
public void willDumpViewerConfigOnLastClientDisconnected()
throws RemoteException, FileNotFoundException {
- final ProtoLogConfigurationService.ViewerConfigFileTracer tracer =
- Mockito.mock(ProtoLogConfigurationService.ViewerConfigFileTracer.class);
- final ProtoLogConfigurationService service = new ProtoLogConfigurationService(tracer);
+ final ProtoLogConfigurationServiceImpl.ViewerConfigFileTracer tracer =
+ Mockito.mock(ProtoLogConfigurationServiceImpl.ViewerConfigFileTracer.class);
+ final ProtoLogConfigurationService service = new ProtoLogConfigurationServiceImpl(tracer);
- final ProtoLogConfigurationService.RegisterClientArgs args =
- new ProtoLogConfigurationService.RegisterClientArgs()
- .setGroups(new ProtoLogConfigurationService.RegisterClientArgs
+ final ProtoLogConfigurationServiceImpl.RegisterClientArgs args =
+ new ProtoLogConfigurationServiceImpl.RegisterClientArgs()
+ .setGroups(new ProtoLogConfigurationServiceImpl.RegisterClientArgs
.GroupConfig(TEST_GROUP, true))
.setViewerConfigFile(mViewerConfigFile.getAbsolutePath());
service.registerClient(mMockClient, args);
@@ -225,10 +225,10 @@
@Test
public void sendEnableLoggingToLogcatToClient() throws RemoteException {
- final var service = new ProtoLogConfigurationService();
+ final var service = new ProtoLogConfigurationServiceImpl();
- final var args = new ProtoLogConfigurationService.RegisterClientArgs()
- .setGroups(new ProtoLogConfigurationService.RegisterClientArgs
+ final var args = new ProtoLogConfigurationServiceImpl.RegisterClientArgs()
+ .setGroups(new ProtoLogConfigurationServiceImpl.RegisterClientArgs
.GroupConfig(TEST_GROUP, false));
service.registerClient(mMockClient, args);
@@ -242,11 +242,11 @@
@Test
public void sendDisableLoggingToLogcatToClient() throws RemoteException {
- final ProtoLogConfigurationService service = new ProtoLogConfigurationService();
+ final ProtoLogConfigurationService service = new ProtoLogConfigurationServiceImpl();
- final ProtoLogConfigurationService.RegisterClientArgs args =
- new ProtoLogConfigurationService.RegisterClientArgs()
- .setGroups(new ProtoLogConfigurationService.RegisterClientArgs
+ final ProtoLogConfigurationServiceImpl.RegisterClientArgs args =
+ new ProtoLogConfigurationServiceImpl.RegisterClientArgs()
+ .setGroups(new ProtoLogConfigurationServiceImpl.RegisterClientArgs
.GroupConfig(TEST_GROUP, true));
service.registerClient(mMockClient, args);
@@ -260,11 +260,11 @@
@Test
public void doNotSendLoggingToLogcatToClientWithoutRegisteredGroup() throws RemoteException {
- final ProtoLogConfigurationService service = new ProtoLogConfigurationService();
+ final ProtoLogConfigurationService service = new ProtoLogConfigurationServiceImpl();
- final ProtoLogConfigurationService.RegisterClientArgs args =
- new ProtoLogConfigurationService.RegisterClientArgs()
- .setGroups(new ProtoLogConfigurationService.RegisterClientArgs
+ final ProtoLogConfigurationServiceImpl.RegisterClientArgs args =
+ new ProtoLogConfigurationServiceImpl.RegisterClientArgs()
+ .setGroups(new ProtoLogConfigurationServiceImpl.RegisterClientArgs
.GroupConfig(TEST_GROUP, false));
service.registerClient(mMockClient, args);
@@ -277,15 +277,15 @@
@Test
public void handlesToggleToLogcatBeforeClientIsRegistered() throws RemoteException {
- final ProtoLogConfigurationService service = new ProtoLogConfigurationService();
+ final ProtoLogConfigurationService service = new ProtoLogConfigurationServiceImpl();
Truth.assertThat(service.getGroups()).asList().doesNotContain(TEST_GROUP);
service.enableProtoLogToLogcat(TEST_GROUP);
Truth.assertThat(service.isLoggingToLogcat(TEST_GROUP)).isTrue();
- final ProtoLogConfigurationService.RegisterClientArgs args =
- new ProtoLogConfigurationService.RegisterClientArgs()
- .setGroups(new ProtoLogConfigurationService.RegisterClientArgs
+ final ProtoLogConfigurationServiceImpl.RegisterClientArgs args =
+ new ProtoLogConfigurationServiceImpl.RegisterClientArgs()
+ .setGroups(new ProtoLogConfigurationServiceImpl.RegisterClientArgs
.GroupConfig(TEST_GROUP, false));
service.registerClient(mMockClient, args);
diff --git a/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityManager.java b/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityManager.java
index f68ae2c..fc4a909 100644
--- a/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityManager.java
+++ b/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityManager.java
@@ -173,6 +173,10 @@
}
}
}
+
+ Executor getExecutor() {
+ return mExecutor;
+ }
}
private ISharedConnectivityService mService;
@@ -188,7 +192,7 @@
private final String mServicePackageName;
private final String mIntentAction;
private ServiceConnection mServiceConnection;
- private UserManager mUserManager;
+ private final UserManager mUserManager;
/**
* Creates a new instance of {@link SharedConnectivityManager}.
@@ -316,15 +320,19 @@
private void registerCallbackInternal(SharedConnectivityClientCallback callback,
SharedConnectivityCallbackProxy proxy) {
- try {
- mService.registerCallback(proxy);
- synchronized (mProxyDataLock) {
- mProxyMap.put(callback, proxy);
- }
- } catch (RemoteException e) {
- Log.e(TAG, "Exception in registerCallback", e);
- callback.onRegisterCallbackFailed(e);
- }
+ proxy.getExecutor().execute(
+ () -> {
+ try {
+ mService.registerCallback(proxy);
+ synchronized (mProxyDataLock) {
+ mProxyMap.put(callback, proxy);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Exception in registerCallback", e);
+ callback.onRegisterCallbackFailed(e);
+ }
+ }
+ );
}
/**